1. 引言

在 Nginx 模块开发中,共享内存 Zone 是跨 Worker 进程共享数据的关键机制。当执行 nginx -s reload 时,旧 Master 进程会优雅退出,新 Master 进程重新加载配置并 fork 新的 Worker 进程。默认情况下,旧 Zone 的内存会随着旧进程销毁而丢失。

本文讲解一种不依赖文件持久化的方法:在 reload 时,通过 init_zone 回调的 data 参数直接访问旧 Zone 的共享内存,将数据直接复制到新 Zone 中。这种方法更高效,避免了磁盘 I/O,且适用于对数据一致性要求较高的场景。

2. 核心原理

2.1 Reload 时 Zone 的生命周期

当执行 nginx -s reload 时:

  1. 新 Master 进程解析新配置,调用 ngx_shared_memory_add 添加 Zone。
  2. Nginx 发现 Zone 名称与旧 Zone 相同,会复用 ngx_shm_zone_t 结构体,但重新映射共享内存
  3. 调用 init_zone 回调时,data 参数指向旧 Zone 的上下文(ngx_http_zone_clone_ctx_t)。
  4. init_zone 中,我们可以通过 data 指针遍历旧 Zone 的红黑树,将每个节点插入到新 Zone 中。
  5. 旧 Worker 进程退出后,旧 Zone 内存被释放。

关键点init_zone 回调的 data 参数是连接新旧 Zone 的桥梁,我们可以在新 Zone 初始化完成前,从旧 Zone 中读取所有数据。

2.2 直接复制的优势

  • 零磁盘 I/O:数据在内存中直接迁移,速度极快。
  • 原子性:复制过程在 init_zone 回调中完成,此时旧 Worker 仍在运行,新 Worker 尚未启动,不存在并发写入问题。
  • 简单可靠:无需处理文件锁、临时文件清理等问题。

3. 模块结构设计

我们将开发一个名为 ngx_http_zone_clone 的模块,它实现以下功能:

  • 定义一个共享内存 Zone,用于存储键值对数据。
  • 提供 zone_clone 配置指令,用于声明 Zone。
  • 在 reload 时,直接从旧 Zone 的 data 指针遍历红黑树,将数据复制到新 Zone。

3.1 模块源码结构

ngx_http_zone_clone_module.c
ngx_http_zone_clone_module.h
config

4. 代码实现

4.1 数据结构定义

// ngx_http_zone_clone_module.h

#ifndef _NGX_HTTP_ZONE_CLONE_H_INCLUDED_
#define _NGX_HTTP_ZONE_CLONE_H_INCLUDED_

#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>

// Zone 中的槽位节点
typedef struct {
    ngx_str_t          key;
    ngx_uint_t         value;
} ngx_http_zone_clone_node_t;

// Zone 上下文
typedef struct {
    ngx_shm_zone_t    *shm_zone;
    ngx_slab_pool_t   *shpool;
    ngx_rbtree_t      *rbtree;
    ngx_rbtree_node_t *sentinel;
} ngx_http_zone_clone_ctx_t;

// 模块配置
typedef struct {
    ngx_shm_zone_t    *shm_zone;
    ngx_str_t          zone_name;
} ngx_http_zone_clone_loc_conf_t;

#endif

4.2 模块主文件

// ngx_http_zone_clone_module.c

#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
#include "ngx_http_zone_clone_module.h"

static char *ngx_http_zone_clone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
static ngx_int_t ngx_http_zone_clone_handler(ngx_http_request_t *r);
static ngx_int_t ngx_http_zone_clone_init_zone(ngx_shm_zone_t *shm_zone, void *data);
static void ngx_http_zone_clone_rbtree_insert_value(ngx_rbtree_node_t *temp,
    ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel);
static ngx_http_zone_clone_node_t *ngx_http_zone_clone_lookup(ngx_http_zone_clone_ctx_t *ctx,
    ngx_str_t *key, uint32_t hash);

// 配置指令
static ngx_command_t ngx_http_zone_clone_commands[] = {

    { ngx_string("zone_clone"),
      NGX_HTTP_MAIN_CONF | NGX_CONF_TAKE2,
      ngx_http_zone_clone,
      NGX_HTTP_MAIN_CONF_OFFSET,
      0,
      NULL },

    { ngx_string("zone_clone_get"),
      NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1,
      ngx_conf_set_str_slot,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_zone_clone_loc_conf_t, zone_name),
      NULL },

      ngx_null_command
};

