AIGC标识 nginx-源码带读-04-内存管理与进程模型

NGINX 源码带读 第4篇:内存管理与进程模型

本篇目标

理解NGINX的内存管理机制和多进程模型,包括内存池分配器、Slab分配器、共享内存和master-worker架构。

前置知识

  • 了解fork()和Copy-on-Write
  • 了解信号处理
  • 阅读过第1-3篇

1. 内存池:ngx_palloc.c

1.1 设计理念

NGINX使用内存池(memory pool)管理内存。大部分内存分配从池中获取,池销毁时一次性释放所有内存。这避免了频繁的malloc/free和内存泄漏。

1.2 ngx_pool_t 结构

// src/core/ngx_palloc.h
typedef struct {
    u_char       *last;       // 下次分配的起始位置
    u_char       *end;        // 数据块末尾
    ngx_pool_t   *next;       // 下一个数据块
    ngx_uint_t    failed;     // 分配失败次数
} ngx_pool_data_t;

struct ngx_pool_s {
    ngx_pool_data_t d;        // 当前可分配数据
    size_t max;               // 小块分配的上限(默认4096)
    ngx_pool_t *current;      // 当前使用的数据块
    ngx_chain_t *chain;       // 空闲chain链表
    ngx_pool_large_t *large;  // 大块分配链表
    ngx_pool_cleanup_t *cleanup; // 清理回调链表
    ngx_log_t *log;           // 日志
};

1.3 分配策略

分配大小 策略 时间复杂度
<= max 从当前数据块中分配(指针移动) O(1)
> max 直接malloc,加入large链表 O(1)

小块分配是O(1)的——只需移动指针。大块分配走系统malloc但注册到large链表。

1.4 三种池

池类型 生命周期 用途 默认大小
进程池 整个worker生命周期 连接池、配置 16KB
请求池 单个请求 请求处理期间的所有分配 4KB
连接池 单个连接 连接数据 -

1.5 ngx_create_pool()

// src/core/ngx_palloc.c
ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log) {
    ngx_pool_t *p;

    // 分配内存(16字节对齐)
    p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
    if (p == NULL) {
        return NULL;
    }

    // 初始化:pool头部就在第一个数据块内部
    p->d.last = (u_char *) p + sizeof(ngx_pool_t);  // 跳过pool结构体
    p->d.end = (u_char *) p + size;
    p->d.next = NULL;
    p->d.failed = 0;

    // 计算max:min(size - sizeof(ngx_pool_t), NGX_MAX_ALLOC_FROM_POOL)
    size -= sizeof(ngx_pool_t);
    p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;

    // 初始化其他字段
    p->current = p;
    p->chain = NULL;
    p->large = NULL;
    p->cleanup = NULL;

    return p;
}

关键设计:pool头部与第一个数据块共存于同一块内存中,减少了分配次数。

1.6 ngx_palloc() —— 小块分配

void *ngx_palloc(ngx_pool_t *pool, size_t size) {
    if (size <= pool->max) {
        // 小块分配
        return ngx_palloc_small(pool, size, pool->clean);
    }
    // 大块分配
    return ngx_palloc_large(pool, size);
}

static void *ngx_palloc_small(ngx_pool_t *pool, size_t size,
    ngx_uint_t align) {
    u_char *m;
    ngx_pool_t *p;

    p = pool->current;

    do {
        m = p->d.last;

        if (align) {
            // 16字节对齐
            m = ngx_align_ptr(m, NGX_POOL_ALIGNMENT);
        }

        // 检查当前数据块是否有足够空间
        if ((size_t) (p->d.end - m) >= size) {
            p->d.last = m + size;  // 指针移动分配
            return m;
        }

        // 当前数据块空间不足,尝试下一个
        p = p->d.next;

    } while (p);

    // 所有数据块都不够,创建新数据块
    return ngx_palloc_block(pool, size);
}

1.7 ngx_palloc_block() —— 创建新数据块

