使用nginx+lua+redis实现服务降级
服务降级介绍
面对一大波访问量出乎意料地涌入,超出了系统正常负载范围,可以临时采用服务降级来保证基础服务的运行。
降级的最终目的是保证核心服务的高可用。过程就是丢卒保帅,有些服务是无法降级的,比如支付。
降级的种类
1.设置开关位置实现降级
根据降级的开关位置:分为服务代码降级和开关前置降级。
代码降级就是利用代码控制,这种方式比前置降级要low并不推荐;
前置降级是把降级开关放到http请求链路层的上游,降低链路层消耗。
比如提升到nginx,甚至可以提升到前端,当提升到前端,后端访问压力接近于0
拓展:【如何提升到前端】可以通过一个从服务器获取的js脚本进行控制。
2.读写降级
根据读写:分为读降级和写降级。
读降级,比如,读取动态数据,降级为读取静态数据。
写降级,比如,写入mysql,降级为写入消息队列, 等高峰期过后,再从队列写入mysql。
3.返回内容、限流、限速降级
根据降级的性质:分为返回内容降级,限流降级,限速降级。
返回内容降级,比如,返回实时数据,降级为返回静态缓存数据
限流降级,比如,1000个请求,我只接受500个。 这么做也是无奈之下选择,要不然系统崩了,谁都访问不了
限速降级,比如,对于那些访问过于频繁的ip进行限速
拓展:【限流限速】
nginx 自带限流限速模块,比如ngx_http_limit_req_module和ngx_http_limit_conn_module 。
但是这类模块只是提供了在nginx配置文件中进行简单的参数配置。
如果想更加灵活和功能更多,可以编写自己的lua代码进行控制,而不是用现成的模块进行配置文件修改。
4.手动和自动降级
手动降级,是人为看到系统负载异常后,手动调整降级;
自动降级,是系统监测到异常后自动降级,自动降级虽然更智能,但有时候自动脚本可能会干一些超乎预料的事情。
降级代码实例
本次降级实现过程原理:
采用nginx+lua+redis对广告推荐模块实现前置降级,读降级,内容降级,手动降级(附自动降级)。
利用一个降级开关,以这个开关为判断依据,切换数据的获取方式,
比如当mysql负载高的时候,可以从mysql切换到redis,
比如从redis切换到静态文件,比如从错误频发的新版本切换到老版本等等。
这个开关是根据现状来配置的,比如当新版本错误频发的时候,我们可以配置这个开关为从老版本获取数据。
1.配置nginx
/etc/nginx/nginx.conf
user nobody;
worker_processes 1;
events {
worker_connections 1024;
}
http {
lua_package_path "/usr/share/lua/5.1/lua-resty-redis/lib/?.lua;;/usr/share/lua/5.1/luaresty-redis-cluster/lib/resty‘7/?.lua;;";
lua_package_cpath "/usr/share/lua/5.1/lua-resty-redis-cluster/lib/libredis_slot.so;;";
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name 127.0.0.1;
server_name 192.168.232.100;
#获取广告推荐数据
location /goods_list_advert {
default_type 'application/x-javascript;charset=utf-8';
content_by_lua_file /etc/nginx/lua/goods_list_advert.lua;
}
#从服务层+mysql获取数据
location /goods_list_advert_from_data {
default_type 'application/x-javascript;charset=utf-8';
content_by_lua '
ngx.say("从服务层+mysql获取数据")
';
}
}
}
2.lua降级代码
/etc/nginx/lua/goods_list_advert.lua
--获取get或post参数--------------------
local request_method = ngx.var.request_method
local args = nil
local param = nil
--获取参数的值
if "GET" == request_method then
args = ngx.req.get_uri_args()
elseif "POST" == request_method then
ngx.req.read_body()
args = ngx.req.get_post_args()
end
sku_id = args["sku_id"]
--关闭redis的函数--------------------
local function close_redis(redis_instance)
if not redis_instance then
return
end
local ok,err = redis_instance:close();
if not ok then
ngx.say("close redis error : ",err);
end
end
--连接redis--------------------
local redis = require("resty.redis");
--local redis = require "redis"
-- 创建一个redis对象实例。在失败,返回nil和描述错误的字符串的情况下
local redis_instance = redis:new();
--设置后续操作的超时(以毫秒为单位)保护,包括connect方法
redis_instance:set_timeout(1000)
--建立连接
local ip = '127.0.0.1'
local port = 6379
--尝试连接到redis服务器正在侦听的远程主机和端口
local ok,err = redis_instance:connect(ip,port)
if not ok then
ngx.say("connect redis error : ",err)
return close_redis(redis_instance);
end
--从redis里面读取开关--------------------
local key = "level_goods_list_advert"
local switch, err = redis_instance:get(key)
if not switch then
ngx.say("get msg error : ", err)
return close_redis(redis_instance)
end
--得到的开关为空处理--------------------
if switch == ngx.null then
switch = "FROM_DATA" --比如默认值
end
--当开关是要从服务中获取数据时--------------------
if "FROM_DATA" == switch then
ngx.exec('/goods_list_advert_from_data');
--当开关是要从缓存中获取数据时--------------------
elseif "FROM_CACHE" == switch then
local resp, err = redis_instance:get("nihao")
ngx.say(resp)
--当开关是要从静态资源中获取数据时--------------------
elseif "FROM_STATIC" == switch then
ngx.header.content_type="application/x-javascript;charset=utf-8"
local file = "/etc/nginx/html/goods_list_advert.json"
local f = io.open(file, "rb")
local content = f:read("*all")
f:close()
ngx.print(content)
--当开关是要停掉数据获取时--------------------
elseif "SHUT_DOWN" == switch then
ngx.say('no data')
end
3.验证降级测试
登录redis客户端,设置降级配置 set level_goods_list_advert FROM_DATA
这里我们采用手工操作redis的方式,实际上一般是通过管理后台页面来操作的redis进行配置。
浏览器访问http://192.168.3.66/goods_list_advert
4.自动降级
在/etc/nginx/lua/goods_list_advert.lua中添加如下代码:
--判断错误的响应,并进行计数, 后续便可以参考这个数值进行降级
if tonumber(ngx.var.status) == 200 then
ngx.say(ngx.var.status)
ngx.log(ngx.ERR,"upstream reponse status is " .. ngx.var.status .. ",please notice it")
local error_count, err = redis_instance:get("error_count_goods_list_advert")
local error_count_max = redis_instance:get("error_count_max")
--得到的数据为空处理
if error_count == ngx.null then
error_count = 0
end
error_count = error_count + 1
--当错误次数达到设定阀值时,降级为请求老版本
if (error_count >= error_count_max) then
ngx.exec('/goods_list_advert_from_old_ver')
else
--设定error_count到error_count_goods_list_advert
local resp,err = redis_instance:set("error_count_goods_list_advert",error_count)
if not resp then
ngx.say("set msg error : ",err)
return close_redis(redis_instance)
end
end
end
对错误返回进行统计,lua中判断当统计到的错误次数达到设定阀值时,降级为请求另外一个版本(老版本程序)。
nginx漏桶原理改进版
上面是普通的nginx漏桶原理,保证对微服务+mysql是平缓的。超出桶容量的请求将被拒绝。
由于桶很小,很多请求会被拒绝掉,用户体验非常不好。服务器抗并发能力也并不高。
下面是nginx+lua实现的多级缓存降级,可以在多种降级方案(mysql,nosql,静态文件)中自由切换,
配备多个出水口,这样桶的容量和每秒出水量都增加了不少。
到git下载自动降级的 lua-resty-limit-traffic 模块
https://github.com/openresty/lua-resty-limit-traffic
nginx漏桶原理改进版自动降级lua代码:
-- 加载nginx—lua限流模块
local limit_req = require "resty.limit.req"
-- 这里设置rate=50个请求/每秒,漏桶桶容量设置为1000个请求
-- 因为模块中控制粒度为毫秒级别,所以可以做到毫秒级别的平滑处理
local lim, err = limit_req.new("my_limit_req_store", 50, 1000)
if not lim then
ngx.log(ngx.ERR, "failed to instantiate a resty.limit.req object: ", err)
return ngx.exit(501)
end
local key = ngx.var.binary_remote_addr
local delay, err = lim:incoming(key, true)
ngx.say(delay)
if ( delay <0 or delay==nil ) then
return ngx.exit(502)
end
-- delay值就是当前这个请求的等待时长,这个时长是通过resty.limit.req模块计算出来的
-- 1000以外的就溢出
if not delay then
if err == "rejected" then
return ngx.say("1000以外的就溢出")
-- return ngx.exit(502)
end
ngx.log(ngx.ERR, "failed to limit req: ", err)
return ngx.exit(502)
end
-- 50-100的等待从微服务+mysql获取实时数据;(100-50)/50 =1
if ( delay >0 and delay <=1 ) then
ngx.sleep(delay)
-- 100-400的直接从redis获取实时性略差的数据;(400-50)/50 =7
elseif ( delay >1 and delay <=7 ) then
local resp, err = redis_instance:get("redis_goods_list_advert")
ngx.say(resp)
return
-- 400-1000的从静态文件获取实时性非常低的数据(1000-50)/50 =19
elseif ( delay >7) then
ngx.header.content_type="application/x-javascript;charset=utf-8"
local file = "/etc/nginx/html/goods_list_advert.json"
local f = io.open(file, "rb")
local content = f:read("*all")
f:close()
ngx.print(content)
return
end
ngx.say("进入查询微服务+mysql")
最后可以使用jemter软件进行模拟并发压力测试。