V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
doggg
V2EX  ›  程序员

理解 Nginx 的优雅退出机制

  •  4
     
  •   doggg ·
    vm-001 · 11 天前 · 2292 次点击

    理解 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 条附言  ·  11 天前
    最后两个问题笔者留给大家自行探索或在评论区相互探讨,我认为探索得到的收获比直接得到答案更加丰富深刻。

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

    如果大家特别感兴趣,笔者后续会再写一遍文章来揭晓谜题,欢迎大家 follow 我的推特收听后面的消息,我偶尔还会分享理财经验还有人生杂谈(推特号需要通过 github 去查找 XD )
    10 条回复    2024-05-13 14:03:14 +08:00
    NeedI09in
        1
    NeedI09in  
       11 天前
    问题 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  
       11 天前
    @NeedI09in 补充一下,如果说的有误,麻烦大佬斧正,我对 nginx 底层源码了解甚少,只是看了这篇文章,尝试了解了一下源码。如果说的有什么不妥的地方,烦请大佬斧正,感谢。
    不知道 ngx_close_idle_connections 是否是只 close 客户端的长连接,烦请大佬斧正。
    shinession
        3
    shinession  
       11 天前
    nginx reload 多久会生效? 刚开始用的时候试过 reload, 发现新配置并没生效, 等了几分钟还是放弃, 用了 stop
    NeedI09in
        4
    NeedI09in  
       11 天前
    问题 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
       11 天前
    @ 如果 nginx conf 都正常的话,理论上 nginx reload 后新的 worker 创建后就可以服务新连接和请求了。你是不会指 worker shutting down 的时间太久了?`worker_shutdown_timeout` 可以试试这种强制设置旧 worker 的最长关闭时间。
    busier
        6
    busier  
       10 天前
    这又不是什么新鲜功能

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

    @NeedI09in 你的探究比我直接给出答案更有意义,文章结尾我最后 append 了一些测试方法和工具,希望对你有帮助
    NeedI09in
        10
    NeedI09in  
       10 天前
    @doggg 👌
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   4914 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 09:33 · PVG 17:33 · LAX 02:33 · JFK 05:33
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.