static void *ngx_palloc_block(ngx_pool_t *pool, size_t size) {
    u_char *m;
    size_t psize;
    ngx_pool_t *p, *new;

    // 新数据块大小与第一个数据块相同
    psize = (size_t) (pool->d.end - (u_char *) pool);

    m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
    if (m == NULL) {
        return NULL;
    }

    new = (ngx_pool_t *) m;
    new->d.last = m + sizeof(ngx_pool_data_t);  // 跳过data结构体
    new->d.end = m + psize;
    new->d.next = NULL;
    new->d.failed = 0;

    // 在所有数据块中分配空间
    for (p = pool->current; p->d.next; p = p->d.next) {
        if (p->d.failed++ > 4) {
            // 优化:跳过失败过多的数据块
            pool->current = p->d.next;
        }
    }

    // 链接到链表末尾
    p->d.next = new;

    // 在新数据块中分配
    m = ngx_align_ptr(new->d.last, NGX_POOL_ALIGNMENT);
    new->d.last = m + size;

    return m;
}

关键优化:当数据块分配失败超过4次时,跳过该数据块,避免重复尝试。

1.8 ngx_palloc_large() —— 大块分配

static void *ngx_palloc_large(ngx_pool_t *pool, size_t size) {
    void *p;
    ngx_uint_t n;
    ngx_pool_large_t *large;

    // 直接malloc
    p = ngx_alloc(size, pool->log);
    if (p == NULL) {
        return NULL;
    }

    // 查找空闲的large节点(最多查找4个)
    n = 0;
    for (large = pool->large; large; large = large->next) {
        if (large->alloc == NULL) {
            large->alloc = p;
            return p;
        }
        if (n++ > 3) {
            break;  // 优化:避免遍历过多节点
        }
    }

    // 创建新的large节点(从pool小块分配)
    large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);
    if (large == NULL) {
        ngx_free(p);
        return NULL;
    }

    // 插入large链表头部
    large->alloc = p;
    large->next = pool->large;
    pool->large = large;

    return p;
}

1.9 ngx_pfree() —— 大块释放

ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p) {
    ngx_pool_large_t *l;

    for (l = pool->large; l; l = l->next) {
        if (p == l->alloc) {
            ngx_free(l->alloc);
            l->alloc = NULL;  // 标记为空闲,节点保留
            return NGX_OK;
        }
    }

    return NGX_DECLINED;  // 小块不能单独释放
}

设计决策:小块分配不能单独释放,只能在pool销毁时批量释放。这简化了内存管理,但可能导致内存浪费。

1.10 ngx_destroy_pool() —— 销毁内存池

void ngx_destroy_pool(ngx_pool_t *pool) {
    ngx_pool_t *p, *n;
    ngx_pool_large_t *l;
    ngx_pool_cleanup_t *c;

    // 1. 执行所有清理回调
    for (c = pool->cleanup; c; c = c->next) {
        if (c->handler) {
            c->handler(c->data);
        }
    }

    // 2. 释放所有大块
    for (l = pool->large; l; l = l->next) {
        if (l->alloc) {
            ngx_free(l->alloc);
        }
    }

    // 3. 释放所有数据块(从后向前)
    for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
        // 在NGX_DEBUG模式下,先记录日志再释放
        ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
                       "free: %p", p);
        ngx_free(p);
        if (n == NULL) {
            break;
        }
    }
}

1.11 ngx_pool_cleanup_add() —— 注册清理回调

ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size) {
    ngx_pool_cleanup_t *c;

    c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));
    if (c == NULL) {
        return NULL;
    }

    if (size) {
        c->data = ngx_palloc(p, size);
        if (c->data == NULL) {
            return NULL;
        }
    } else {
        c->data = NULL;
    }

    c->handler = ngx_pool_cleanup_file;  // 默认关闭文件
    c->next = p->cleanup;
    p->cleanup = c;

    return c;
}

// 预定义的清理回调
static void ngx_pool_cleanup_file(void *data) {
    ngx_pool_cleanup_file_t *c = data;
    ngx_close_file(c->fd);
}

2. 共享内存:ngx_slab.c

2.1 用途

NGINX的多个worker进程需要共享数据(如连接计数、缓存数据、限流状态)。共享内存在多个进程间可见。

2.2 Slab分配器原理

