公司業務需求 需要在新功能上線前給一部分用戶作測試
網上查到 可以使用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