理解 Nginx 的优雅退出机制 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
doggg
V2EX    程序员

理解 Nginx 的优雅退出机制

  •  4
     
  •   doggg
    vm-001 2024-05-11 20:27:57 +08:00 3334 次点击
    这是一个创建于 525 天前的主题,其中的信息可能已经有所发展或是发生改变。

    理解 Nginx 的优雅退出机制

    Nginx 是目前最流行的反向代理和 Web 服务器,它的性能非常高,单机可处理 10 万 RPS ,C10k 仅占用 2.5MB 内存。Nginx 被广泛应用于代理、负载均衡、HTTP 缓存、CDN 、API Gateway 等不同领域。

    Nginx 流行的原因之一还包括它本身支持零停机的配置热更新(reload)。

    什么是配置热更新

    在修改 Nginx 配置文件后,通过 nginx -s reload 命令应用新配置而不需要重启,称为配置热更新。这行命令向 master 进程发送 HUP 信号,master 进程收到信号会校验配置是否合法,并启动新的 worker 进程,再向旧 worker 发送 QUIT 信号请求其执行优雅退出(graceful shutdown)。当 worker 收到信号后,会首先停止接收新请求,但不会中断当前正在处理的请求,在所有请求处理完毕后,进程才会关闭,这个过程称为优雅退出。

    优雅退出机制

    要理解 Nginx 的优雅退出,离不开阅读源码来理解它的底层实现。好在这部分逻辑并不复杂,即使没有丰富的 C 语言经验也不妨碍窥探它的原理。Nginx 的一个特点是事件循环,所有 worker 进程被创建后都会进入 ngx_worker_process_cycle 函数,process_cycle 顾名思义就是处理循环(aka 事件循环) 在一个 for ( ;; ) 循环中读取并处理 event ,也包括执行优雅退出。

    ngx_worker_process_cycle 函数解析

    函数代码如下

    // ngx_process_cycle.c static void ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data) { ngx_int_t worker = (intptr_t) data; ngx_process = NGX_PROCESS_WORKER; ngx_worker = worker; ngx_worker_process_init(cycle, worker); ngx_setproctitle("worker process"); for ( ;; ) { if (ngx_exiting) { if (ngx_event_no_timers_left() == NGX_OK) { ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting"); ngx_worker_process_exit(cycle); } } ngx_process_events_and_timers(cycle); // 事件处理入口函数 if (ngx_terminate) { ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting"); ngx_worker_process_exit(cycle); } if (ngx_quit) { ngx_quit = 0; ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "gracefully shutting down"); ngx_setproctitle("worker process is shutting down"); if (!ngx_exiting) { ngx_exiting = 1; ngx_set_shutdown_timer(cycle); ngx_close_listening_sockets(cycle); ngx_close_idle_connections(cycle); ngx_event_process_posted(cycle, &ngx_posted_events); } } // ... ngx_reopen } } 

    ngx_process_events_and_timers 是 Nginx 事件处理的入口函数,内部包括处理像 HTTP 请求的解析和响应的生成。不过这跟 graceful shutdown 无关,因此不做赘述。

    需要我们关注的只有 for ( ;; ) 块的代码

    for ( ;; ) { if (ngx_exiting) { if (ngx_event_no_timers_left() == NGX_OK) { ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting"); ngx_worker_process_exit(cycle); } } ngx_process_events_and_timers(cycle); // 事件处理入口函数 if (ngx_terminate) { ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting"); ngx_worker_process_exit(cycle); } if (ngx_quit) { ngx_quit = 0; ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "gracefully shutting down"); ngx_setproctitle("worker process is shutting down"); if (!ngx_exiting) { ngx_exiting = 1; ngx_set_shutdown_timer(cycle); ngx_close_listening_sockets(cycle); ngx_close_idle_connections(cycle); ngx_event_process_posted(cycle, &ngx_posted_events); } } } 

    这里先简单回顾一下执行 nginx -s reload 时,master 进程发生了什么

    1. master 进程接收到 HUP 信号
    2. master 进程检查配置文件的语法,并打开日志文件和新的 listening socket
    3. master 进程启动新的 worker ,向旧 worker 发送信号请求执行优雅退出

    worker 进程里通过解析信号后将 ngx_quit 置为 1 ,在 for ( ;; ) 里对应

    for ( ;; ) { // ... // QUIT signal (graceful shutdown) if (ngx_quit) { ngx_quit = 0; ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "gracefully shutting down"); ngx_setproctitle("worker process is shutting down"); if (!ngx_exiting) { ngx_exiting = 1; // 标记 worker 为正在退出状态 ngx_set_shutdown_timer(cycle); ngx_close_listening_sockets(cycle); ngx_close_idle_connections(cycle); ngx_event_process_posted(cycle, &ngx_posted_events); } } // ... } 

    1. ngx_set_shutdown_timer(cycle)

    在 1.11.11 (Mar 2017) 版本,Nginx 新增指令 worker_shutdown_timeout 用于控制优雅退出的最长超时时间,默认值在 0 ,表示不设超时时间。内部是通过 nginx timer 实现的。

    2. ngx_close_listening_sockets(cycle)

    关闭监听 socket ,确保不会再产生新的客户端连接。

    3. ngx_close_idle_connections(cycle)

    Nginx 中的 Connection 是指客户端和服务器之间的通信通道。主要类型有客户端连接(client connection)和服务端连接(upstream connection)两种。比如在 HTTP 协议中,客户端和服务器之间可以通过建立长连接(Connection: keep-alive)来避免频繁建立连接的开销,在 Nginx 每次处理完连接上的请求时都会将 Connection 的 idle 属性设置为 1 ,表示处于空闲状态。所以 ngx_close_idle_connections 的目的是关闭所有空闲的长连接(比如和客户端的长连接),底层通过调用 socket close 函数关闭套接字。

    由于 ngx_exiting 被置为 1 ,那么下一次循环会进入 for ( ;; ) 里开头的 if (ngx_exiting) 分支

    if (ngx_exiting) { if (ngx_event_no_timers_left() == NGX_OK) { ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting"); ngx_worker_process_exit(cycle); } } 

    如果当前已经没有需要处理的 event 和 timer ,则调用 ngx_worker_process_exit 关闭 worker ,否则继续调用 ngx_process_events_and_timers 函数处理所有未处理完成的请求。

    以上就是 Nginx 优雅退出的实现机制。我们做个简单的总结,worker 要执行优雅退出,先通过关闭 listening socket 来停止和客户端建立新连接(新连接会由新 worker 处理),正在处理的请求不会中断,而是等待处理完成后才会退出 worker 进程。

    文章结尾,笔者留下几个问题供有兴趣的读者思考

    • nginx -s reload 真的是零停机吗?
    • Nginx reload 之后,客户端先前通过 Connection: keep-alive 保持的长连接是否还有效呢?
    第 1 条附言    2024-05-12 17:17:38 +08:00
    最后两个问题笔者留给大家自行探索或在评论区相互探讨,我认为探索得到的收获比直接得到答案更加丰富深刻。

    比如读者可以尝试
    - 用熟悉的编程语言发送两个 HTTP 请求,通过程序断点来和 nginx -s reload 来测试第二个请求是否会失败
    - 通过使用 wrk 命令压测来验证 nginx -s reload 是否会造成请求失败
    - 通过 tcpdump 命令抓取数据包来佐证得到的行为

    如果大家特别感兴趣,笔者后续会再写一遍文章来揭晓谜题,欢迎大家 follow 我的推特收听后面的消息,我偶尔还会分享理财经验还有人生杂谈(推特号需要通过 github 去查找 XD )
    10 条回复    2024-05-13 14:03:14 +08:00
    NeedI09in
        1
    NeedI09in  
       2024-05-11 23:01:41 +08:00
    问题 2:
    从小聪明的角度,感觉函数调用名字是 ngx_close_idle_connections ,应该只是关闭 client 和 upstream 的空闲连接。
    从源代码角度分析,收到 NGX_RECONFIGURE_SIGNAL 信号后会走到 ngx_reconfigure ,会通过 ngx_start_worker_processes 启动一个新的 worker ,然后 ngx_signal_worker_processes 会处理掉旧 worker 。旧 worker 的处理方式跟大佬文章里写的一样,旧 worker 的 connection 是否还有效,我是从 ngx_close_idle_connections 出发看他是怎么获取的 connection 。发现他是这么取的 connections
    ``` c
    void
    ngx_close_idle_connections(ngx_cycle_t *cycle)
    {
    ngx_uint_t i;
    ngx_connection_t *c;

    c = cycle->connections;

    for (i = 0; i < cycle->connection_n; i++) {

    /* THREAD: lock */

    if (c[i].fd != (ngx_socket_t) -1 && c[i].idle) {
    c[i].close = 1;
    c[i].read->handler(c[i].read);
    }
    }
    }

    ```


    侧面追踪发现 shutdown 超时也会出发关闭连接。大胆猜测 cycle->connections 就是连接池

    ``` c

    static void
    ngx_shutdown_timer_handler(ngx_event_t *ev)
    {
    ngx_uint_t i;
    ngx_cycle_t *cycle;
    ngx_connection_t *c;

    cycle = ev->data;

    c = cycle->connections;

    for (i = 0; i < cycle->connection_n; i++) {

    if (c[i].fd == (ngx_socket_t) -1
    || c[i].read == NULL
    || c[i].read->accept
    || c[i].read->channel
    || c[i].read->resolver)
    {
    continue;
    }

    ngx_log_debug1(NGX_LOG_DEBUG_CORE, ev->log, 0,
    "*%uA shutdown timeout", c[i].number);

    c[i].close = 1;
    c[i].error = 1;

    c[i].read->handler(c[i].read);
    }
    }
    ```

    那就追踪 cycle ,从 ngx_master_process_cycle 函数追踪到这一行代码
    ``` c

    cycle = ngx_init_cycle(cycle);
    if (cycle == NULL) {
    cycle = (ngx_cycle_t *) ngx_cycle;
    continue;
    }

    ```

    显然,如果这里的 ngx_init_cycle 返回是 NULL ,那么长连接就会无效,问题就回到了 ngx_init_cycle 里发生了什么。大胆猜测这个 init_cycle 正常情况返回自己,异常情况返回 Null 。点进去还真是。
    所以结论就是非空闲长连接不会释放,cycle 还是老 cycle ,看起来很合理

    不知道我推论对不对,烦请大佬解惑。大佬的文章收益匪浅,看完有种会捕鱼了的快乐,非常感谢。
    不过想请教大佬 ngx_temp_pool 是做什么用的,为何 ngx_temp_pool 是 Null 会需要清理长连接呢?
    也就是这段代码 https://github.com/nginx/nginx/blob/master/src/core/ngx_cycle.c#L778C1-L801C6

    最后感谢大佬的输出,受益匪浅。
    NeedI09in
        2
    NeedI09in  
       2024-05-11 23:12:00 +08:00
    @NeedI09in 补充一下,如果说的有误,麻烦大佬斧正,我对 nginx 底层源码了解甚少,只是看了这篇文章,尝试了解了一下源码。如果说的有什么不妥的地方,烦请大佬斧正,感谢。
    不知道 ngx_close_idle_connections 是否是只 close 客户端的长连接,烦请大佬斧正。
    shinession
        3
    shinession  
       2024-05-12 08:33:44 +08:00
    nginx reload 多久会生效? 刚开始用的时候试过 reload, 发现新配置并没生效, 等了几分钟还是放弃, 用了 stop
    NeedI09in
        4
    NeedI09in  
       2024-05-12 16:11:43 +08:00
    问题 1:
    从大佬文章介绍出发,先看如何关闭 listen port
    发现下列代码
    ``` c

    ls = cycle->listening.elts;
    for (i = 0; i < cycle->listening.nelts; i++) {

    #if (NGX_QUIC)
    if (ls[i].quic) {
    continue;
    }
    #endif

    c = ls[i].connection;

    if (c) {
    if (c->read->active) {
    if (ngx_event_flags & NGX_USE_EPOLL_EVENT) {

    /*
    * it seems that Linux-2.6.x OpenVZ sends events
    * for closed shared listening sockets unless
    * the events was explicitly deleted
    */

    ngx_del_event(c->read, NGX_READ_EVENT, 0);

    } else {
    ngx_del_event(c->read, NGX_READ_EVENT, NGX_CLOSE_EVENT);
    }
    }

    ngx_free_connection(c);

    c->fd = (ngx_socket_t) -1;
    }

    ```
    cycle->listening.elts 明显存储着监听相关对象
    接着全局查询 cycle->listening.elts

    https://github.com/nginx/nginx/blob/6f7494081ae8a56664afb480eff583d639b60ab4/src/core/ngx_cycle.c#L505-L620 部分找到代码,这部分应该是处理 listening 数组的一些信号,调用 ngx_open_listening_sockets 开始监听 cycle 中的端口。

    那他真的是零停机吗?

    从流程上来看是的,ngx_init_cycle 阶段就已经开始监听端口了,在启动新 worker 后,会按照顺序删除用于接受 IO 通知的事件,关闭监听端口,关闭空闲连接,之后 ngx_process_events_and_timers 会保证处理完所有的未完成的请求。

    但是这一切都是基于在指定 worker_shutdown_timeout 时间内能够执行完请求的前提下,能够正常处理完请求,所以如果在这段时间内处理不完,或者接口 duration 超过设置超时时间,那这个请求就会来不及处理,就结束了。
    所以,worker_shutdown_timeout 设置要贴合实际场景。这个值如果设置非常大,就会有 worker 进程泄露的风险,设置的比较小,就会 reload 期间,存在接口返回报错。

    我认为 nginx 已经处理得很好了,在 reload 期间,有些耗时较长的接口会存在一定问题,但是要根据具体场景,去规划这个超时值就可以避免,我认为他是零停机的。
    doggg
        5
    doggg  
    OP
       2024-05-12 16:53:38 +08:00
    @ 如果 nginx conf 都正常的话,理论上 nginx reload 后新的 worker 创建后就可以服务新连接和请求了。你是不会指 worker shutting down 的时间太久了?`worker_shutdown_timeout` 可以试试这种强制设置旧 worker 的最长关闭时间。
    busier
        6
    busier  
       2024-05-13 06:21:54 +08:00
    这又不是什么新鲜功能

    早在 Windows Server 2003 的 IIS6 上,就通过 App Pool 程序池解决了所谓的“幽雅”重启,并且 IIS6 解决的还是对目标程序脚本语言环境,诸如.net .php 环境的“优雅”重启。nginx 还只能“优雅”重启自身,php-fpm 他还管不着。
    NeedI09in
        7
    NeedI09in  
       2024-05-13 09:53:48 +08:00
    @doggg 我可能没有表述清楚,我是指在 reload 前进来的请求耗时较长,且`worker_shutdown_timeout `设置较短,这样的话请求就返回报错了。但是这个不影响,还是看实际场景的,我是想到了这个例子哈哈。
    NeedI09in
        8
    NeedI09in  
       2024-05-13 10:00:59 +08:00
    @doggg 嗯嗯,关于进程泄漏的说法应该是设置比较长,然后卡在 timer 那里。
    我还遇到过一种进程泄漏,是 worker 里起了 timer ,然后 timer 是一直递归的,发现 reload 后,worker 一直处于 shutting down 。我加了检测 worker.exiting 进程便不泄漏了,现在看来应该是卡在 ngx_process_events_and_timers 里。
    doggg
        9
    doggg  
    OP
       2024-05-13 11:45:48 +08:00
    @NeedI09in 5 楼我的评论其实是回复 @shinession 的(没 @ 出来)

    @NeedI09in 你的探究比我直接给出答案更有意义,文章结尾我最后 append 了一些测试方法和工具,希望对你有帮助
    NeedI09in
        10
    NeedI09in  
       2024-05-13 14:03:14 +08:00
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2501 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 66ms UTC 05:12 PVG 13:12 LAX 22:12 JFK 01:12
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86