Slab分配器将内存页面划分为固定大小的块(2的幂次),每次分配从对应大小的slot中取空闲块。

Slab分配器布局:
┌─────────────────────────────────────────────────┐
│ ngx_slab_pool_t (头部)                           │
├─────────────────────────────────────────────────┤
│ slots[0] (8B)  │ slots[1] (16B) │ slots[2] (32B)│ ...
├─────────────────────────────────────────────────┤
│ stats[0]        │ stats[1]       │ stats[2]      │ ...
├─────────────────────────────────────────────────┤
│ pages[] (页面元数据数组)                          │
├─────────────────────────────────────────────────┤
│ 数据区 (实际分配的页面)                            │
└─────────────────────────────────────────────────┘

2.3 ngx_slab_pool_t 结构

typedef struct {
    ngx_shmtx_t lock;            // 互斥锁(自旋锁)
    size_t min_size;             // 最小分配大小(8字节)
    size_t min_shift;            // 最小分配大小的log2(3)
    ngx_buf_t *log;              // 日志缓冲区
    ngx_uint_t pfree;            // 空闲页面数
    ngx_slab_page_t *pages;      // 页面数组
    ngx_slab_page_t free;        // 空闲页面链表头
    ngx_slab_stats_t *stats;     // 统计信息
    ngx_uint_t start;            // 数据区起始偏移
    ngx_uint_t end;              // 数据区结束偏移
    ngx_shmtx_t mutex;           // 互斥锁
    u_char *log_ctx;             // 日志上下文
    u_char zero;                 // 零字节
    void *addr;                  // 共享内存地址
} ngx_slab_pool_t;

2.4 页面类型

// 4种页面类型
#define NGX_SLAB_PAGE     0  // 整页(分配给大对象)
#define NGX_SLAB_BIG      1  // 大块(chunk > exact_size)
#define NGX_SLAB_EXACT    2  // 精确块(chunk = exact_size,bitmap在page->slab中)
#define NGX_SLAB_SMALL    3  // 小块(chunk < exact_size,bitmap在页面开头)

2.5 分配流程

void *ngx_slab_alloc(ngx_slab_pool_t *pool, size_t size) {
    void *p;

    // 加锁
    ngx_shmtx_lock(&pool->mutex);

    // 分配
    p = ngx_slab_alloc_locked(pool, size);

    // 解锁
    ngx_shmtx_unlock(&pool->mutex);

    return p;
}

static void *ngx_slab_alloc_locked(ngx_slab_pool_t *pool, size_t size) {
    size_t s;
    ngx_uint_t i, m, page, mask, shift;

    // 大块分配(超过页面大小的一半)
    if (size > ngx_slab_max_size) {
        return ngx_slab_alloc_pages(pool, (size >> ngx_slab_page_shift) + 1);
    }

    // 计算slot索引
    shift = ngx_slab_page_shift - ngx_slab_min_shift;
    for (s = size - 1; shift; shift--) {
        if (s < (size_t) 1 << (ngx_slab_min_shift + shift)) {
            break;
        }
    }
    m = (1 << ngx_slab_min_shift) - 1;
    page = (size - m - 1) >> ngx_slab_page_shift;
    mask = (ngx_slab_t) 1 << ngx_slab_page_shift;

    // 查找空闲chunk
    for ( ;; ) {
        // 遍历该slot的所有页面
        for (p = pool->slots[page]; p; p = p->next) {
            // 检查bitmap,找到空闲chunk
            for (i = 0; i < ngx_slab_pages; i++) {
                if (ngx_slab_page_type(p) == NGX_SLAB_SMALL) {
                    // 小块:bitmap在页面开头
                    if (p->slab & mask) {
                        p->slab &= ~mask;
                        // 返回分配的chunk
                        return (u_char *) p->s + (i << ngx_slab_page_shift);
                    }
                } else if (ngx_slab_page_type(p) == NGX_SLAB_EXACT) {
                    // 精确块:bitmap在page->slab中
                    if (p->slab != NGX_SLAB_BUSY) {
                        // 找到空闲位
                        for (i = 0; i < 32; i++) {
                            if (!(p->slab & (1 << i))) {
                                p->slab |= (1 << i);
                                return ...;
                            }
                        }
                    }
                }
            }
        }

        // 该slot没有空闲页面,创建新页面
        p = ngx_slab_alloc_pages(pool, 1);
        if (p == NULL) {
            break;
        }

        // 初始化新页面
        p->slab = ngx_slab_page_type(p);
        p->next = NULL;
        // 插入slot链表
    }

    return NULL;  // 分配失败
}

