需求
- 在实现了动态路由选路后,也实现了基于tag的选路,同时还能进行降级
- 基于此,现在出现了其他需求,需要基于现有的模式下,实现部署的服务模式改为精简版,而非微服务版,这样就需要全量替换原先的key-value键值对
- 服务的精简版和微服务版,普通模式下,需要配置两套nginx的conf,如何将两套进行融合,同时能够快速的切换,是此需求的重点
思路
- 由于入口仍然使用
^~ /api/xx
进入,所以微服务版本和精简版本是无法同时存在的,只能进行手动或半自动切换 - 为了解决此需求,需要设置一个开关,通过开关来进行选择使用哪种模式进行动态路由的转发
- 当有了开关,就可以为后续扩展留好口子,保证以后的复杂业务和需求
实现方式
路由模式切换
- 具体做法是,在 redis 中塞入一个键值对,value值是integer,从0开始,每个value对应一种模式
ECHO =========dynamic_routing===========
HSET dynamic_routing type 0 # 微服务模式
HSET dynamic_routing type 1 # 精简模式
# .......................... # 可扩展
键值对调整
- 由于不同模式下,路由对应的后端服务名会变化,所以整体插入redis的键值对也需要全量调整
- 键值对需要根据业务需求,准确的进行调整并多次测试
精简版模式下的key-value, dynamic_api_hash_share.txt
ECHO =========dynamic_routing===========
HSET dynamic_routing type 1
ECHO =========app===========
HMSET app standardOpen dmo-apaas-rest-share.default.svc.cluster.local/lego-standard-openapi-rest
微服务版模式下的key-value,dynamic_api_hash.txt
ECHO =========dynamic_routing===========
HSET dynamic_routing type 0
ECHO =========app===========
HMSET app standardOpen dmo-lego-standard-openapi
支持动态调整,只是要 redis 中导入 key-value 就能够动态切换
cat dynamic_api_hash.txt | redis-cli -h xxxx -p 6379
cat dynamic_api_hash_share.txt | redis-cli -h xxxx -p 6379
Lua代码适配
- 基本逻辑是从 redis 中获取 dynamic_routing 中 type的值
- 根据type值,进行条件判断,走对应的代码逻辑
- 精简模式下目前不管tag,不做降级
Dynamic_location_routing.lua
- 由于从redis中获取的value包括host和一部分path,所以在传给target后,proxy_pass,不会带上之前的
uri
和args
,所以target最后需要将这两个参数拼接,使用ngx.var.request_uri
,此参数包含path和args
-- 基于nginx和lua的懂动态路由选路
-- 根据 location 第二段 /api/xxx ,来转发到对应名称的后端FQDN
-- 动态路由支持微服务版 0 和精简版 1 (支持其余自定义模板)
-- 支持路由根据userTag进行选路
-- 支持路由降级
-- yunjie.xiao@clickpaas.com
-- 局部变量
-- 降级默认地址
local degrade_target = "dmo-apaas-gateway.default.svc.cluster.local"
-- 分割字符串,局部函数
local function split( str,reps )
local resultStrList = {}
string.gsub(str,'[^'..reps..']+',function ( w )
table.insert(resultStrList,w)
end)
return resultStrList
end
-- 通过请求uri,提取接口字符串,并从redis里获取对应的值
local function get_value()
local uri_data = ngx.var.uri
local key = split(ngx.var.host, '.')[1]
local field = split(uri_data, '/')[2]
local db = 0
local res = ngx.location.capture("/redis_hget", { args = { key = key , field = field , db = db } })
-- redis取值状态判断
if res.status ~= 200 then
ngx.log(ngx.ERR, "【redis server returned bad status】: ", res.status)
ngx.exit(res.status)
end
-- redis取值内容非空判断
if not res.body then
ngx.log(ngx.ERR, "【redis returned empty body】")
ngx.exit(500)
end
-- redis解析,多值返回 (redis无密码)
local parser = require "redis.parser"
local results = parser.parse_replies(res.body, 2)
for i, result in ipairs(results) do
if i == 2 then
local server = result[1]
local typ = result[2]
-- 检查结果类型
if typ ~= parser.BULK_REPLY or not server then
ngx.log(ngx.ERR, "【bad redis response】: ", res.body)
ngx.exit(500)
end
return server
end
end
end
local function get_target(server_type, server)
-- 判断动态路由使用哪种模式(微服务版 0 和精简版 1 )
ngx.log(ngx.INFO, "【server_type_tonumber】:", tonumber(server_type))
if tonumber(server_type) == 1 then
-- 精简版
ngx.var.target = server .. ngx.var.request_uri
return
else
local default_fqdn = ".default.svc.cluster.local"
local target = server .. default_fqdn
-- cookie_userTag
local upstream = require "ngx.upstream"
local get_servers = upstream.get_servers
local userTag = ngx.var.cookie_userTag
-- cookie_userTag 检测
if( userTag ~= nil ) and ( userTag ~= "" ) then
ngx.log(ngx.INFO, "【userTag】: ", userTag)
local cookie_target = server .. "-" .. userTag .. default_fqdn
ngx.log(ngx.INFO, "【cookie_target】: ", cookie_target)
local servers, err = get_servers(cookie_target)
if( servers ~= nil ) then
ngx.var.target = cookie_target
return
else
ngx.log(ngx.ERR, "【failed to get servers in upstream】: ", err)
-- 降级
ngx.var.target = degrade_target
return
end
else
ngx.var.target = target
return
end
end
end
-- 从redis中获取动态路由模式
local key_type = "dynamic_routing"
local field_type = "type"
local db_type = 0
local res_type = ngx.location.capture("/redis_hget", { args = { key = key_type , field = field_type , db = db_type } })
-- redis取值状态判断
if res_type.status ~= 200 then
ngx.log(ngx.ERR, "【tengine_lua get redis type , returned bad status】: ", res_type.status)
ngx.exit(res_type.status)
end
-- redis取值内容非空判断
if not res_type.body then
ngx.log(ngx.ERR, "【tengine_lua get redis type , returned empty body】")
ngx.exit(500)
end
-- redis解析,多值返回,返回动态路由的模式(微服务版0和精简版1) (redis无密码)
local parser_type = require "redis.parser"
local results_type = parser_type.parse_replies(res_type.body, 2)
for x, result_type in ipairs(results_type) do
if x == 2 then
local server_type = result_type[1]
local typ_type = result_type[2]
-- 检查结果类型
if typ_type ~= parser_type.BULK_REPLY or not server_type then
ngx.log(ngx.ERR, "【tengine_lua bad redis response】: ", res_type.body)
ngx.exit(500)
end
local server = get_value()
get_target(server_type, server)
end
end
ngx.log(ngx.INFO, "【target】: ", ngx.var.target)
nginx 配置
server {
listen 80;
location = /redis_hget {
internal;
set_unescape_uri $key $arg_key;
set_unescape_uri $field $arg_field;
set_unescape_uri $db $arg_db;
redis2_query select $db;
redis2_query hget $key $field;
redis2_pass 127.0.0.1:6379;
}
# Dynamic_location_routing
location ^~ /api/ {
set $target '';
access_by_lua_file lua/Dynamic_location_routing.lua;
proxy_pass http://$target;
}
FAQ
- 对于多种模式下的调整,在编译tengine时,无需新增resty的依赖
- 实现动态路由切换,只需要更换redis的key-value值,调整方便
- 动态切换,无需软重启nginx,平滑过度
- 动态切换时间短,秒级切换,时间取决于key-value插入redis的时间