极客时间已完结课程限时免费阅读

20 | 超越 Web 服务器:特权进程和定时任务

20 | 超越 Web 服务器:特权进程和定时任务-极客时间

20 | 超越 Web 服务器:特权进程和定时任务

讲述:温铭

时长09:14大小8.45M

你好,我是温铭。
前面我们介绍了 OpenResty API、共享字典缓存和 cosocket。它们实现的功能,都还在 Nginx 和 Web 服务器的范畴之内,算是提供了开发成本更低、更容易维护的一种实现,提供了可编程的 Web 服务器。
不过,OpenResty 并不满足于此。我们今天就挑选几个,OpenResty 中超越 Web 服务器的功能来介绍一下。它们分别是定时任务、特权进程和非阻塞的 ngx.pipe。

定时任务

在 OpenResty 中,我们有时候需要在后台定期地执行某些任务,比如同步数据、清理日志等。如果让你来设计,你会怎么做呢?最容易想到的方法,便是对外提供一个 API 接口,在接口中完成这些任务;然后用系统的 crontab 定时调用 curl,来访问这个接口,进而曲线地实现这个需求。
不过,这样一来不仅会有割裂感,也会给运维带来更高的复杂度。所以, OpenResty 提供了 ngx.timer 来解决这类需求。你可以把ngx.timer ,看作是 OpenResty 模拟的客户端请求,用以触发对应的回调函数。
其实,OpenResty 的定时任务可以分为下面两种:
ngx.timer.at,用来执行一次性的定时任务;
ngx.time.every,用来执行固定周期的定时任务。
还记得上节课最后我留下的思考题吗?问题是如何突破 init_worker_by_lua 中不能使用 cosocket 的限制,这个答案其实就是 ngx.timer
下面这段代码,就是启动了一个延时为 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_timerslua_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 运行下面这个函数:
$ 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 上下文中才有意义,因为没有请求触发,也就不会走到contentaccess 等上下文去。
下面,我们来看一个开启特权进程的示例:
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
不过,如果特权只在 init_worker_by_lua 阶段运行一次,显然不是一个好主意,那我们应该怎么来触发特权进程呢?
没错,答案就藏在刚刚讲过的知识里。既然它不监听端口,也就是不能被终端请求触发,那就只有使用我们刚才介绍的 ngx.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 信号量的功能。自然,你也可以在此基础上实现更多有趣的功能,比如轮询数据库,看是否有特权进程的任务并执行。因为特权进程是 root 权限,这显然就有点儿“后门”程序的意味了。

非阻塞的 ngx.pipe

最后我们来看非阻塞的 ngx.pipe。刚刚讲过的这个代码示例中,我们使用了 Lua 的标准库,来执行外部命令行,把信号发送给了 master 进程:
os.execute("kill -HUP " .. pid)
这种操作自然是会阻塞的。那么,在 OpenResty 中,是否有非阻塞的方法来调用外部程序呢?毕竟,要知道,如果你是把 OpenResty 当做一个完整的开发平台,而非 Web 服务器来使用的话,这就是你的刚需了。
为此,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 的另外一种写法了,它调用系统的 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)'
这其实也就是 lua-resty-shell 底层的实现代码了。你可以去查看 ngx.pipe 的文档和测试案例,来获取更多的使用方法,这里我就不再赘述了。

写在最后

到此,今天的主要内容我就讲完了。从上面的几个功能,我们可以看出,OpenResty 在做一个更好用的 Nginx 的前提下,也在尝试往通用平台的方向上靠拢,希望开发者能够尽量统一技术栈,都用 OpenResty 来解决开发需求。这对于运维来说是相当友好的,因为只要部署一个 OpenResty 就可以了,维护成本更低。
最后,给你留一个思考题。由于可能会存在多个 Nginx worker,那么 timer 就会在每个 worker 中都运行一次,这在大多数场景下都是不能接受的。我们应该如何保证 timer 只能运行一次呢?
欢迎留言说说你的解决方法,也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流,一起进步。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 3

提建议

上一篇
19 | OpenResty 的核心和精髓:cosocket
下一篇
21 | 带你玩转时间、正则表达式等常用API
unpreview
 写留言