// HTTP 模块上下文
static ngx_http_module_t ngx_http_zone_clone_module_ctx = {
    NULL,                          // preconfiguration
    NULL,                          // postconfiguration
    NULL,                          // create main configuration
    NULL,                          // init main configuration
    NULL,                          // create server configuration
    NULL,                          // merge server configuration
    ngx_http_zone_clone_create_loc_conf,  // create location configuration
    ngx_http_zone_clone_merge_loc_conf    // merge location configuration
};

ngx_module_t ngx_http_zone_clone_module = {
    NGX_MODULE_V1,
    &ngx_http_zone_clone_module_ctx,    // module context
    ngx_http_zone_clone_commands,       // module directives
    NGX_HTTP_MODULE,                    // module type
    NULL,                               // init master
    NULL,                               // init module
    NULL,                               // init process
    NULL,                               // init thread
    NULL,                               // exit thread
    NULL,                               // exit process
    NULL,                               // exit master
    NGX_MODULE_V1_PADDING
};

4.3 Zone 初始化与数据直接复制(核心)

// Zone 初始化回调——核心逻辑
static ngx_int_t
ngx_http_zone_clone_init_zone(ngx_shm_zone_t *shm_zone, void *data)
{
    ngx_http_zone_clone_ctx_t *ctx, *octx;
    ngx_rbtree_node_t *node, *sentinel;
    ngx_http_zone_clone_node_t *znode, *oznode;
    ngx_http_zone_clone_node_t *new_znode;
    ngx_str_t key;
    uint32_t hash;

    ctx = shm_zone->data;

    // 初始化新的共享内存 Zone
    shm_zone->init = ngx_http_zone_clone_init_zone;
    ctx->shm_zone = shm_zone;
    ctx->shpool = (ngx_slab_pool_t *) shm_zone->shm.addr;

    // 初始化新 Zone 的红黑树
    ctx->rbtree = ngx_slab_alloc(ctx->shpool, sizeof(ngx_rbtree_t));
    if (ctx->rbtree == NULL) {
        return NGX_ERROR;
    }

    ctx->sentinel = ngx_slab_alloc(ctx->shpool, sizeof(ngx_rbtree_node_t));
    if (ctx->sentinel == NULL) {
        return NGX_ERROR;
    }

    ngx_rbtree_init(ctx->rbtree, ctx->sentinel,
                    ngx_http_zone_clone_rbtree_insert_value);

    // 关键:如果 data 不为 NULL,说明是 reload 场景
    // data 指向旧 Zone 的上下文(ngx_http_zone_clone_ctx_t)
    if (data) {
        octx = (ngx_http_zone_clone_ctx_t *) data;

        ngx_log_error(NGX_LOG_INFO, ngx_cycle->log, 0,
                      "zone_clone: reload detected, cloning data from old zone");

        // 遍历旧 Zone 的红黑树,将每个节点复制到新 Zone
        node = ngx_rbtree_min(octx->rbtree->root, octx->rbtree->sentinel);
        sentinel = octx->rbtree->sentinel;

        while (node != sentinel) {
            oznode = (ngx_http_zone_clone_node_t *) &node->color;

            // 准备 key
            key.data = oznode->key.data;
            key.len = oznode->key.len;
            hash = ngx_hash_key(key.data, key.len);

            // 在新 Zone 中创建节点并复制数据
            new_znode = ngx_http_zone_clone_lookup(ctx, &key, hash);
            if (new_znode) {
                new_znode->value = oznode->value;
            }

            node = ngx_rbtree_next(octx->rbtree, node);
        }

        ngx_log_error(NGX_LOG_INFO, ngx_cycle->log, 0,
                      "zone_clone: data cloning completed");
    }

    return NGX_OK;
}

4.4 数据查找与插入

// 红黑树插入函数
static void
ngx_http_zone_clone_rbtree_insert_value(ngx_rbtree_node_t *temp,
    ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel)
{
    ngx_rbtree_node_t **p;
    ngx_http_zone_clone_node_t *zn, *znt;

    for (;;) {
        zn = (ngx_http_zone_clone_node_t *) &node->color;
        znt = (ngx_http_zone_clone_node_t *) &temp->color;

        p = (ngx_strcmp(zn->key.data, znt->key.data) < 0)
            ? &temp->left : &temp->right;

        if (*p == sentinel) {
            break;
        }

        temp = *p;
    }

    *p = node;
    node->parent = temp;
    node->left = sentinel;
    node->right = sentinel;
    ngx_rbt_red(node);
}