2.6 释放流程

ngx_int_t ngx_slab_free(ngx_slab_pool_t *pool, void *p) {
    ngx_shmtx_lock(&pool->mutex);
    ngx_slab_free_locked(pool, p);
    ngx_shmtx_unlock(&pool->mutex);
    return NGX_OK;
}

static void ngx_slab_free_locked(ngx_slab_pool_t *pool, void *p) {
    ngx_uint_t type;
    ngx_slab_page_t *page;

    // 计算页面索引
    page = (ngx_slab_page_t *) ((u_char *) p & ~(ngx_pagesize - 1));
    type = ngx_slab_page_type(page);

    // 清除bitmap中的对应位
    if (type == NGX_SLAB_SMALL) {
        // 小块:清除bitmap位
    } else if (type == NGX_SLAB_EXACT) {
        // 精确块:清除page->slab中的位
        page->slab &= ~(1 << bit);
    }

    // 如果页面完全空闲,释放回空闲列表
    if (page->slab == 0) {
        ngx_slab_free_pages(pool, page, 1);
    }

    // 检测double-free(通过验证chunk是否在bitmap中标记为busy)
}

2.7 共享内存初始化

// src/core/ngx_cycle.c
static ngx_int_t ngx_init_zone_pool(ngx_cycle_t *cycle, ngx_shm_zone_t *zn) {
    ngx_slab_pool_t *sp;

    sp = (ngx_slab_pool_t *) zn->addr;

    // 检查是否是重用的zone
    if (sp->addr != NULL) {
        // 重用:所有worker共享同一块内存
        return NGX_OK;
    }

    // 初始化slab
    ngx_slab_init(sp);

    // 初始化自旋锁
    ngx_shmtx_create(&sp->mutex, &sp->lock, cycle->conf.pid);

    return NGX_OK;
}

3. Master-Worker进程模型

3.1 启动流程

nginx 命令行
  -> ngx_master_process_cycle()
    -> ngx_init_cycle() 初始化cycle
    -> ngx_start_worker_processes(N) fork出worker进程
      -> ngx_worker_process_cycle() 每个worker的主循环
        -> while (!ngx_quit) {
             ngx_process_events_and_timers();  // 事件循环
           }

3.2 ngx_master_process_cycle()

// src/os/unix/ngx_process_cycle.c
void ngx_master_process_cycle(ngx_cycle_t *cycle) {
    // 1. 设置进程名
    ngx_setproctitle("master process");

    // 2. 创建监听socket
    ngx_open_listening_sockets(cycle);

    // 3. 启动worker进程
    ngx_start_worker_processes(cycle, ccf->worker_processes,
                               NGX_PROCESS_RESPAWN);

    // 4. 启动缓存管理进程(可选)
    ngx_start_cache_manager_processes(cycle, 0);

    // 5. 主循环:等待信号
    for ( ;; ) {
        // 检查定时器
        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                       "cycle:%V", &cycle->conf_file);

        // 等待信号
        sigsuspend(&set);

        // 更新时间
        ngx_time_update();

        // 处理信号
        if (ngx_quit) {
            ngx_signal_worker_processes(cycle, NGX_SHUTDOWN_SIGNAL);
            ngx_close_listening_sockets(cycle);
            ngx_close_old_listening_sockets(cycle);
            exit(0);
        }

        if (ngx_reopen) {
            ngx_reopen_files(cycle, ccf->user);
            ngx_reopen = 0;
        }

        if (ngx_restart) {
            ngx_restart_worker_processes(cycle);
            ngx_restart = 0;
        }
    }
}

3.3 Worker进程启动

