做者:张超nginx
以前工做时候,一台引流测试机器的一个 ngx_lua 服务忽然出现了一些 HTTP/500 响应,从错误日志打印的堆栈来看,是不久前新发布的版本里添加的一个 Lua table 不存在,而有代码向其进行索引致使的。这使人百思不得其解,若是是版本回退致使的,那么为何使用这个 Lua table 的代码没有被回退,恰恰定义这个 table 的代码被回退了呢?服务器
通过排查发现,当时 nginx 刚刚完成热更新操做,旧的 master 进程还存在,由于要准备机器重启,先切掉了引流流量(但有些请求还在),同时系统触发了 nginx -s stop,这才致使了这个问题。网络
下面我将使用一个原生的 nginx,在个人安装了 fedora26 的虚拟机上复现这个过程,我使用的 nginx 版本是目前最新的 1.13.4运维
首先启动 nginx函数
能够看到 master 和 worker 都已经在运行。性能
接着咱们向 master 发送一个 SIGUSR2 信号,当 nginx 核心收到这个信号后,就会触发热更新。测试
能够看到新的 master 和该 master fork 出来的 worker 已经在运行了,此时咱们接着向旧 master 发送一个 SIGWINCH 信号,旧 master 收到这个信号后,会向它的 worker 发送 SIGQUIT,因而旧 master 的 worker 进程就会退出:ui
此时只剩下旧的 master,新的 master 和新 master 的 worker 在运行,这和当时线上运行的状况相似。lua
接着咱们使用 stop 命令:spa
咱们会发现,新的 master 和它的 worker 都已经退出,而旧的 master 还在运行,并产生了 worker 出来。这就是当时线上的状况了。
事实上,这个现象和 nginx 自身的设计有关:当旧的 master 准备产生 fork 新的 master 以前,它会把 nginx.pid 这个文件重命名为 nginx.pid.oldbin,而后再由 fork 出来的新的 master 去建立新的 nginx.pid,这个文件将会记录新 master 的 pid。nginx 认为热更新完成以后,旧 master 的使命几乎已经结束,以后它随时会退出,所以以后的操做都应该由新 master 接管。固然,在旧 master 没有退出的状况下经过向新 master 发送 SIGUSR2 企图再次热更新是无效的,新 master 只会忽略掉这个信号而后继续它本身的工做。
更不巧的是,咱们上面提到的这个 Lua table,定义它的 Lua 文件早在运行 init_by_lua 这个 hook 的时候,就已经被 LuaJIT 加载到内存并编译成字节码了,那么显然旧的 master 必然没有这个 Lua table,由于它加载那部分 Lua 代码是旧版本的。
而索引该 table 的 Lua 代码并无在 init_by_lua 的时候使用到,这些代码都是在 worker 进程里被加载起来的,这时候项目目录里的代码都是最新的,因此 worker 进程加载的都是最新的代码,若是这些 worker 进程处理到相关的请求,就会出现 Lua 运行时错误,外部表现则是对应的 HTTP 500。
吸取了这个教训以后,咱们须要更加合理地关闭咱们的 nginx 服务。 因此一个更加合理的 nginx 服务启动关闭脚本是必需的,网上流传的一些脚本并无对这个现象作处理,咱们更应该参考 NGINX 官方提供的脚本。
这段代码引自 NGINX 官方的 /etc/init.d/nginx 。
接下来咱们来全面梳理下 nginx 信号集,这里不会涉及到源码细节,感兴趣的同窗能够自行阅读相关源码。
咱们有两种方式来向 master 进程发送信号,一种是经过 nginx -s signal 来操做,另外一种是经过 kill 命令手动发送。
第一种方式的原理是,产生一个新进程,该进程经过 nginx.pid 文件获得 master 进程的 pid,而后把对应的信号发送到 master,以后退出,这种进程被称为 signaller。
第二种方式要求咱们了解 nginx -s signal 到真实信号的映射。下表是它们的映射关系:
operation signal
reload SIGHUP
reopen SIGUSR1
stop SIGTERM
quit SIGQUIT
hot update SIGUSR2 & SIGWINCH & SIGQUIT
stop vs quit
stop 发送 SIGTERM 信号,表示要求强制退出,quit 发送 SIGQUIT,表示优雅地退出。 具体区别在于,worker 进程在收到 SIGQUIT 消息(注意不是直接发送信号,因此这里用消息替代)后,会关闭监听的套接字,关闭当前空闲的链接(能够被抢占的链接),而后提早处理全部的定时器事件,最后退出。没有特殊状况,都应该使用 quit 而不是 stop。
reload
master 进程收到 SIGHUP 后,会从新进行配置文件解析、共享内存申请,等一系列其余的工做,而后产生一批新的 worker 进程,最后向旧的 worker 进程发送 SIGQUIT 对应的消息,最终无缝实现了重启操做。
reopen
master 进程收到 SIGUSR1 后,会从新打开全部已经打开的文件(好比日志),而后向每一个 worker 进程发送 SIGUSR1 信息,worker 进程收到信号后,会执行一样的操做。reopen 可用于日志切割,好比 NGINX 官方就提供了一个方案:
这里 sleep 1 是必须的,由于在 master 进程向 worker 进程发送 SIGUSR1 消息到 worker 进程真正从新打开 access.log 之间,有一段时间窗口,此时 worker 进程仍是向文件 access.log.0 里写入日志的。经过 sleep 1s,保证了 access.log.0 日志信息的完整性(若是没有 sleep 而直接进行压缩,颇有可能出现日志丢失的状况)。
hot update
某些时候咱们须要进行二进制热更新,nginx 在设计的时候就包含了这种功能,不过没法经过 nginx 提供的命令行完成,咱们须要手动发送信号。
经过上面的问题复现,你们应该已经了解到如何进行热更新了,咱们首先须要给当前的 master 进程发送 SIGUSR2,以后 master 会重命名 nginx.pid 到 nginx.pid.oldbin,而后 fork 一个新的进程,新进程会经过 execve 这个系统调用,使用新的 nginx ELF 文件替换当前的进程映像,成为新的 master 进程。新 master 进程起来以后,就会进行配置文件解析等操做,而后 fork 出新的 worker 进程开始工做。
接着咱们向旧的 master 发送 SIGWINCH 信号,而后旧的 master 进程则会向它的 worker 进程发送 SIGQUIT 信息,从而使得 worker 进程退出。向 master 进程发送 SIGWINCH 和 SIGQUIT 都会使得 worker 进程退出,可是前者不会使得 master 进程也退出。
最后,若是咱们以为旧的 master 进程使命完成,就能够向它发送 SIGQUIT 信号,让其退出了。
worker 进程如何处理来自 master 的信号消息
实际上,master 进程再向 worker 进程通信,不是使用 kill 函数,而是使用了经过管道实现的 nginx channel,master 进程向管道一端写入信息(好比信号信息),worker 进程则从另一端收取信息,nginx channel 事件,在 worker 进程刚刚起来的时候,就被加入事件调度器中(好比 epoll,kqueue),因此当有数据从 master 发来时,便可被事件调度器通知到。
nginx 这么设计是有理由的,做为一个优秀的反向代理服务器,nginx 追求的就是极致的高性能,而 signal handler 会中断 worker 进程的运行,使得全部的事件都被暂停一个时间窗口,这对性能是有必定损失的。
不少人可能会认为当 master 进程向 worker 进程发送信息以后,worker 进程马上会有对应操做回应,然而 worker 进程是很是繁忙的,它不断地处理着网络事件和定时器事件,当调用 nginx channel 事件的 handler 以后,nginx 仅仅只是处理了一些标志位。真正执行这些动做是在一轮事件调度完成以后。因此这之间存在一个时间窗口,尤为是业务复杂且流量巨大的时候,这个窗口就有可能被放大,这也就是为何 NGINX 官方提供的日志切割方案里要求 sleep 1s 的缘由。
固然,咱们也能够绕过 master 进程,直接向 worker 进程发送信号,worker 能够处理的信号有
signal effect
SIGINT 强制退出
SIGTERM 强制退出
SIGQUIT 优雅退出
SIGUSR1 从新打开文件
nginx 信号操做在平常运维中是最多见的,也是很是重要的,这个环节若是出现失误则可能形成业务异常,带来损失。因此理清楚 nginx 信号集是很是必要的,能帮助咱们更好地处理这些工做。
另外,经过此次的经验教训和对 nginx 信号集的认知,咱们认为如下几点是比较重要的:
慎用 nginx -s stop,尽量使用 nginx -s quit热更新以后,若是肯定业务没问题,尽量让旧的 master 进程退出关键性的信号操做完成后,等待一段时间,避免时间窗口的影响不要直接向 worker 进程发送信号