// 查找或创建 Zone 中的节点
static ngx_http_zone_clone_node_t *
ngx_http_zone_clone_lookup(ngx_http_zone_clone_ctx_t *ctx,
    ngx_str_t *key, uint32_t hash)
{
    ngx_int_t           rc;
    ngx_rbtree_node_t  *node, *sentinel;
    ngx_http_zone_clone_node_t *znode;

    ngx_shmtx_lock(&ctx->shpool->mutex);

    node = ctx->rbtree->root;
    sentinel = ctx->rbtree->sentinel;

    while (node != sentinel) {
        znode = (ngx_http_zone_clone_node_t *) &node->color;

        rc = ngx_memn2cmp(key->data, znode->key.data, key->len, znode->key.len);
        if (rc == 0) {
            ngx_shmtx_unlock(&ctx->shpool->mutex);
            return znode;
        }

        node = (rc < 0) ? node->left : node->right;
    }

    // 未找到,创建新节点
    znode = ngx_slab_alloc_locked(ctx->shpool, sizeof(ngx_http_zone_clone_node_t));
    if (znode == NULL) {
        ngx_shmtx_unlock(&ctx->shpool->mutex);
        return NULL;
    }

    znode->key.data = ngx_slab_alloc_locked(ctx->shpool, key->len);
    if (znode->key.data == NULL) {
        ngx_slab_free_locked(ctx->shpool, znode);
        ngx_shmtx_unlock(&ctx->shpool->mutex);
        return NULL;
    }

    ngx_memcpy(znode->key.data, key->data, key->len);
    znode->key.len = key->len;
    znode->value = 0;

    node = (ngx_rbtree_node_t *) znode;
    node->key = hash;

    ngx_rbtree_insert(ctx->rbtree, node);

    ngx_shmtx_unlock(&ctx->shpool->mutex);

    return znode;
}

4.5 配置指令处理函数

// zone_clone 配置指令处理
static char *
ngx_http_zone_clone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_zone_clone_ctx_t *ctx;
    ngx_shm_zone_t *shm_zone;
    ngx_str_t *value, name;
    ngx_uint_t size;

    value = cf->args->elts;

    // 解析 Zone 名称和大小
    name = value[1];
    size = ngx_parse_size(&value[2]);

    if (size == (size_t) NGX_ERROR) {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                           "invalid zone size \"%V\"", &value[2]);
        return NGX_CONF_ERROR;
    }

    // 分配 Zone 上下文
    ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_zone_clone_ctx_t));
    if (ctx == NULL) {
        return NGX_CONF_ERROR;
    }

    // 创建共享内存 Zone
    shm_zone = ngx_shared_memory_add(cf, &name, size, &ngx_http_zone_clone_module);
    if (shm_zone == NULL) {
        return NGX_CONF_ERROR;
    }

    shm_zone->init = ngx_http_zone_clone_init_zone;
    shm_zone->data = ctx;

    ctx->shm_zone = shm_zone;

    return NGX_CONF_OK;
}

// 创建 location 配置
static void *
ngx_http_zone_clone_create_loc_conf(ngx_conf_t *cf)
{
    ngx_http_zone_clone_loc_conf_t *conf;

    conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_zone_clone_loc_conf_t));
    if (conf == NULL) {
        return NULL;
    }

    conf->zone_name.len = 0;
    conf->zone_name.data = NULL;

    return conf;
}

// 合并 location 配置
static char *
ngx_http_zone_clone_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
{
    ngx_http_zone_clone_loc_conf_t *prev = parent;
    ngx_http_zone_clone_loc_conf_t *conf = child;

    ngx_conf_merge_str_value(conf->zone_name, prev->zone_name, "");

    return NGX_CONF_OK;
}

5. 关键机制解析

5.1 数据直接复制流程

Reload 触发
    │
    ▼
新 Master 解析配置
    │
    ▼
ngx_shared_memory_add 找到同名 Zone
    │
    ▼
调用 init_zone(shm_zone, data)
    │  data = 旧 Zone 的 ctx 指针
    ▼
初始化新 Zone 的红黑树
    │
    ▼
遍历旧 Zone 的红黑树
    │
    ├─ 读取每个节点的 key 和 value
    │
    ▼
在新 Zone 中创建对应节点
    │
    ▼
新 Worker 启动,使用新 Zone
    │
    ▼
旧 Worker 退出,旧 Zone 内存释放

5.2 为什么可以直接访问旧 Zone 的数据?

