10. Nginx 模块开发:Reload 时复用 Zone 数据
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 时:
- 新 Master 进程解析新配置,调用
ngx_shared_memory_add添加 Zone。 - Nginx 发现 Zone 名称与旧 Zone 相同,会复用
ngx_shm_zone_t结构体,但重新映射共享内存。 - 调用
init_zone回调时,data参数指向旧 Zone 的上下文(ngx_http_zone_clone_ctx_t)。 - 在
init_zone中,我们可以通过data指针遍历旧 Zone 的红黑树,将每个节点插入到新 Zone 中。 - 旧 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。核心思路是:
- 在
init_zone回调中,通过data参数访问旧 Zone 的上下文。 - 遍历旧 Zone 的红黑树,将每个节点插入到新 Zone 中。
- 新 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
更多推荐




所有评论(0)