nginx用lua脚本实现动态重载压测结果

压测结果(QPS):

频率无reload5s3s1s500ms200ms100ms
reload35705.9634784.1533432.3931955.7728205.4320778.9016340.17
lua重载29335.2329185.1929017.0629579.6729258.4129230.1329938.52
对比-17.84%-16.09%-13.21%-7.44%3.73%40.68%83.22%

结论:

  • reload方式随着频率的增加,QPS 明显下降。尤其在 500ms、200ms 和 100ms 重载频率下,性能影响逐渐显著,QPS 分别下降到 28205.4320778.9016340.17。最大降幅达到54%,如果频率继续提高,降幅会更显著。
  • reload方式在 reload 期间,由于其会保留旧 worker 进程同时开启新的 worker 进程,导致 CPU 使用率显著下降,内存占用率显著上升(幅度均超过50%)。
  • lua动态重载在不同频率下的表现稳定,QPS 维持在 29000-30000 左右,没有明显波动。
  • 在频率不高(≥1秒)时,reload方式性能略优;但随着频率增加,动态重载表现出明显性能优势。

环境:

  • 四台4C8G机器
  • 客户端:1台(用于发起压测请求)
  • NGINX:1台(使用 OpenResty,内置 Lua 模块)
  • 后端服务:2台
  • 压测工具:wrk;参数:4线程,4000连接

大致思路

在 worker 进程中,通过定时器定期检查并更新配置,将最新配置写入 NGINX 的共享内存,并利用 Lua 脚本实现轮询转发。

如果通过 HTTP 请求来实现配置更新,在高并发情况下可能会出现配置失效的情况。

性能调优

Lua 脚本还有代码优化空间,例如减少共享内存的set和flushAll操作、引入LuaJIT、减少日志等等,可以进一步降低 Lua 脚本的执行开销,提高整体 QPS 表现。

配置文件:

user  nginx;
worker_processes  4;

# 设置内存和文件描述符限制
worker_rlimit_core  1000M;       # 增大核心文件大小限制
worker_rlimit_nofile 65535;     # 增大文件描述符限制

pid        /var/run/nginx.pid;
error_log  error.log debug;

events {
    worker_connections  65535;
    multi_accept on;  # 每次尽可能接受多个新连接
}

http {
    lua_shared_dict endpoints_data 5m; #定义upstream共享内存空间
    lua_shared_dict cache 1m; #定义计数共享空间
    access_log  access.log;

    lua_package_path "/your/path/to/lua/?.lua;;";
    init_by_lua_block {
        collectgarbage("collect")
        local ok, res

        -- ngx.log(ngx.ERR, "Loading configuration")

        ok, res = pcall(require, "configuration")

        if not ok then

            ngx.log(ngx.ERR, "Failed to load configuration: " .. tostring(res))
        else
            -- ngx.log(ngx.ERR, "Loaded")
            configuration = res
        end

        local success, err = pcall(configuration.load_configuration_from_file)
        if not success then
            ngx.log(ngx.ERR, "Failed to load configuration from file: " .. tostring(err))
        else
            ngx.log(ngx.ERR, "Configuration loaded from file successfully in init phase.")
        end
    }

    init_worker_by_lua_block {
        local worker_id = ngx.worker.id()
        if worker_id == 0 then
            local timer_ok, timer_err = ngx.timer.at(0, configuration.check_for_updates)
            if not timer_ok then
                ngx.log(ngx.ERR, "Failed to create initial timer: " .. timer_err)
            else
                ngx.log(ngx.ERR, "Timer started successfully by worker with ID: " .. worker_id)
            end
        else
            ngx.log(ngx.ERR, "Worker with ID: " .. worker_id .. " is not starting the timer.")
        end
    }

    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;
    server {
        # 测试用
        location /hello {
            default_type 'text/plain';
            content_by_lua 'ngx.say("hello, lua")';
        }
        # 配置接口
        # 通过http请求进行配置(发送json格式的post请求)
        location /configuration {
            client_max_body_size                    5m;
            client_body_buffer_size                 1m;
            proxy_buffering                         off;

            content_by_lua_block {
              configuration.call()
            }
        }
        # 查询共享内存中的path
        location /lua {
            default_type 'text/plain';
            # 读取请求中的 path 参数 并从共享 dict 中查询这个值,
            # 返回查询到的结果
            content_by_lua '
                ngx.log(ngx.ERR, "Entering rewrite_by_lua block")
                local path = ngx.req.get_uri_args()["path"]
                if path == nil then
                    ngx.say("path not found")
                    return
                end
                local data = ngx.shared.endpoints_data:get("/"..path)
                if not data then
                    ngx.say("unknown path")
                    return
                end
                ngx.say("paths: "..data)
            ';
        }
        
        # other path
        location / {
            set $load_ups "";

            # 动态设置当前 upstream, 未找到返回404
            rewrite_by_lua '
                local ups = configuration.getEndpoints()

                ngx.log(ngx.ERR, "upstream: ", ups)

                if ups ~= nil then
                    ngx.log(ngx.ERR,"got upstream: ", ups)
                    ngx.var.load_ups = ups
                    ngx.log(ngx.ERR, "Full proxy URL: " .. ngx.var.load_ups .. ngx.var.request_uri)

                    return
                end

                ngx.log(ngx.ERR, "No upstream found for path: " .. ngx.var.request_uri) -- 记录未找到的情况

                ngx.status = ngx.HTTP_NOT_FOUND
                ngx.exit(ngx.status)
            ';
            proxy_pass http://$load_ups;
            # add_header  X-Upstream  $upstream_addr always; # 添加 backend ip
        }
    }
}