static void ngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t n,
    ngx_int_t type) {
    ngx_int_t i;

    for (i = 0; i < n; i++) {
        ngx_spawn_process(cycle, ngx_worker_process_cycle, NULL,
                          "worker process", type);
    }
}

static void ngx_worker_process_cycle(ngx_cycle_t *cycle, char *me) {
    // 1. 初始化
    ngx_worker_process_init(cycle, me);

    // 2. 设置进程名
    ngx_setproctitle("worker process");

    // 3. 主事件循环
    for ( ;; ) {
        // 检查退出标志
        if (ngx_quit) {
            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0,
                          "shutting down");
            ngx_close_listening_sockets(cycle);
            ngx_close_idle_connections(cycle);
            break;
        }

        if (ngx_terminate) {
            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0,
                          "terminating");
            break;
        }

        if (ngx_quit) {
            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0,
                          "gracefully shutting down");
            ngx_close_listening_sockets(cycle);
            ngx_close_idle_connections(cycle);
            break;
        }

        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                       "worker cycle: %V", &cycle->conf_file);

        // 核心:事件处理
        ngx_process_events_and_timers(cycle);
    }

    // 4. 清理
    ngx_worker_process_exit(cycle);
}

3.4 Worker进程初始化

static void ngx_worker_process_init(ngx_cycle_t *cycle, ngx_int_t worker) {
    ngx_connection_t *c;
    ngx_int_t n;

    // 1. 设置用户权限
    if (setuid(ccf->user) == -1) {
        ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_errno,
                      "setuid() failed");
        ngx_abort();
    }

    // 2. 设置进程优先级
    if (ccf->priority && setpriority(PRIO_PROCESS, 0, ccf->priority) == -1) {
        ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_errno,
                      "setpriority() failed");
    }

    // 3. 设置资源限制
    if (ccf->rlimit_nofile != NGX_CONF_UNSET) {
        rl.rlim_cur = ccf->rlimit_nofile;
        rl.rlim_max = ccf->rlimit_nofile;
        setrlimit(RLIMIT_NOFILE, &rl);
    }

    // 4. 初始化事件处理
    ngx_event_process_init(cycle);

    // 5. 初始化日志
    ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0,
                  "start worker processes");

    // 6. 执行模块的init_process钩子
    for (n = 0; n < ngx_modules_n; n++) {
        if (ngx_modules[n]->init_process) {
            if (ngx_modules[n]->init_process(cycle) == NGX_ERROR) {
                ngx_abort();
            }
        }
    }
}

3.5 Master进程职责

职责 说明
读取配置 验证配置合法性
创建worker fork出N个worker进程
管理worker 监控worker健康状态
处理信号 接收reload/stop/reopen信号
热升级 替换二进制文件

3.6 Worker进程职责

职责 说明
事件循环 处理所有客户端请求
接受连接 从accept mutex竞争获取
执行模块 运行HTTP/Stream等模块逻辑
回收连接 关闭空闲连接

4. 信号处理

4.1 关键信号

信号 作用 处理者
SIGQUIT 优雅退出(等待请求完成) master
SIGTERM 强制停止 master
SIGHUP 重载配置 master
SIGUSR1 重新打开日志文件(日志轮转) master
SIGUSR2 热升级(exec新二进制) master
SIGCHLD 子进程退出 master
SIGALRM 定时器 worker

4.2 信号处理器注册

// src/os/unix/ngx_process.c
void ngx_init_signals(void) {
    struct sigaction sa;

    // SIGCHLD:非阻塞处理子进程退出
    sa.sa_handler = ngx_process_get_status;
    sa.sa_flags = SA_NOCLDSTOP|SA_NOCLDWR|SA_RESTART|SA_SIGINFO;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGCHLD, &sa, NULL);

    // SIGHUP:重载配置
    sa.sa_handler = ngx_signal_handler;
    sigaction(SIGHUP, &sa, NULL);

    // SIGUSR1:重新打开日志
    sa.sa_handler = ngx_signal_handler;
    sigaction(SIGUSR1, &sa, NULL);

    // SIGUSR2:热升级
    sa.sa_handler = ngx_signal_handler;
    sigaction(SIGUSR2, &sa, NULL);

    // SIGQUIT:优雅退出
    sa.sa_handler = ngx_signal_handler;
    sigaction(SIGQUIT, &sa, NULL);

    // SIGTERM:强制停止
    sa.sa_handler = ngx_signal_handler;
    sigaction(SIGTERM, &sa, NULL);
}