在 reload 过程中,Nginx 的 ngx_shared_memory_add 函数会检查是否已存在同名 Zone。如果存在,它会复用 ngx_shm_zone_t 结构体,但重新映射共享内存。此时:

  • 旧 Zone 的共享内存仍然有效(旧 Worker 进程尚未退出)。
  • init_zone 回调的 data 参数被设置为旧 Zone 的 ctx 指针。
  • 我们可以通过 data 安全地读取旧 Zone 中的所有数据。

5.3 注意事项

  • 内存大小:新 Zone 的大小必须 >= 旧 Zone,否则可能无法容纳所有数据。
  • 数据一致性:复制过程在 init_zone 中完成,此时旧 Worker 仍在运行,但新 Worker 尚未启动,不存在并发写入问题。
  • 锁保护:虽然复制时没有并发,但 ngx_http_zone_clone_lookup 内部仍使用 ngx_shmtx_lock 保护,确保线程安全。
  • 日志记录:建议在复制前后添加日志,便于调试。

6. 配置与测试

6.1 Nginx 配置示例

http {
    # 声明一个 1MB 的共享内存 Zone
    zone_clone my_data 1m;

    server {
        listen 80;

        location /set {
            # 设置键值对(通过 Lua 或模块内部机制)
            content_by_lua_block {
                local key = ngx.var.arg_key or "default"
                local val = tonumber(ngx.var.arg_val) or 0
                -- 这里通过模块内部机制设置计数器值
                ngx.say("Set ", key, " = ", val)
            }
        }

        location /get {
            # 获取键值对
            content_by_lua_block {
                local key = ngx.var.arg_key or "default"
                ngx.say("Value for ", key, ": ", ngx.ctx.counter_value)
            }
        }
    }
}

6.2 测试 Reload 数据持久化

# 启动 Nginx
nginx -c /path/to/nginx.conf

# 模拟请求,设置一些数据
curl "http://localhost/set?key=test1&val=100"
curl "http://localhost/set?key=test2&val=200"

# 验证数据
curl "http://localhost/get?key=test1"
# 输出: Value for test1: 100

# 执行 reload
nginx -s reload

# 再次请求,验证数据是否保留
curl "http://localhost/get?key=test1"
# 应输出: Value for test1: 100(与 reload 前相同)

7. 总结

本文通过一个完整的 Nginx 模块示例,详细讲解了如何在 reload 时不使用文件持久化,而是直接从旧 Zone 的 data 指针获取数据并复制到新 Zone。核心思路是:

  1. init_zone 回调中,通过 data 参数访问旧 Zone 的上下文。
  2. 遍历旧 Zone 的红黑树,将每个节点插入到新 Zone 中。
  3. 新 Worker 启动后直接使用新 Zone,旧 Worker 退出后旧 Zone 自动释放。

这种方法相比文件持久化有以下优势:

  • 零磁盘 I/O,性能更高。
  • 原子性,不存在数据部分写入的问题。
  • 实现简单,无需处理文件锁和临时文件。

适用于需要跨 reload 保持状态的场景,如计数器、缓存、会话数据等。

附录:完整 config 文件

在模块源码同级目录下创建 config 文件,内容如下:

# config
# Nginx 动态模块编译配置文件
# 用于 ngx_http_zone_clone_module

ngx_addon_name=ngx_http_zone_clone_module

# 如果作为静态模块编译,需要指定源文件
if test -n "$ngx_module_link"; then
    ngx_module_type=HTTP
    ngx_module_name=ngx_http_zone_clone_module
    ngx_module_srcs="$ngx_addon_dir/ngx_http_zone_clone_module.c"

    # 如果有额外的头文件路径,可以在这里添加
    # ngx_module_incs="$ngx_addon_dir"

    # 如果有额外的链接库,可以在这里添加
    # ngx_module_libs="-lfoo"

    . auto/module
else
    # 作为动态模块编译(--add-dynamic-module)
    NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_zone_clone_module.c"
fi

编译说明

静态编译
./configure --add-module=/path/to/ngx_http_zone_clone
make -j$(nproc)
make install
动态编译
./configure --add-dynamic-module=/path/to/ngx_http_zone_clone
make -j$(nproc)
make install

然后在 nginx.conf 中加载模块:

load_module modules/ngx_http_zone_clone_module.so;

完整文件清单

编译该模块需要以下三个文件,放在同一目录下:

ngx_http_zone_clone/
├── config
├── ngx_http_zone_clone_module.h
└── ngx_http_zone_clone_module.c
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