定时任务场景
在 OpenResty 中,有时候需要在后台定期地执行某些任务,比如同步数据、清理日志等。OpenResty 提供了 ngx.timer 来解决这类需求,可以把ngx.timer看作是 OpenResty 模拟的客户端请求,用以触发对应的回调函数。
OpenResty 的定时任务可以在任意处理阶段发起任意多个定时器,执行任意的功能。分为下面两种:
- ngx.timer.at,用来执行一次性的定时任务;
- ngx.time.every,用来执行固定周期的定时任务。
使用ngx.timer可以突破 init_worker_by_lua 中不能使用 cosocket 的限制。
一次性定时任务
下面这段代码,启动了一个延时为0的定时任务。它启动了回调函数 handler,并在这个函数中,用 cosocket 去访问一个网站:
init_worker_by_lua_block {
local function handler()
local sock = ngx.socket.tcp()
local ok, err = sock:connect(“www.baidu.com", 80)
end
local ok, err = ngx.timer.at(0, handler)
}
这样,就绕过了 cosocket 在这个阶段不能使用的限制。
ngx.timer.at 并没有解决周期性运行这个需求,在上面的示例中,它是一个一次性的任务。
周期性定时任务
基于 ngx.timer.at 这个API,如果要做到周期性运行:
1、可以在回调函数中,使用一个while true 的死循环,执行完任务后 sleep 一段时间,自己来实现周期任务;
2、你还可以在回调函数的最后,再创建另外一个新的 timer。
timer 的本质是一个请求,虽然这个请求不是终端发起 的;而对于请求来讲,在完成自己的任务后它就要退出,不能一直常驻,否则很容易造成各种资源的泄漏。
第一种使用 while true 来自行实现周期任务的方案并不靠谱。第二种方案虽然是可行的,但递归地创建 timer ,并不容易理解。
OpenResty 后面新增的 ngx.time.every API,就是专门为了解决这个问题而出现的,它是更加接近 crontab 的解决方案。
在启动了一个 timer 之后,就再也没有机会来取消这个定时任务了,毕竟 ngx.timer.cancel 还是一个 todo 的功能。
就会面临一个问题:定时任务是在后台运行的,并且无法取消;如果定时任务的数量很多,就很容易耗尽系统资源。
所以,OpenResty 提供了 lua_max_pending_timers 和 lua_max_running_timers 这两个指令,来对其进行限制。前者代表等待执行的定时任务的最大值,后者代表当前正在运行的定时任务的最大值。
可以通过 Lua API,来获取当前等待执行和正在执行的定时任务的值:
content_by_lua_block {
ngx.timer.at(3, function() end)
ngx.say(ngx.timer.pending_count())
}
这段代码会打印出 1,表示有 1 个计划任务正在等待被执行。
content_by_lua_block {
ngx.timer.at(0.1, function() ngx.sleep(0.3) end)
ngx.sleep(0.2)
ngx.say(ngx.timer.running_count())
}
这段代码会打印出 1,表示有 1 个计划任务正在运行中。
ngx.timer.at(0, func)
指定0延迟,在这种情况下,当前处理程序执行时,计时器将立即过期。当计时器过期时,计时器回调中的Lua代码在一个“轻线程”中运行,该线程与创建计时器的原始请求完全分离。
因此,与创建它们的请求具有相同生存期的对象(如cosocket)不能在原始请求和计时器回调函数之间共享。
Nginx的许多Lua API都是在计时器回调的上下文中启用的,比如流/数据报cosockets(ngx.socket.tcp和ngx.socket.udp)、共享内存字典(ngx.shared.DICT)、用户协程(协程)、用户“轻线程”(ngx.thread)、ngx.exit、ngx.now/ngx.time、ngx.md5/ngx。sha1_bin都是允许的。但是子请求API(如ngx.location.capture)、ngx.req.*API、下游输出API(如ngx.say、ngx.print和ngx.flush)在此上下文中被显式禁用。
可以将大多数标准Lua值(nils、布尔值、数字、字符串、表、闭包、文件句柄等)显式地作为用户参数或隐式地作为回调闭包的up值传递给计时器回调。然而,有几个例外:不能传递coroutine.create和ngx.thread.spawn返回的任何线程对象,或ngx.socket.tcp、ngx.socket.udp和ngx.req.socket返回的任何cosocket对象。因为这些对象的生存期绑定到创建它们的请求上下文,而计时器回调与创建请求的上下文分离(通过设计),并在其自己的(假)请求上下文中运行。如果试图在创建请求的边界上共享线程或cosocket对象,则会出现“未找到co-ctx”错误(对于线程)或“错误请求”(对于cosocket)。但是,可以在计时器回调中创建所有这些对象。