lua脚本:

-- 引入变量
local io = io
local ngx = ngx
local table = table
-- 当前包的对象,类似 go 语言的定义结构体 让给这个结构体实现方法
local _M = {}
-- 与 Nginx 共享的空间 可读写
local Endpoints = ngx.shared.endpoints_data
local cjson = require "cjson"

if cjson then
    ngx.log(ngx.ERR, "cjson is loaded successfully.")
else
    ngx.log(ngx.ERR, "Failed to load cjson.")
end

local function split(inputstr, sep)
    sep = sep or "%s"
    local t = {}
    for str in string.gmatch(inputstr, "([^" .. sep .. "]+)") do
        table.insert(t, str)
    end
    return t
end

-- 读取请求 body 部分
local function fetch_request_body()
    ngx.req.read_body()
    local body = ngx.req.get_body_data()
  
    if not body then
      -- request body might've been written to tmp file if body > client_body_buffer_size
        local file_name = ngx.req.get_body_file()
        local file = io.open(file_name, "rb")
    
        if not file then
            return nil
        end
    
        body = file:read("*all")
        file:close()
    end
  
    return body
end

-- handle_backends .
local function handle_backends()
    if ngx.var.request_method == "GET" then
        ngx.status = ngx.HTTP_OK
        -- 返回查询的服务列表
        local path = ngx.req.get_uri_args()["path"]
        ngx.print(Endpoints:get(path))
        return
    end

    -- 读取请求 body
    local obj = fetch_request_body()
    if not obj then
        ngx.log(ngx.ERR, "dynamic-configuration: unable to read valid request body")
        ngx.status = ngx.HTTP_BAD_REQUEST
        return
    end

    -- 通过 第三方包 json 解析 body到 lua table
    ngx.log(ngx.ERR, "Received object: ", obj)
    local rule, err = cjson.decode(obj)
    if not rule then
        ngx.log(ngx.ERR, "could not parse backends data: ", err)
        return
    end

    ngx.log(ngx.ERR, "decoded rule", obj)

    -- 清空共享空间
    Endpoints:flush_all()
    -- 遍历并写入
    for _, new_rule in ipairs(rule.rules) do
        -- 更新
        -- 将数组合并
        local succ, err1, forcible = Endpoints:set(new_rule.path, table.concat(new_rule.upstreams, ","))
        ngx.log(ngx.ERR, "set result", succ, err1,forcible)
    end

    ngx.status = ngx.HTTP_CREATED
    ngx.say("ok")
end


-- call called by ngx

-- 从配置文件加载配置
function _M.load_configuration_from_file()
    local file = io.open("your/path/to/config.json", "r")  -- 指定配置文件路径
    if not file then
        ngx.log(ngx.ERR, "Failed to open configuration file.")
        return
    end

    local content = file:read("*all")
    file:close()

    local config, err = cjson.decode(content)
    if not config then
        ngx.log(ngx.ERR, "Failed to parse configuration: " .. err)
        return
    end

    ngx.log(ngx.ERR, "Parsed configuration: " .. cjson.encode(config))

    -- 清空共享空间
    Endpoints:flush_all()

    for _, rule in ipairs(config.rules) do
        ngx.log(ngx.ERR, "Loading rule - Path: " .. rule.path .. ", Upstreams: " .. table.concat(rule.upstreams, ","))
        -- 更新共享字典
        local succ, err1, forcible = Endpoints:set(rule.path, table.concat(rule.upstreams, ","))
        ngx.log(ngx.ERR, "Set result for path: " .. rule.path .. " - success: " .. tostring(succ) .. ", err: " .. tostring(err1))
    end

    ngx.log(ngx.ERR, "Configuration loaded from file successfully.")
end

-- 定时器回调函数
function _M.check_for_updates(premature)
    if premature then
        return
    end

    ngx.log(ngx.ERR, "check_for_updates is running")  -- 检查定时器任务是否运行

    _M.load_configuration_from_file() -- 调用函数加载配置

    -- 设置下一个定时器,设定间隔时间
    local ok, err = ngx.timer.at(0.1, _M.check_for_updates) -- 根据需要调整时间
    if not ok then
        ngx.log(ngx.ERR, "Failed to create timer: " .. err)
    end
end

function _M.call()
    -- 只处理 GET 和 POST
    if ngx.var.request_method ~= "POST" and ngx.var.request_method ~= "GET" then
        ngx.status = ngx.HTTP_BAD_REQUEST
        ngx.print("Only POST and GET requests are allowed!")
        return
      end
    -- 目前只处理后端服务的配置 所以判断路由
    if ngx.var.request_uri:find("/configuration/backends") then
        -- 调用内部方法
        handle_backends()
        return
    end
    -- 非法请求 返回 404
    ngx.status = ngx.HTTP_NOT_FOUND
    ngx.log(ngx.ERR, "Requested URI: ", ngx.var.request_uri)
    ngx.print("Not found!")
end



-- 轮询获取节点
function _M.getEndpoints() 
    local cache = ngx.shared.cache
    local path = ngx.var.request_uri
    local eps =  Endpoints:get(path)
    if not eps then
        return nil
    end

    local tab = split(eps,",")
    local index = cache:get(path)
    -- if index ==  nil or index > #tab then
    if index ==  nil or index > 2 then
        index = 1
    end
    -- 加一
    cache:set(path,index+1)
    return tab[index]
end

return _M 

转发路径文件

{
  "rules": [
    {
      "path": "/yourpath",
      "upstreams": ["backend1:8080", "backend2:8080"]
    }
  ]
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值