您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   Code  
会员   
   
 
     
   
 订阅
  捐助
小模块解决大痛点—nginx+lua托底数据解决方案
 
作者:马顺风
   次浏览      
 2019-10-18
 
编辑推荐:
本文主要介绍通过使用aop原理基于nginx+lua的一种组件化托底方法,来解决业务系统的痛点。
本文来自于亿级流量网站架构,由火龙果软件Alice编辑、推荐。

随着京东商城的发展,内部也出现了一些比较有意思的小系统小模块来解决一些业务系统的痛点,而这些小系统小模块虽说不复杂但是解决了当时的痛点。数据托底就是其中一个痛点,因为依赖系统或者其他方面的不稳定性导致用户访问页面是404或者503、或者出现天窗(页面局部内容没出来),这在一个大流量系统中是不允许的。因此就需要更健壮的系统设计来解决此问题,解决此问题的方法大家又都是类似的,因此作者就抽象了一个小模块来解决更多人的兜底问题。

大多数以读为主的系统为了提高系统的可用性,会用到各种策略来增强用户体验;其中数据托底就是其中一种策略;数据托底也可以叫做数据兜底,一般来解决如下几个问题:保证数据”永不消失”(不能开天窗),即假设依赖的服务出问题了,内容还可以展示给用户;保证数据的正确性,如果以来的服务数据不正确,可以暂时走托底数据;甚至要保证托底数据的高性能,大促时可以直接走托底数据,因为托底数据一般会通过缓存或者静态化技术完成。

实际项目中数据托底的方式也是多种多样,最简单直接的一种方式是将托底功能完全耦合在自己业务系统中,后续每开发一个系统,甚至每增加一种业务功能都要重新实现一次托底功能,并且当系统本身挂掉后托底也就无能为力了。

为了减少代码的冗余,降低代码的维护成本,可以把这块功能抽象数出来(如果业务系统是java语言,可以以jar包的形式提供服务),然后收集出所有需要托底的业务(可以收集url、业务方法等),将其放到一个配置文件中,用worker去更新托底数据;比如京东三级列表页通过Worker去爬所有页面的内容然后静态化存储,即存储整个HTML片段,当动态列表页依赖的服务出现问题了,则直接走托底数据;假设有些页面没有爬到,可以将列表页第一页作为托底数据返回。这种形式显然要比第一种好许多(比如耦合性降低),但仍然没有足够好。首先他需要准确的知道是哪些业务要托底(明确url或方法入参),不能跨语言使用,系统本身挂掉后托底也会失败。

另一种方式是将这块功能完全独立成一个系统,并且以http的形式和目标系统通信。假设目标系统有10个页面需要做托底处理,那么我们可以将这10个页面的url告诉托底系统,托底系统可以定时的去抓取我们的页面,然后将正确的数据放到存储设备上,目标系统可以通过nginx做判断决定如何以及何时去读取这些数据。可以看到这种方式可以做到对目标系统无侵入,也可以跨语言,并且即使目标系统挂掉只要nginx不挂就仍然可以提供服务。但是这种方法无法动态获取要托底的资源,他在抓取托底数据时需要明确url,试想如果目标系统有几亿的页面要托底,根本没法告知托底系统;多个目标系统依赖同一个托底系统,会有单点风险。

可以看到以上方法都或多或少有些缺陷,因此为了更好地解决这些问题。

该方法具有以下几个特性:

a. 对目标系统零侵入

b. 可动态拦截请求(不需要预先配置url)

c. 可以选择数据存储设备

d. 可以选择何时更新托底数据

e. 可以校验托底数据

f. 记录性能日志

g. 配置简单

数据流向图:

具体执行过程:

1.当用户发起请求时,该组件将其拦截,然后由该组件负责向后端发起请求;在这个过程中我们可以对请求进行限流,这样就可以有效的保护后端服务器,这边我用的是lua-resty-lock和lua-resty-limit-traffic实现的。 lua-resty-lock是一个基于nginx的时间事件实现的非阻塞锁,使用他可以有效的防止dog-pile效应;lua-resty-limit-traffic基于漏桶算法实现的限流组件。

2.如果回源失败则直接获取托底数据并返回;回源成功则继续向下走。

3.回源成功后需要检查数据是否正确,比如校验数据格式是否正确、是否缺少某个字段、是否缺少某个html元素等,这些校验器可以根据各自的业务自行扩展。如果校验失败则字节取托底数据并返回;校验成功则继续向下走。

4.检查是否可以更新托底数据。这里有三种策略可供选择,一种是实时更新,也就是每次请求都要更新一次托底数据;一种是每隔多长时间更新一次;最后一种是按照固定访问次数来更新。第一种策略实现简单,第二种和第三种都需要为请求打上标识。

用第二种方式解释如何实现,可以使用nginx的共享字典存储标记,伪代码如下:

# 配置共享缓存 lua_shared_dict dict 100m;

# 伪代码
local key = uri + querystring;
local ok = dict:get(key);
if ok then
-- key存在,说明未到时间,不可更新
return no;
end
dict:set(key,间隔更新时间);
return yes;

5.如果不需要更新则直接将回源数据返回,如果需要更新则继续向下走,到第6步。

6.这一步会将回源的响应数据存入到托底存储中。托底存储可以是redis、memcached、mysql、nginx共享缓存、本地文件等,分别使用lua-resty-redis、lua-resty-memcached、lua-resty-mysql、lua_shared_dict、popen来实现。所有的托底存储都要实现规定的动作,比如get、set、del等动作。

使用redis存储的实现伪代码如下:

local db = require "lib.redis";
local var = ngx.var;
local _M = {};
function _M.get(key)
……
local ok, res, err = db.excute(red, "get", key);
if not ok then return nil; end ……
return res;end

function _M.set(key, time, value)
……
local ok, res, err = db.excute(red,"setex", key, time, value);
……
end

return _M;

另外在请求过程、请求回源、请求托底时都会记录性能日志,以备后续分析接口性能和报警使用。

使用方法

对目标系统无侵入,使用简单;但是Nginx需要集成Lua功能才能使用,如OpenResty,首先在nginx.conf中配置如下指令:

lua_package_path "/yourpath/lua/?.lua;;"; lua_shared_dict demo 100m;

server {
location ~ ^/demo {
content_by_lua '
local bottom = require "bottom";
-- 设置存储类型为nginx的共享缓存
bottom.env("store_type", "nginx共享缓存");
bottom.set_env("store_name", "demo");
bottom.set_env("bottom_key", “指定托底key”);
local data = bottom.get_data();
ngx.print(data);
';
}

#backend是agent回源时的请求前缀,以便前端请求和后端请求的uri区分开来
location ~ ^/backend/demo $ {
internal;
proxy_pass http://目标系统;
}

注:/backend/demo是/demo的回源调用uri,即此处要按照约定写回源uri。

该实现暂时没有开源出去,有兴趣的可以留言联系作者私聊。该模块是纯Lua实现,如下是该模块的简介:

总结

从以上描述可以看到, 该组件对目标系统无任何侵入性;使用简单,只需要在nginx层做一些简单的配置;并且只要nginx不挂,目标系统即使挂掉仍然可以提供服务。整体思路其实很简单,就是在目标系统外加一层代理,然后我们就可以在代理层做各种事情;即总体思想是遵循AOP。目前京东一些频道页、三峡项目在使用该方案。

 

   
次浏览       
????

HTTP????
nginx??????
SD-WAN???
5G?????
 
????

??????????
IPv6???????
??????????
???????
????

????????
????????
???????????????
??????????