公司业务需求 需要在新功能上线前给一部分用户作测试
网上查到 可以使用openresty 较为快速且侵入较小的实现
过程为不同用户浏览网站时, nginx获取到userId, 根据预先指定的userId转发至对应的服务器
在不重启nginx的情况下 可以动态指定服务地址给对应userId 做到动态添加灰度服务
conf/lua/redirect_by_user.lua
local _UTIL = {}
-- 获取请求当中的 userId
function _UTIL.get_user_by_req()
-- header
local headers = ngx.req.get_headers()
local header_user_id = headers["userId"]
if header_user_id ~=nil then
return header_user_id
end
local req_method = ngx.var.request_method
-- get
if req_method == "GET" then
local args = ngx.req.get_uri_args()
return args["userId"]
end
-- post
if req_method == "POST" then
-- 区分content-type
local receive_headers = ngx.req.get_headers()
local content_type = receive_headers["content-type"]
if string.find(content_type, "application/json", 1, true) then
ngx.req.read_body()
local body_data = ngx.req.get_body_data()
local cjson = require "cjson"
local ok, json_data = pcall(cjson.decode, body_data)
if ok then
local jsusid = json_data["userId"]
return jsusid
end
return nil
end
if string.find(content_type, "application/x-www-form-urlencoded", 1, true) then
ngx.req.read_body()
local post_args = ngx.req.get_post_args()
return post_args["userId"]
end
end
return nil;
end
local _M = {}
-- 连接redis 用于持久化灰度服务器地址(CANARY_MAP), 也可以不使用, 但nginx服务重启后 canary_map内容会失效
local redis_service = require "redis_service"
-- 用于缓存灰度的服务器地址,内容为{key: userId, value: 服务器ip/url}
local CANARY_MAP = nil
-- 初始化灰度服务的地址, 可删除, 删除后将 CANARY_MAP = nil 改为 CANARY_MAP = {} 否则报空异常
function init_canary_map()
if CANARY_MAP == nil then
CANARY_MAP = redis_service.get_canary_server()
end
end
-- 获取转跳服务器地址 无则返回nil
function _M.redirect_for_canary()
-- 初始化灰度服务地址 可删除
init_canary_map()
local version = ngx.var.cookie_version
-- 保存cookie 用做快速判断
if version == nil then
-- 获取参数
local user_id = _UTIL.get_user_by_req()
if user_id then
-- 保存标识 过期时间30分钟
ngx.header.set_cookie = "version=" .. user_id .. "; path=/; Expires=" .. ngx.cookie_time(ngx.time() + 60 * 30)
local canary_server = CANARY_MAP[tostring(user_id)]
if canary_server then
return canary_server
end
end
return nil
end
local canary_server = CANARY_MAP[tostring(version)]
if canary_server then
return canary_server
else
return nil
end
end
-- 添加灰度服务器
function _M.add_canary()
local args = ngx.req.get_uri_args()
local user_id = args["uid"]
local canary_val = args["val"]
ngx.say("\n" .. user_id .. "|" .. canary_val .. "<< \n")
if user_id and canary_val then
-- 初始化灰度服务地址 可删除
init_canary_map()
CANARY_MAP[tostring(user_id)] = canary_val
-- 持久化 可删除
redis_service.set_canary_server(CANARY_MAP)
for k,v in pairs(CANARY_MAP) do
ngx.say(k .. "|" .. v)
end
ngx.say("success")
return
end
ngx.say("err, uid or val is nil")
end
return _M
conf/lua/redis_service.lua 用于灰度服务器记录的持久化 可以不使用
local _M = {}
-- 序列化 摘抄网上
function serialize(obj)
local lua = ""
local t = type(obj)
if t == "number" then
lua = lua .. obj
elseif t == "boolean" then
lua = lua .. tostring(obj)
elseif t == "string" then
lua = lua .. string.format("%q", obj)
elseif t == "table" then
lua = lua .. "{"
for k, v in pairs(obj) do
lua = lua .. "[" .. serialize(k) .. "]=" .. serialize(v) .. ","
end
local metatable = getmetatable(obj)
if metatable ~= nil and type(metatable.__index) == "table" then
for k, v in pairs(metatable.__index) do
lua = lua .. "[" .. serialize(k) .. "]=" .. serialize(v) .. ","
end
end
lua = lua .. "}"
elseif t == "nil" then
return nil
else
ngx.log(ngx.ERR, "can not serialize a " .. t .. " type.")
end
return lua
end
-- 反序列化 摘抄网上
function unserialize(lua)
local t = type(lua)
if t == "nil" or lua == "" then
return nil
elseif t == "number" or t == "string" or t == "boolean" then
lua = tostring(lua)
else
ngx.log(ngx.ERR, "can not unserialize a " .. t .. " type.")
end
lua = "return " .. lua
local func = loadstring(lua)
if func == nil then
return nil
end
return func()
end
-- redis连接 需要close
function redis_content()
local redis_c = require "resty.redis"
local red = redis_c:new()
red:set_timeout(3000)
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
return nil
end
return red
end
function _M.get_canary_server()
local red = redis_content()
if red == nil then
return {}
end
local res, err = red:get("server_map")
red:close()
if res ~= nil and res ~= null and res ~= ngx.null then
return unserialize(res)
end
return {}
end
function _M.set_canary_server(val)
local red = redis_content()
if red == nil then
return
end
local res, err = red:set("server_map", serialize(val))
red:close()
end
return _M
conf/nginx.conf 配置
worker_processes 1;
error_log logs/error.log;
events {
worker_connections 1024;
}
http {
# 热加载
# lua_code_cache off;
lua_package_path '$prefix/conf/lua/?.lua;;';
# 默认服务器
upstream default_ups {
server 127.0.0.1:8080;
}
server {
listen 10001;
location / {
# include proxy-options.conf;
set $curl_val 'default_ups';
rewrite_by_lua '
local fun = require "redirect_by_user"
local cur_val = fun.redirect_for_canary()
if cur_val then
ngx.var.curl_val = cur_val
end
';
proxy_pass http://$curl_val;
}
}
server {
listen 10002;
# 例
# curl -s http://127.0.0.1:10002/add?uid=1\&val=127.0.0.1:8081
# 添加灰度服务器
location /add {
include proxy-options.conf;
content_by_lua '
local fun = require "redirect_by_user"
fun.add_canary()
';
}
}
}
安装openresty后(网上可找到教程), 建一个目录存放以上文件, 启动命令
openresty -p `pwd`/ -c conf/nginx.conf
写过程中还遇到resty.redis的一些坑
原本在ngxin.conf中配置init_by_lua 使canary_map初始化, 但init_by_lua中无法使用resty.redis包 会报异常(详见: https://github.com/openresty/lua-nginx-module/issues/206)
使用resty.redis 获取redis中的值时(res:get()方法), 假如返回为空, 不是lua中的nil, 而是null, 所以用↓判断(可参考: https://github.com/openresty/lua-resty-redis/issues/90)
if res ~= nil and res ~= null and res ~= ngx.null then
return nil
end
