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