OpenResty:特权进程和定时任务

定时任务

在 OpenResty 中,有时候需要在后台定期地执行某些任务,比如同步数据、清理日志等。最容易想到的方法,便是对外提供一个 API 接口,在接口中完成这些任务;

然后用系统的 crontab 定时调用 curl,来访问这个接口,进而曲线地实现这个需求。

不过,这样会给运维带来更高的复杂度。所以, 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,如果要做到周期性运行:

  • 可以在回调函数中,使用一个 while true 的死循环,执行完任务后 sleep 一段时间,自己来实现周期任务;
  • 你还可以在回调函数的最后,再创建另外一个新的 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 个计划任务正在运行中。

 

特权进程

Nginx 主要分为 master 进程和 worker 进程,其中,真正处理用户请求的是 worker 进程。可以通过 lua-resty-core 中提供的 process.type API ,获取到进程的类型。

运行下面这个函数:

resty -e 'local process = require "ngx.process"
ngx.say("process type:", process.type())'

返回的结果不是 worker, 而是single。这意味 resty 启动的 Nginx 只有 worker 进程, 没有 master 进程。在 resty 的实现中,下面这样的一行配置, 关闭了 master 进程:

master_process off;

而OpenResty 在 Nginx 的基础上进行了扩展,增加了特权进程:privileged agent。特权进程很特别:

它不监听任何端口,这就意味着不会对外提供任何服务;

它拥有和 master 进程一样的权限,一般来说是 root 用户的权限,这就让它可以做很多 worker 进程不可能完成的任务;

特权进程只能在 init_by_lua 上下文中开启;

特权进程只有运行在 init_worker_by_lua 上下文中才有意义,因为没有请求触发,也就不会走到content、access 等上下文去。

来看一个开启特权进程的示例:

init_by_lua_block {
    local process = require "ngx.process"
    local ok, err = process.enable_privileged_agent()
    if not ok then
        ngx.log(ngx.ERR, "enables privileged agent failed error:", err) 
    end
}

通过这段代码开启特权进程后,再去启动 OpenResty 服务,可以看到,Nginx 的进程中多了特权进程:

nginx: master process
nginx: worker process
nginx: privileged agent process

特权进程不监听端口,也就不能被终端请求触发,那就只有使用gx.timer ,来周期性地触发:

init_worker_by_lua_block {
    local process = require "ngx.process"
    local function reload(premature)
        local f, err = io.open(ngx.config.prefix() .. "/logs/nginx.pid", "r")
        if not f then
            return
        end
        local pid = f:read()
        f:close()
        os.execute("kill -HUP " .. pid)
    end
    if process.type() == "privileged agent" then
        local ok, err = ngx.timer.every(5, reload)
        if not ok then
            ngx.log(ngx.ERR, err)
        end
    end
}

上面这段代码,实现了每 5 秒给 master 进程发送 HUP信号量的功能

非阻塞的 ngx.pipe

上面示例中,使用了 Lua 的标准库,来执行外部命令行,把信号发送给了 master 进程,这种操作是会阻塞的。

os.execute("kill -HUP " .. pid)

lua-resty-shell 库来调用命令行就是非阻塞的:

$  resty -e 'local shell = require "resty.shell"
> local ok, stdout, stderr, reason, status =
> shell.run([[echo "hello, world"]])
> ngx.say(stdout)'

hello, world

这段代码可以算是 hello world 的另外一种写法了,它调用系统的 echo 命令来完成输出。类似的,可以 用 resty.shell ,来替代 Lua 中的 os.execute 调用。

lua-resty-shell 的底层实现,依赖了 lua-resty-core 中的 [ngx.pipe] API,所以,这个 使用 lua-resty-shell 打印出 hello wrold 的示例,改用 ngx.pipe ,可以写成下面这样:

$  resty -e 'local ngx_pipe = require "ngx.pipe"
> local proc = ngx_pipe.spawn({"echo", "hello world"})
> local data, err = proc:stdout_read_line()
> ngx.say(data)'

hello world

 

OpenResty 在做一个更好用的 Nginx 的前提下,也在尝试往通用平台的方向上靠拢,希望开发者能够尽量统一技术栈,都用 OpenResty 来解决开发需求。这对于运维来说是相当友好的,因为只要部署一个 OpenResty 就可以了,维护成本更低。 

posted @ 2020-04-23 23:15  liekkas01  阅读(3894)  评论(0编辑  收藏  举报