4.3 优雅退出流程

master收到SIGQUIT
  -> 设置 ngx_quit = 1
  -> 发送SIGQUIT给所有worker
    -> worker设置 ngx_quit = 1
      -> 不再接受新连接
      -> 等待现有连接处理完毕
      -> 进程退出

4.4 子进程退出处理

static void ngx_process_get_status(void) {
    int status;
    ngx_pid_t pid;
    ngx_int_t i;

    // 非阻塞等待子进程
    for ( ;; ) {
        pid = waitpid(-1, &status, WNOHANG);

        if (pid == 0) {
            return;  // 没有更多子进程退出
        }

        if (pid == -1) {
            if (errno == EINTR) {
                continue;
            }
            return;
        }

        // 记录退出状态
        if (WIFEXITED(status)) {
            ngx_log_error(NGX_LOG_INFO, cycle->log, 0,
                          "%P %d exited with code %d",
                          pid, ngx_errno, WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            ngx_log_error(NGX_LOG_INFO, cycle->log, 0,
                          "%P %d was killed by signal %d",
                          pid, ngx_errno, WTERMSIG(status));
        }

        // 重新启动worker进程
        ngx_spawn_process(cycle, ngx_worker_process_cycle, NULL,
                          "worker process", NGX_PROCESS_JUST_RESPAWN);
    }
}

5. 优雅退出

5.1 优雅退出流程

master收到SIGQUIT
  -> 设置 ngx_quit = 1
  -> 发送SIGQUIT给所有worker
    -> worker检测到 ngx_quit = 1
      -> ngx_close_listening_sockets(cycle)  关闭监听socket
      -> ngx_close_idle_connections(cycle)   关闭空闲连接
      -> 等待活跃连接处理完毕
      -> ngx_worker_process_exit(cycle)      进程退出

5.2 关闭空闲连接

void ngx_close_idle_connections(ngx_cycle_t *cycle) {
    ngx_connection_t *c;
    ngx_uint_t i;

    c = cycle->connections;

    for (i = 0; i < cycle->connection_n; i++) {
        // 如果连接空闲(没有活跃事件)
        if (c[i].fd != -1 && c[i].read->active == 0) {
            ngx_close_connection(&c[i]);
        }
    }
}

6. 内存管理最佳实践

1. 使用池分配器避免内存泄漏
2. 请求级别的分配使用请求池
3. 连接级别的分配使用连接池
4. 共享内存使用slab分配器
5. 避免在worker间直接共享堆内存
6. 使用ngx_pfree释放大块内存
7. 注册清理回调处理文件描述符等资源

7. 本篇小结

概念 要点
内存池 指针移动分配,池销毁时释放
小块分配 O(1),指针移动
大块分配 直接malloc,加入large链表
共享内存 slab分配器,锁保护
slab 4种页面类型,bitmap管理
进程模型 master-fork-worker
accept锁 避免惊群效应
信号 reload/stop/reopen/热升级
优雅退出 关闭监听socket,等待连接完成

思考题

  1. 内存池的最大块大小(max)为什么要设定上限?超过怎么办?
  2. 如果worker进程崩溃,NGINX如何恢复?
  3. 热升级过程中如何保证现有请求不中断?
  4. 为什么NGINX选择accept mutex而不是SO_REUSEPORT?

思考题解答

1. 内存池max上限为什么要设定?超过怎么办?

为什么设定上限

  1. 防止大块分配淹没小块:如果所有分配都走内存池,大块分配(如读取整个请求体)会占用大量连续内存,导致小块分配(如配置字符串)无法找到合适的连续空间。

  2. 减少碎片:大块分配直接走系统malloc,使用完后立即释放,避免在内存池中留下碎片。

  3. 快速释放:内存池销毁时,大块通过free链表快速释放,小块通过指针移动批量释放。如果大块也走池,释放成本增加。

超过max时的处理

// src/core/ngx_palloc.c
void *ngx_palloc(ngx_pool_t *pool, size_t size) {
    if (size <= pool->max) {
        // 小块:从当前pool数据块分配
        return ngx_palloc_small(pool, size, pool->clean);
    }
    // 大块:直接malloc,加入large链表
    return ngx_palloc_large(pool, size);
}

大块分配流程:

  1. 调用系统malloc分配内存
  2. 将分配的内存块加入 pool->large 链表
  3. 内存块使用完后,在 ngx_destroy_pool() 中释放

max的典型值:默认为页面大小(4096字节),可通过 pool->max 调整。

2. worker进程崩溃如何恢复?

自动恢复机制

worker进程崩溃
  -> master检测到子进程退出
    -> 记录日志
    -> 重新fork新的worker进程
    -> 新worker继承监听socket
    -> 继续处理请求

实现细节

// src/os/unix/ngx_process.c
void ngx_process_get_status(void) {
    pid = waitpid(-1, &status, WNOHANG);
    if (WIFEXITED(status)) {
        // worker正常退出
        ngx_log_error(..., "worker %d exited with code %d", pid, WEXITSTATUS(status));
    } else if (WIFSIGNALED(status)) {
        // worker被信号杀死(崩溃)
        ngx_log_error(..., "worker %d was killed by signal %d", pid, WTERMSIG(status));
    }
    // 重新启动worker
    ngx_start_worker_processes(cycle, ngx_process_slot, NGX_PROCESS_JUST_RESPAWN);
}

恢复策略

  1. master进程永不退出(除非手动停止)
  2. worker崩溃只影响该worker处理的连接,其他worker不受影响
  3. 新worker立即继承监听socket,可以接受新连接
  4. 旧worker的连接在超时后被清理

配置

worker_rlimit_nofile 65535;  # 增加文件描述符限制
worker_shutdown_timeout 10s;  # 优雅退出超时

3. 热升级如何保证请求不中断?

热升级流程

1. 下载新二进制文件到临时路径
2. master接收SIGUSR2信号
  -> fork新master(使用新二进制)
    -> 新master fork新worker
3. 旧worker继续处理现有请求
4. 旧worker优雅退出
  -> 等待现有连接处理完毕
  -> 退出
5. 新master完全接管

无缝切换的关键

  1. 监听socket继承:新master从旧master继承监听socket,新worker可以立即接受连接
  2. 旧连接不中断:旧worker继续处理已建立的连接,直到连接关闭或超时
  3. 配置文件共享:新master使用新二进制但可以加载旧配置文件
  4. 回滚机制:如果新版本有问题,可以发送 SIGQUIT 给新master,旧master仍可恢复

回滚步骤(如果新版本有问题):

# 向旧master发送信号恢复
kill -QUIT old_master_pid
# 旧master会重新启动worker

4. 为什么选择accept mutex而不是SO_REUSEPORT?

SO_REUSEPORT

  • 内核层面的负载均衡
  • 多个进程可以bind同一个端口
  • 内核自动分发连接到不同进程

NGINX选择accept mutex的原因

  1. 更细粒度的控制

    • 可以根据worker负载动态调整连接分配
    • 支持不同的调度策略(如权重)
  2. 兼容性

    • SO_REUSEPORT需要Linux 3.9+
    • accept mutex在所有平台都支持
    • NGINX需要支持FreeBSD、Linux、Windows等
  3. 惊群效应的处理

    • SO_REUSEPORT在某些内核版本仍有惊群问题
    • accept mutex完全避免惊群
  4. 代码复杂度

    • accept mutex实现简单,不需要额外的内核支持
    • SO_REUSEPORT需要处理内核版本差异

SO_REUSEPORT的优势(可选启用):

# NGINX 1.9.1+ 支持reuseport
listen 80 reuseport;

启用后,每个worker有独立的accept队列,内核直接分发连接,无需accept mutex,性能更好。

建议:Linux 3.9+环境推荐启用 reuseport,其他平台使用accept mutex。

posted @ 2026-09-04 17:26  IcarusLee  阅读(2)  评论(0)    收藏  举报