当前位置: 首页 > 科技观察

从实战出发,说说Nginx的信号设置

时间:2023-03-12 18:06:09 科技观察

Nginx的信号操作是日常运维中最常见也是非常重要的。如果这个环节出现错误,可能会导致业务异常,带来损失。所以弄清楚Nginx的信号集是非常有必要的,可以帮助我们更好的处理这些任务。本文导航00%场景复现05%问题分析33%Nginx信号设置46%stopvsquit54%reload58%reopen61%热更新71%worker进程如何处理来自master的信号消息81%总结94%Preface工作前,某引流测试机的一个ngx_lua服务突然出现一些HTTP/500响应。从错误日志中打印的堆栈来看,是因为新发布版本中添加的一个Lua表不存在,并且有代码对其进行索引。的。这令人费解。如果是版本回滚导致的,那为什么使用这张Lua表的代码没有回滚,而定义这张表的代码却回滚了呢?经过排查,发现当时Nginx刚刚完成热更新操作,旧的master进程依然存在。因为重启了机器,先断了引流流量(但是还有一些请求还在),同时系统触发了nginx-sstop。引起了问题。场景复现接下来,我将在我安装了fedora26的虚拟机上使用原生的Nginx来复现这个过程。我使用的Nginx版本是目前最新的1.13.4。首先启动Nginx:alex@Fedora26-64:~/bin_install/nginx./sbin/nginxalex@Fedora26-64:~/bin_install/nginxpsauxf|grepginxalex61740.00.028876428?Ss14:350:00nginx:masterprocess./sbin/nginxalex61750.0420.269033er_process:3er_pross?可以看到master和worker都已经在运行了。然后我们向主机发送一个SIGUSR2信号。当Nginx核心收到这个信号后,就会触发热更新。alex@Fedora26-64:~/bin_install/nginxkill-USR26174alex@Fedora26-64:~/bin_install/nginxpsauxf|grepnginxalex61740.00.1288761996?Ss14:350:00nginx:masterprocess./sbin/nginxalex61750.00.2603ng_xinx:masterprocess:workerprocessalex62090.00.2288762804?S14:370:00\_nginx:masterprocess./sbin/nginxalex62130.00.1293642004?S14:370:00\_nginx:workerprocess可以看到新的master和masterfork的worker已经在运行了。然后我们向老主人发送一个SIGWINCH信号。老master收到这个信号后,会向它的worker发送SIGQUIT,于是老master的worker进程就会退出:alex@Fedora26-64:~/bin_install/nginxkill-WINCH6174alex@Fedora26-64:~/bin_install/nginxpsauxf|grepnginxalex61740.00.1288761996?Ss14:350:00nginx:masterprocess./sbin/nginxalex62090.00.2288762804?S14:370:00\_nginx:masterprocess./sbin/nginxalex62130.00.1293642004?sbin/nginxalex62130.00.1293642004?S14:370:00\_nginx:masterprocess./sbin/nginxalex62130.00.1293642004?此时只有workerxin:0:S14:0:S14:0:00\_nginx:masterprocessoldmaster、newmaster和newmaster的worker都在跑,跟当时线上跑的情况差不多。然后我们使用停止命令:alex@Fedora26-64:~/bin_install/nginx./sbin/nginx-sstopalex@Fedora26-64:~/bin_install/nginxpsauxf|grepnginxalex61740.00.1288761996?ss14:350:00nginx:masterprocess./sbin/nginxalex63010.00.2293642124?S14:490:00\_nginx:workerprocess我们会发现新的master和它的worker都已经退出了,而老的master还在运行并且已经产生了worker。网上就是这么回事。其实这种现象与Nginx本身的设计有关:当老master准备fork新master时,会将文件Nginx.pid重命名为Nginx.pid.oldbin,然后新master从fork到新建一个Nginx.pid,这个文件会记录新master的pid。Nginx认为,热更新完成后,老master的使命就差不多结束了,之后随时会退出,所以后续的操作应该由新master接手。当然,在老master不退出的情况下,通过向新master发送SIGUSR2再次尝试热更新是无效的。新的主人将忽略这个信号并继续自己的工作。问题分析更不幸的是,我们上面提到的Lua表,定义它的Lua文件,早在init_by_luahook运行的时候就已经被LuaJIT加载到内存并编译成字节码了,所以显然老高手肯定不会有这个Lua表,因为它加载的Lua代码部分是旧版本。init_by_lua中没有使用索引表的Lua代码,这些代码是在worker进程中加载??的,此时工程目录下的代码都是***,所以worker进程加载的都是***代码,如果这些worker进程处理相关请求,会出现Lua运行时错误,对外表现为对应的HTTP500。吸取了这个教训之后,我们需要更加合理的关闭我们的Nginx服务。因此,一个更合理的Nginx服务启动和关闭脚本是很有必要的。网上流传的一些脚本并没有处理这种现象。我们应该参考官方的Nginx脚本。stop(){echo-n$"Stopping$prog:"killproc$prog-QUITretval=$?echo[$retval-eq0]&&rm-f$lockfilereturn$retval}这段代码引用自Nginx官方/etc/init.dd/Nginx[1].Nginx信号集接下来我们来全面梳理一下Nginx信号集。源码的细节这里不再赘述。有兴趣的同学可以自行阅读相关源码。我们有两种方式向master进程发送信号,一种是通过nginx-ssignal来操作,另一种是通过kill命令手动发送。第一种方法的原理是生成一个新进程,通过Nginx.pid文件获取master进程的pid,然后向master发送相应的信号,然后退出。这个过程称为信号器。第二种方式需要我们理解Nginx-s信号到真实信号的映射。下表是它们的映射关系:stopvsquitstop发送一个SIGTERM信号,表示需要强制退出,quit发送一个SIGQUIT,表示优雅退出。具体区别在于,worker进程收到SIGQUIT消息后(注意不是直接发送信号,所以这里用消息代替),会关闭监听socket,关闭当前空闲连接(可以连接的连接)被抢占),然后提前处理好所有定时器事件,***退出。没有特殊情况,应该使用quit而不是stop。reloadmaster进程收到SIGHUP后,会重新解析配置文件,申请共享内存等一系列工作,然后生成一批新的worker进程。最后,它会向老的worker进程发送一个SIGQUIT对应的消息,最终noseam实现restart操作。reopenmaster进程收到SIGUSR1后,会重新打开所有打开的文件(比如日志),然后向各个worker进程发送SIGUSR1信息。工作进程收到信号后,会执行同样的操作。reopen可用于日志切割。比如Nginx官方提供了解决方案:$mvaccess.logaccess.log.0$kill-USR1`catmaster.nginx.pid`$sleep1$gzipaccess.log.0#dosomethingwithaccess.log.0sleephere1是必须的,因为这里有是master进程向worker进程发送SIGUSR1消息到worker进程真正重新打开access.log之间的一个时间窗口,此时worker进程仍然将日志写入文件access.log.0。通过sleep1s,保证了access.log.0日志信息的完整性(如果没有sleep,直接压缩,很有可能会丢失日志)。热更新有时我们需要进行二进制热更新。Nginx在设计中就包含了这个功能,但是不能通过Nginx提供的命令行来完成。我们需要手动发送信号。通过以上问题的重现,你应该已经学会了如何进行热更新了。我们首先需要发送SIGUSR2给当前master进程,然后master将Nginx.pid重命名为Nginx.pid.oldbin,然后fork一个新进程,新进程会使用execve系统调用替换当前进程镜像使用新的NginxELF文件并成为新的主进程。新的master进程启动后,会进行配置文件解析等操作,然后fork出一个新的worker进程开始工作。然后我们给oldmaster发送一个SIGWINCH信号,然后oldmaster进程给它的worker进程发送一个SIGQUIT消息,这样worker进程就退出了。向master进程发送SIGWINCH和SIGQUIT会导致worker进程退出,但前者不会导致master进程退出。***,如果我们觉得老主进程的任务完成了,我们可以给它发送一个SIGQUIT信号,让它退出。worker进程如何处理来自master的signal消息其实master进程与worker进程再次通信,不是使用kill函数,而是使用通过pipeline实现的Nginxchannel。master进程向管道的一端写入信息(如信号信息),worker进程从另一端接收信息,当Nginxchannel事件被添加到事件调度器(如epoll、kqueue)中时worker进程刚刚启动,所以当master有数据发送过来时,eventscheduler可以通知它到达。Nginx这样设计是有原因的。Nginx作为一款优秀的反向代理服务器,追求的是最高的高性能,signalhandler会中断worker进程的运行,让所有事件暂停一个时间窗口。性能上有一定损失。很多人可能会认为,当master进程向worker进程发送信息时,worker进程会立即响应相应的操作。但是worker进程很忙,不停地处理网络事件和定时器事件。当调用Nginxchannel事件的处理程序后,Nginx只是处理一些标志。这些动作的实际执行是在一轮事件调度完成之后。因此,它们之间存在一个时间窗口,尤其是在业务复杂、流量巨大的时候,这个窗口可能会被拉大,这也是为什么Nginx官方提供的日志切割方案需要sleep1s的原因。当然,我们也可以绕过master进程,直接向worker进程发送信号。总结了工人可以处理的信号。Nginx信号操作是日常运维中最常见也是非常重要的。如果此环节出现错误,可能会导致业务异常,造成损失。所以弄清楚Nginx的信号集是非常有必要的,可以帮助我们更好的处理这些任务。另外,通过这次对Nginx信号集的体验和了解,我们认为以下几点比较重要:谨慎使用Nginx-sstop,尽量使用Nginx-squit。热更新后,如果确定业务没问题,尽量让老主进程退出关键信号操作完成后,等待一段时间,避免时间窗的影响,不要直接向工作进程发送信号