openresty 灰度發布 根據請求地址的參數轉發至指定服務


公司業務需求 需要在新功能上線前給一部分用戶作測試

網上查到 可以使用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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM