公司业务需求 需要在新功能上线前给一部分用户作测试
网上查到 可以使用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