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 config4. 代码实现4.1 数据结构定义// ngx_http_zone_clone_module.h#ifndef_NGX_HTTP_ZONE_CLONE_H_INCLUDED_#define_NGX_HTTP_ZONE_CLONE_H_INCLUDED_#includengx_config.h#includengx_core.h#includengx_http.h// Zone 中的槽位节点typedefstruct{ngx_str_tkey;ngx_uint_tvalue;}ngx_http_zone_clone_node_t;// Zone 上下文typedefstruct{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;// 模块配置typedefstruct{ngx_shm_zone_t*shm_zone;ngx_str_tzone_name;}ngx_http_zone_clone_loc_conf_t;#endif4.2 模块主文件// ngx_http_zone_clone_module.c#includengx_config.h#includengx_core.h#includengx_http.h#includengx_http_zone_clone_module.hstaticchar*ngx_http_zone_clone(ngx_conf_t*cf,ngx_command_t*cmd,void*conf);staticngx_int_tngx_http_zone_clone_handler(ngx_http_request_t*r);staticngx_int_tngx_http_zone_clone_init_zone(ngx_shm_zone_t*shm_zone,void*data);staticvoidngx_http_zone_clone_rbtree_insert_value(ngx_rbtree_node_t*temp,ngx_rbtree_node_t*node,ngx_rbtree_node_t*sentinel);staticngx_http_zone_clone_node_t*ngx_http_zone_clone_lookup(ngx_http_zone_clone_ctx_t*ctx,ngx_str_t*key,uint32_thash);// 配置指令staticngx_command_tngx_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 模块上下文staticngx_http_module_tngx_http_zone_clone_module_ctx{NULL,// preconfigurationNULL,// postconfigurationNULL,// create main configurationNULL,// init main configurationNULL,// create server configurationNULL,// merge server configurationngx_http_zone_clone_create_loc_conf,// create location configurationngx_http_zone_clone_merge_loc_conf// merge location configuration};ngx_module_tngx_http_zone_clone_module{NGX_MODULE_V1,ngx_http_zone_clone_module_ctx,// module contextngx_http_zone_clone_commands,// module directivesNGX_HTTP_MODULE,// module typeNULL,// init masterNULL,// init moduleNULL,// init processNULL,// init threadNULL,// exit threadNULL,// exit processNULL,// exit masterNGX_MODULE_V1_PADDING};4.3 Zone 初始化与数据直接复制核心// Zone 初始化回调——核心逻辑staticngx_int_tngx_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_tkey;uint32_thash;ctxshm_zone-data;// 初始化新的共享内存 Zoneshm_zone-initngx_http_zone_clone_init_zone;ctx-shm_zoneshm_zone;ctx-shpool(ngx_slab_pool_t*)shm_zone-shm.addr;// 初始化新 Zone 的红黑树ctx-rbtreengx_slab_alloc(ctx-shpool,sizeof(ngx_rbtree_t));if(ctx-rbtreeNULL){returnNGX_ERROR;}ctx-sentinelngx_slab_alloc(ctx-shpool,sizeof(ngx_rbtree_node_t));if(ctx-sentinelNULL){returnNGX_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_tif(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 的红黑树将每个节点复制到新 Zonenodengx_rbtree_min(octx-rbtree-root,octx-rbtree-sentinel);sentineloctx-rbtree-sentinel;while(node!sentinel){oznode(ngx_http_zone_clone_node_t*)node-color;// 准备 keykey.dataoznode-key.data;key.lenoznode-key.len;hashngx_hash_key(key.data,key.len);// 在新 Zone 中创建节点并复制数据new_znodengx_http_zone_clone_lookup(ctx,key,hash);if(new_znode){new_znode-valueoznode-value;}nodengx_rbtree_next(octx-rbtree,node);}ngx_log_error(NGX_LOG_INFO,ngx_cycle-log,0,zone_clone: data cloning completed);}returnNGX_OK;}4.4 数据查找与插入// 红黑树插入函数staticvoidngx_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(*psentinel){break;}temp*p;}*pnode;node-parenttemp;node-leftsentinel;node-rightsentinel;ngx_rbt_red(node);}// 查找或创建 Zone 中的节点staticngx_http_zone_clone_node_t*ngx_http_zone_clone_lookup(ngx_http_zone_clone_ctx_t*ctx,ngx_str_t*key,uint32_thash){ngx_int_trc;ngx_rbtree_node_t*node,*sentinel;ngx_http_zone_clone_node_t*znode;ngx_shmtx_lock(ctx-shpool-mutex);nodectx-rbtree-root;sentinelctx-rbtree-sentinel;while(node!sentinel){znode(ngx_http_zone_clone_node_t*)node-color;rcngx_memn2cmp(key-data,znode-key.data,key-len,znode-key.len);if(rc0){ngx_shmtx_unlock(ctx-shpool-mutex);returnznode;}node(rc0)?node-left:node-right;}// 未找到创建新节点znodengx_slab_alloc_locked(ctx-shpool,sizeof(ngx_http_zone_clone_node_t));if(znodeNULL){ngx_shmtx_unlock(ctx-shpool-mutex);returnNULL;}znode-key.datangx_slab_alloc_locked(ctx-shpool,key-len);if(znode-key.dataNULL){ngx_slab_free_locked(ctx-shpool,znode);ngx_shmtx_unlock(ctx-shpool-mutex);returnNULL;}ngx_memcpy(znode-key.data,key-data,key-len);znode-key.lenkey-len;znode-value0;node(ngx_rbtree_node_t*)znode;node-keyhash;ngx_rbtree_insert(ctx-rbtree,node);ngx_shmtx_unlock(ctx-shpool-mutex);returnznode;}4.5 配置指令处理函数// zone_clone 配置指令处理staticchar*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_tsize;valuecf-args-elts;// 解析 Zone 名称和大小namevalue[1];sizengx_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]);returnNGX_CONF_ERROR;}// 分配 Zone 上下文ctxngx_pcalloc(cf-pool,sizeof(ngx_http_zone_clone_ctx_t));if(ctxNULL){returnNGX_CONF_ERROR;}// 创建共享内存 Zoneshm_zonengx_shared_memory_add(cf,name,size,ngx_http_zone_clone_module);if(shm_zoneNULL){returnNGX_CONF_ERROR;}shm_zone-initngx_http_zone_clone_init_zone;shm_zone-datactx;ctx-shm_zoneshm_zone;returnNGX_CONF_OK;}// 创建 location 配置staticvoid*ngx_http_zone_clone_create_loc_conf(ngx_conf_t*cf){ngx_http_zone_clone_loc_conf_t*conf;confngx_pcalloc(cf-pool,sizeof(ngx_http_zone_clone_loc_conf_t));if(confNULL){returnNULL;}conf-zone_name.len0;conf-zone_name.dataNULL;returnconf;}// 合并 location 配置staticchar*ngx_http_zone_clone_merge_loc_conf(ngx_conf_t*cf,void*parent,void*child){ngx_http_zone_clone_loc_conf_t*prevparent;ngx_http_zone_clone_loc_conf_t*confchild;ngx_conf_merge_str_value(conf-zone_name,prev-zone_name,);returnNGX_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 数据持久化# 启动 Nginxnginx-c/path/to/nginx.conf# 模拟请求设置一些数据curlhttp://localhost/set?keytest1val100curlhttp://localhost/set?keytest2val200# 验证数据curlhttp://localhost/get?keytest1# 输出: Value for test1: 100# 执行 reloadnginx-sreload# 再次请求验证数据是否保留curlhttp://localhost/get?keytest1# 应输出: 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_modulengx_addon_namengx_http_zone_clone_module# 如果作为静态模块编译需要指定源文件iftest-n$ngx_module_link;thenngx_module_typeHTTPngx_module_namengx_http_zone_clone_modulengx_module_srcs$ngx_addon_dir/ngx_http_zone_clone_module.c# 如果有额外的头文件路径可以在这里添加# ngx_module_incs$ngx_addon_dir# 如果有额外的链接库可以在这里添加# ngx_module_libs-lfoo.auto/moduleelse# 作为动态模块编译--add-dynamic-moduleNGX_ADDON_SRCS$NGX_ADDON_SRCS$ngx_addon_dir/ngx_http_zone_clone_module.cfi编译说明静态编译./configure --add-module/path/to/ngx_http_zone_clonemake-j$(nproc)makeinstall动态编译./configure --add-dynamic-module/path/to/ngx_http_zone_clonemake-j$(nproc)makeinstall然后在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