OpenResty定时任务

我以前在项目中拆分过Redis的一个事件库来用,其中一个timer就非常好用。因为nginx采用的是相似的架构,所以同样也能提供这样的操作。

定时任务本质上是注册一个回掉函数在任务队列中,每次epoll\select\kqueue之前都会从队列里找到最近一次需要执行的timer,然后将时间差作为超时传入模型中,时间到了之后,检查任务队列就能发现需要执行的任务。

使用timer能保证nginx良好的并发特性,但要注意的事在timer中因为已经没有了请求的上下文,所以不能调用一些nginx内置函数,而且因为worker也是单进程的,所以再timer中也不要使用阻塞的函数。

例子

我们接受一个请求,然后定义一个timer,在3秒之后将redis中的sum加1。

local red = require("iredis")

function handler()
    local r = red:new()
    local ok, err = r:incr("sum")
    if not ok then
        ngx.say("failed to incr")
    end
end

local ok, err = ngx.timer.at(3, handler)
if not ok then
    ngx.say("failed to set timer.")
end

ngx.say("ok")

我们定义另一个接口,用于读取redis中的sum。

local red = require("iredis")

local sum, err = red:get("sum")
if not sum then
    ngx.say("failed to get sum")
end

ngx.say("sum ", sum)

在nginx的config中定义两个location:

worker_processes  1;
error_log logs/error.log info;

events {
    worker_connections 512;
}

http {

    log_format myformat '$remote_addr $status $time_local';
    access_log logs/access.log myformat;

    lua_package_path "/home/zhoulihai/Desktop/work/lua/?.lua;;";

    server {
        listen 8080;
        charset utf-8;

        location /redis {
            content_by_lua_file lua/redis_exp.lua;
        }

        location /mixed {
            content_by_lua_file lua/lru_t.lua;
        }
    }
}

至于iredis的代码可以在我以前的日志里找找。

测试

单独运行代码好像没有问题,sum都能根据请求的次数加1。但当我们使用wrk进行并发测试的时候,再使用接口读取sum,就会发现sum的数值远远低于我们请求的次数。

一开始分析原因可能事并发造成的数据错误,但转念一想,无论redis还是nginx都是单进程的,不可能出现这个问题。随后我将wrk的参数修改为wrk -c 1 -d 30 -t 1 http://127.0.0.1:8080/redis,发现sum的数量还是跟请求的数量不匹配。

最后,想到了,这么大的并发量,3秒的延迟大概队列里能寸100k个timer。可能队列没有这么长,造成队列溢出,有些timer压根就没执行。

修改

修改起来也简单:

local red = require("iredis")

function handler()
    local r = red:new()
    local ok, err = r:incr("sum")
    if not ok then
        ngx.say("failed to incr")
    end
end

local ok, err = ngx.timer.at(0, handler) -- 改为0秒
if not ok then
    ngx.say("failed to set timer.")
end

ngx.say("ok")

这样队列里的timer就能及时处理了。

这里需要注意,timer最好不要每个任务都启动,只有在特殊情况,可能需要通知其它系统时才触发。