精选留言(15)

  • wusiration
    2019-07-13
    通过shared dict判断互斥,存在正在执行中的timer还没更新执行状态,另一个worker继续去执行; 通过判断worker的id指定某个worker执行的方式应该可以实现这一需求
    4
  • Seven
    2019-07-14
    我想在init_worker_by_lua阶段通过timer启动一个websocket客户端一直循环收发数据会不会有问题呢?

    作者回复: while True 的循环吗?我的建议是跑一段时间,比如 5 分钟,就退掉,然后启动一个新的客户端。

    2
  • manatee
    2019-07-10
    想请问下特权进程是怎么回事,启动or本身就是普通用户。如何获取root权限呢,另外特权进程的使用场景有哪些可以介绍下吗

    作者回复: 特权进程和master 进程的权限一样,如果 master 是普通用户,那特权进程也不可能拿到 root 权限。 一般用特权进程来清理日志、重启 OpenResty 自身等需要高权限的任务。

    2
  • manatee
    2019-07-10
    可以通过查看worker id,在指定worker下执行

    作者回复: 是的,没错

    共 2 条评论
    2
  • 过千帆
    2020-07-30
    timer,运行环境,怎么启动的,为什么会每个worker中运行?
  • 旺旺
    2019-09-23
    老师,在init_worker_by_lua_block里 local ok, err = ngx.timer.every(30, reset_server_list) ok, err = ngx.timer.at(0, reget_ssl_certificate) ok, err = ngx.timer.every(3600, reget_ssl_certificate) 写了三个定时器,怎么执行的效果不是想象中那样呢? 本来是想着一开始的时候就执行一次reget_ssl_certificate,然后每隔1个小时再执行一次reget_ssl_certificate的。 现在就算过了30秒reset_server_list也不执行。 如果只写一个“local ok, err = ngx.timer.every(30, reset_server_list)”是可以的。 意思是ngx.timer.every和ngx.timer.at不能同时混用吗?
    展开

    作者回复: 是可以混用的,它们之间不会互相影响的。如果你确定这里有bug,可以整理一个最小的复现代码,给 OpenResty 提交 issue

    共 2 条评论
  • 旺旺
    2019-09-18
    发现在init_by_lua_block里面开了特权进程后,如果init_worker_by_lua_block又有io.open创建文件操作,那么后面在worker进程里面第一次创建文件时的用户也是root了,然后后面worker里面写文件的时候,会报: failed to open file in append mode. error message: /tmp/wscmd_response.log: Permission denied, 因为/tmp/wscmd_response.log一开始是用root身份创建的,后面再用nobody去写的时候,就会报错。
    展开
  • 搞怪者😘 😒 �...
    2019-09-17
    这个定时器跟用curl是一样的嘛,怎么实现毫秒级定时,如果在压力很大的环境下,这样的定时器不就会消耗端口资源吗
    共 1 条评论
  • 旺旺
    2019-08-29
    local f, err = io.open(ngx.config.prefix() .. "/logs/nginx.pid", "r") 老师,这个代码也是Lua 的标准库,是不是也是阻塞的,又改采用什么方式优化呢?

    作者回复: 磁盘 IO 没有什么优化的方法,这里有一个使用 nginx threads pool 来模拟实现 "非阻塞"的方案:https://github.com/tokers/lua-io-nginx-module,你可以参考下。

    共 2 条评论
  • 英雄
    2019-07-23
    如果不能while true ,那websocket如何等待请求呢?

    作者回复: 可以 while true,但是有一个阈值,比如循环 1000 次之后退出循环,重新来一次。

  • HelloBug
    2019-07-14
    老师好,在讲ngx.timer的时候,说如果在回调函数里使用while true+sleep的方式循环执行任务,因为timer本质是一个请求,上面所说的实现会导致这个请求常驻,这些都是可以理解的,后面说会导致资源的泄露,这个怎么理解呢?

    作者回复: 泄漏这个词可能不太恰当,就是会导致很多 Lua 或者 C 对象无法得到释放,长期运行会有很多碎片。

  • helloworld
    2019-07-14
    local function reload(premature),老师,这个函数的参数premature是什么意思,在这段代码中有什么用呢

    作者回复: ngx.timer 的文档是有提到它的作用的: the premature argument takes a boolean value indicating whether it is a premature timer expiration or not. Premature timer expiration happens when the Nginx worker process is trying to shut down, as in an Nginx configuration reload triggered by the HUP signal or in an Nginx server shutdown. When the Nginx worker is trying to shut down, one can no longer call ngx.timer.at to create new timers with nonzero delays and in that case ngx.timer.at will return a "conditional false" value and a string describing the error, that is, "process exiting". 简单的说,就是在 Nginx 重启后者关闭的时候,就不要再去创建 timer 了。

  • Rye
    2019-07-11
    ngx.worker.id() == 0 应该是第一个worker

    作者回复: 是的,没错

  • chundonglinlin
    2019-07-10
    worker slot
  • 星亦辰
    2019-07-10
    share dict存取执行状态,就可以完成互斥了