从实战出发,谈谈 nginx 信号集
前言
之前工作时候,一台引流测试机器的一个 ngx_lua 服务突然出现了一些 HTTP/500 响应,从错误日志打印的堆栈来看,是不久前新发布的版本里添加的一个 Lua table 不存在,而有代码向其进行索引导致的。这令人百思不得其解,如果是版本回退导致的,那么为什么使用这个 Lua table 的代码没有被回退,偏偏定义这个 table 的代码被回退了呢?
经过排查发现,当时 nginx 刚刚完成热更新操作,旧的 master 进程还存在,因为要准备机器重启,先切掉了引流流量(但有些请求还在),同时系统触发了 nginx -s stop
,这才导致了这个问题。
场景复现
下面我将使用一个原生的 nginx,在我的安装了 fedora26 的虚拟机上复现这个过程,我使用的 nginx 版本是目前最新的 1.13.4
首先启动 nginx
alex@Fedora26-64: ~/bin_install/nginx
./sbin/nginx
alex@Fedora26-64: ~/bin_install/nginx
ps auxf | grep nginx
alex 6174 0.0 0.0 28876 428 ? Ss 14:35 0:00 nginx: master process ./sbin/nginx
alex 6175 0.0 0.2 29364 2060 ? S 14:35 0:00 \_ nginx: worker process
可以看到 master 和 worker 都已经在运行。
接着我们向 master 发送一个 SIGUSR2
信号,当 nginx 核心收到这个信号后,就会触发热更新。
alex@Fedora26-64: ~/bin_install/nginx
kill -USR2 6174
alex@Fedora26-64: ~/bin_install/nginx
ps auxf | grep nginx
alex 6174 0.0 0.1 28876 1996 ? Ss 14:35 0:00 nginx: master process ./sbin/nginx
alex 6175 0.0 0.2 29364 2060 ? S 14:35 0:00 \_ nginx: worker process
alex 6209 0.0 0.2 28876 2804 ? S 14:37 0:00 \_ nginx: master process ./sbin/nginx
alex 6213 0.0 0.1 29364 2004 ? S 14:37 0:00 \_ nginx: worker process
可以看到新的 master 和该 master fork 出来的 worker 已经在运行了,此时我们接着向旧 master 发送一个 SIGWINCH
信号,旧 master 收到这个信号后,会向它的 worker 发送 SIGQUIT
,于是旧 master 的 worker 进程就会退出:
alex@Fedora26-64: ~/bin_install/nginx
kill -WINCH 6174
alex@Fedora26-64: ~/bin_install/nginx
ps auxf | grep nginx
alex 6174 0.0 0.1 28876 1996 ? Ss 14:35 0:00 nginx: master process ./sbin/nginx
alex 6209 0.0 0.2 28876 2804 ? S 14:37 0:00 \_ nginx: master process ./sbin/nginx
alex 6213 0.0 0.1 29364 2004 ? S 14:37 0:00 \_ nginx: worker process
此时只剩下旧的 master,新的 master 和新 master 的 worker 在运行,这和当时线上运行的情况类似。
接着我们使用 stop 命令:
alex@Fedora26-64: ~/bin_install/nginx
./sbin/nginx -s stop
alex@Fedora26-64: ~/bin_install/nginx
ps auxf | grep nginx
alex 6174 0.0 0.1 28876 1996 ? Ss 14:35 0:00 nginx: master process ./sbin/nginx
alex 6301 0.0 0.2 29364 2124 ? S 14:49 0:00 \_ nginx: worker process
我们会发现,新的 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 官方提供的脚本。
stop() {
echo -n $"Stopping $prog: "
killproc $prog -QUIT
retval=$?
echo
[ $retval -eq 0 ] && rm -f $lockfile
return $retval
}
这段代码引自 NGINX 官方的 /etc/init.d/nginx 。
nginx 信号集
接下来我们来全面梳理下 nginx 信号集,这里不会涉及到源码细节,感兴趣的同学可以自行阅读相关源码。
我们有两种方式来向 master 进程发送信号,一种是通过 nginx -s signal
来操作,另一种是通过 kill 命令手动发送。
第一种方式的原理是,产生一个新进程,该进程通过 nginx.pid
文件得到 master 进程的 pid,然后把对应的信号发送到 master,之后退出,这种进程被称为 signaller。
第二种方式要求我们了解 nginx -s signal
到真实信号的映射。下表是它们的映射关系:
operationsignalreloadSIGHUPreopenSIGUSR1stopSIGTERMquitSIGQUIThot updateSIGUSR2 & 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 官方就提供了一个方案:
$ mv access.log access.log.0
$ kill -USR1 `cat master.nginx.pid`
$ sleep 1
$ gzip access.log.0 # do something with access.log.0
这里 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 可以处理的信号有
signaleffectSIGINT强制退出SIGTERM强制退出SIGQUIT优雅退出SIGUSR1重新打开文件
总结
nginx 信号操作在日常运维中是最常见的,也是非常重要的,这个环节如果出现失误则可能造成业务异常,带来损失。所以理清楚 nginx 信号集是非常必要的,能帮助我们更好地处理这些工作。
另外,通过这次的经验教训和对 nginx 信号集的认知,我们认为以下几点是比较重要的:
- 慎用
nginx -s stop
,尽可能使用nginx -s quit
- 热更新之后,如果确定业务没问题,尽可能让旧的 master 进程退出
- 关键性的信号操作完成后,等待一段时间,避免时间窗口的影响
- 不要直接向 worker 进程发送信号
推荐阅读:
启用 Brotli 压缩算法,对比 Gzip 压缩 CDN 流量再减少 20%
HTTPS 传输优化详解之动态 TLS Record Size
从实战出发,谈谈 nginx 信号集的更多相关文章
- nginx+tomcat集群配置(1)---根目录设定和多后端分发配置
前言: 对于javaer而言, nginx+tomcat集群配置, 已然成了web应用部署的主流. 大公司如此, 小公司亦然. 对于个人开发者而言, 资源有限, 往往多个web应用混部于一台服务器(云 ...
- nginx.conf 集群完整配置
###############################nginx.conf 集群完整配置############################### #user nobody; # user ...
- nginx+tomcat集群配置(4)--rewrite规则和多应用根目录设定思路
前言: nginx中有一块很重要的概念, 就是rewrite规则. 它会对URL进行修改, 然后进行内部的重定向. rewrite授予了nginx更多的自由, 使得后级服务的接入更加地方便. 本文将简 ...
- nginx+ tomcat集群+动静资源分离
不知道为什么这个随便删不掉,写了也值显示一半一半不显示, 我把重新写了一遍: nginx + tomcat集群和动静资源分离
- signal函数、sigaction函数及信号集(sigemptyset,sigaddset)操作函数
信号是与一定的进程相联系的.也就是说,一个进程可以决定在进程中对哪些信号进行什 么样的处理.例如,一个进程可以忽略某些信号而只处理其他一些信号:另外,一个进程还可以选择如何处理信号.总之,这些总与特定 ...
- 《UNIX环境高级编程》笔记--信号集
1.信号集基本操作 我们需要有一个能表示多个信号--信号集(signal set)的数据类型.POSIX.1定义了数据类型sigset_t以包含一个信号 集,并且定义了一下五个处理信号处理信号集函数. ...
- 【linux信号】10.11信号集
POSIX定义数据类型sigset_t以包含一个信号集,并且定义了下面五个函数处理信号集:
- PCB信号集
每一个进程都有一个pcb进程控制块,用来控制进程的信息,同时信号在pcb中有两个队列去维护他,一个是未决信号集,每一位对应一个信号的状态,0,1,1表示未决态,另一个是信号屏蔽字(阻塞信号集),也就0 ...
- Linux+.NetCore+Nginx搭建集群
本篇和大家分享的是Linux+NetCore+Nginx搭建负载集群,对于netcore2.0发布后,我一直在看官网的文档并学习,关注有哪些新增的东西,我,一个从1.0到2.0的跟随者这里只总结一句话 ...
随机推荐
- js 查询 添加 删除 练习
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title&g ...
- 真正从0开始用Unity3D制作类战地2玩法的类龙之谷、王者荣耀的手游(暨全平台游戏)
如题,(从2017年10月18日开始)正在利用业余时间研发一款神泣Shaiya2手游,引擎用Unity3D. 原因主要有2点: 对神泣太多感情,希望能做点什么来纪念乃至留下神泣这款网游: 时机已到,是 ...
- Android基础知识03—Activity的基本用法
------Activity 活动------ 活动 Activity 是一种包含用户界面的组件,即一个界面就是一个活动 创建活动的过程: >> 创建一个类,继承自Activity类,并且 ...
- Xuan.UWP.Framework
开篇博客,以前总是懒,不喜欢写博客什么,其实都是给自己找理由,从今天开始有空就写写博客.新手博客,写得不好轻喷,哈哈! 开始正题,微软移动平台,从WP7开始,经历了WP8,然后WP8.1,到目前得Wi ...
- (转)利用JConsole工具监控java程序内存和JVM
转自:http://www.cnblogs.com/luihengk/p/5446279.html 一.找到java应用程序对应的进程PI 性能测试应用程序访问地址:http://192.168.29 ...
- (转)Nginx与tomcat组合的简单使用
原文出自:http://www.cnblogs.com/naaoveGIS/ 1.背景 项目中瓦片资源越来越多,如果提高瓦片的访问效率是一个需要解决的问题.这里,我们考虑使用Nginx来代理静态资源进 ...
- Java多线程编程核心技术
Java多线程编程核心技术 这本书有利于对Java多线程API的理解,但不容易从中总结规律. JDK文档 1. Thread类 部分源码: public class Thread implements ...
- vue数据请求
我是vue菜鸟,第一次用vue做项目,写一些自己的理解,可能有些不正确,欢迎纠正. vue开发环境要配置本地代理服务.把config文件加下的index.js里的dev添加一些内容, dev: { e ...
- LeetCode 54. Spiral Matrix(螺旋矩阵)
Given a matrix of m x n elements (m rows, n columns), return all elements of the matrix in spiral or ...
- SAP开发快捷键
F1 帮助 F2 回车确认(在某些地方可用,比如ABAP) F3 返回 F4 选择输入项 F5 新增 F6 复制为... F7 全选 F8 选择 ...