我眼中的 Nginx(四):是什么让你的 Nginx 服务退出这么慢?

张超:又拍云系统开发高级工程师,负责又拍云 CDN 平台相关组件的更新及维护。Github ID: tokers,活跃于 OpenResty 社区和 Nginx 邮件列表等开源社区,专一于服务端技术的研究;曾为 ngx_lua 贡献源码,在 Nginx、ngx_lua、CDN 性能优化、日志优化方面有较为深刻的研究。

 

笔者曾今在更新 Nginx 服务的过程当中发现旧的 Nginx worker 进程退出很是缓慢(旧的 worker 进程始终处在 "is shutting down" 的状态),对此很是好奇,并对此展开了一些研究,本文将介绍 Nginx worker 进程退出时的准备步骤,延缓退出的缘由,并介绍对应的解决办法。html

准备退出

当 worker 进程接收到 master 进程要求它退出的指令后(详见笔者另外一篇文章:谈谈 Nginx 信号集),它便会开始为退出作准备。nginx

首先 worker 进程会将正在监听的套接字从事件分发器(epoll,kqueue 等)中删除,并将它们关闭,以后它将再也不处理链接事件。git

接着关闭全部的空闲链接,所谓的空闲链接,指的是当前没有请求正在使用的链接,例如 Nginx 和后端服务器维持的长链接,或者 ngx_lua Cosocket 对象底层的长链接。github

接着 worker 进程会等待全部定时器过时(ngx_lua 提供给用户使用的定时器比较特殊,在退出阶段,它会提早过时,其余的 Nginx 内部的定时器不会提早过时),并同时处理还没有完成的事件。等事件处理完毕后, worker 进程会调用全部模块注册的 exit_process 钩子,最后退出。web

退出被延缓

了解了 worker 进程退出时的准备过程后,咱们能够深刻分析为何有的时候退出如此缓慢。后端

根据笔者目前的分析,目前有如下两种状况会延缓 worker 进程的退出:性能优化

  • ngx_lua:在提早过时的定时器中使用 Cosocket
  • Nginx http/2 实现上的一个 bug

第一种状况曾有人在 ngx_lua 的 issue 页面提出过( Cosocket :setkeepalive() in a a premature timer handler blocks Nginx worker from exiting · Issue #1279 · openresty/lua-Nginx-module)[1]。服务器

好比 issue 中的示例代码:websocket

ngx.timer.at(100, function ()
-- This blocks Nginx worker from exiting
    local timer_sock = ngx.socket.tcp()
    timer_sock:connect("127.0.0.1", 8080)
    timer_sock:setkeepalive()
end)

固然,这段代码省略了一些错误处理,可是用以解释问题已经足够。这段代码注册了一个定时器,只要这个定时器运行,就会建立一个 Cosocket 对象,而后去链接本机的 8080 端口,而后立刻将这个对象底层的链接置为 keep alive 状态。网络

先说 connect 函数,若是和对端的链接不能一次性完成,ngx_lua 会为此次链接操做添加一个定时器,用以判断链接超时,固然这里是链接本机的端口,所以几乎不会出现链接超时(对端异常除外)。

假如这里所要链接的对端处在公网,并且网络情况不理想的话,链接超时就有可能发生了,ngx_lua 默认的 Cosocket 链接超时是 60s(lua_socket_connect_timeout),这意味着这个 worker 进程会等待至少 60s,而后再退出。

一样地,setkeepalive 也会为这条链接设置一个超时时间,默认也是 60s( lua_socket_keepalive_timeout) ,所以 worker 进程也不得不等到这个定时器过时,或者某个时刻对端主动关闭/异常关闭这条链接后,它才可以退出。

读者可能会有疑惑,以前讲到 worker 进程退出时会主动关闭这些空闲的长链接,那为何这个示例还回形成 worker 进程退出那么慢呢?即便是本机链接,也有可能出现没法一次完成链接( EAGAIN) 的状况,此时当前定时器的 Lua 协程就会被挂起,所以当 worker 进程在关闭全部空闲链接的时候,这个示例里 setkeepalive 是还没被执行到的(甚至可能链接也没有创建完成),因此这条链接在当时不是空闲的。直到后来某个时刻链接创建完成或者超时,当时的 Lua 协程从新获得运行机会,才会为这条链接添加定时器,置为空闲状态。

另一个阻碍 worker 进程退出的缘由来自于一个 Nginx HTTP/2 模块实现上的缺陷(见 Stale workers not exiting after reload (with HTTP/2 long poll requests))[2]。这个问题在 Nginx/1.11.6 发布以后就修复了(见 Nginx: 5e95b9fb33b7)[3],1.11.6 以前的版本,若是一个 HTTP/2 协议的客户端一直在打开新的流,会致使这条链接上一直有事件在处理(固然会伴随着建立定时器),这会致使 worker 进程会一直没法退出,直到这条链接断开。

Nginx 支持透明代理 websocket 链接。在 Nginx/1.13.7 版本之前,若是 worker 进程存在一些 websocket 链接,并且链接上常常有数据传送,使得链接一直在正常工做的话,即便 worker 进程收到来自 master 的退出指令,它也没法马上退出,它须要等到这些链接出现异常、超时或者是某一端主动断开后,才能正常退出。

shutdown timeout

旧 worker 进程不能及时退出,就会一直占用着系统资源(CPU、内存和文件描述符等),这对系统资源是一种浪费,所以 Nginx/1.11.11 加入了一个新的指令(即 worker_shutdown_timeout,见 Core functionality)[4],容许用户自定义 shutdown 超时时间,若是一个 worker 在接收到退出的指令后通过 worker_shutdown_timeout 时长后还不能退出,就会被强制退出。

它的实现原理(Nginx: 97c99bb43737)[5]也是经过建立定时器来实现的,一旦定时器过时, 全部链接都会被设置为 close 和 error 状态(c->error = 1,c->close = 1),这个标志位事实上意味着 TCP 链接异常,Nginx 设计上对于这种状态的链接,都会马上结束对应的全部请求、事件。经过这样一个标志位的设置,就达到了强制关闭全部链接、删除全部定时器的目的,最终及时退出旧的 worker 进程,释放系统资源。

虽然这个功能早在 Nginx/1.11.11 就加入了,可是没有彻底覆盖到全部的状况,例如上文所述的 websocket 链接的处理,那部分代码并无判断 c->close 和 c->error 的状态位。因此仍然没法尽快终止这些 websocket 链接。直到 Nginx/1.13.7,这个问题才被修复。因此若是读者们遇到相似的问题,能够考虑升级 Nginx 至少到 1.13.7 版本。


[1] issue 页面: 

[2] 缺陷: 

[3] 修复: 

[4] 指令: 

[5] 原理: 

 

《我眼中的 Nginx》系列:

我眼中的 Nginx(三):Nginx 变量和变量插值
我眼中的 Nginx(二):HTTP/2 dynamic table size update
我眼中的 Nginx(一):Nginx 和位运算

相关文章
相关标签/搜索