Nginx server级别动态加载方案
背景
nginx配置加载,既全量配置加载,主要过程如下
Nginx master收到reload信号
Nginx master全量加载配置
Nginx master fork 新work 处理新请求
Nginx master 给老worker发送shutting down消息通知其推出
shutting down worker不会处理新连接,不同场景下shutting down worker退出时机不同
http 1.1连接 :
对于已处理完请求,直接关闭连接
对于正在处理请求,处理完当前请求退出
http2连接:
reload时向client发送goaway消息,告知客户端不再接受新请求,处理完所有当前请求后退出
websocket连接 -- 对shutting down影响最大
nginx不感知websocket协议,tcp连接不断,shutting down worker不能退出
全量配置加载存在的问题
影响系统稳定性。
reload产生大量shutting down work,不能及时退出,占用大量内存,影响系统稳定性
生效慢
5000个server,需要5s。
不同规则的变更互相影响
server1的变更影响server2的长连接
负载均衡调度不准
业界解决方案调研
*
方案 | 实现原理 | 存在问题 |
nginx方案 | 通过设置shutting down状态进程存活时间解决 | 没有解决加载慢的问题,依然会fork进程影响系统稳定性
超过存活时间后,用户连接会被强制断开,影响用户体验 |
nginx+lua方案 | nginx配置不变,域名+url相关配置交由lua管理,避免reload
|
功能限制: 无法支持server级别配置(端口等)的增量加载
后续开发维护成本高 nginx模块众多,配置指令更多,需要通过lua实现现有指令;使用新指令都需要进行开发 |
动态upstream方案
|
增量配置加载,不fork进程:
1.master收到增量reload信号 2.master首先增量加载配置 3.然后通知worker增量加载 |
只支持upstream级别配置的增量加载
不支持server、location级别配置增量加载,无法满足云上使用场景
|
nginx全量配置加载实现
worker_processes 1;
http {
upstream test {
server 127.0.0.1:80;
}
server {
listen 80;
server_name localhost;
location /proxy {
proxy_pass http://test;
}
}
}
配置加载: nginx全量配置加载由ngx_init_cycle()函数实现,主要流程如下:
更新时间、申请内存池
初始化pathes数组、open_files链表、shared_memory链表、listening数组等
调用core模块的create_conf()生成core模块配置
通过递归调用ngx_conf_parse完成配置解析
http配置块处理主要流程
创建http配置块的ngx_http_conf_ctx_t并初始化
递归调用ngx_conf_parse 完成所有server、upstream配置块的配置加载
调用ngx_http_optimize_servers完成server相关配置优化,用于加速查找
优化后,根据listen配置生成指定ip+port下的server配置的hash表
调用core模块的init_conf(),进行core模块的配置初始化
遍历open_files链表中的每一个文件并打开
创建共享内存并初始化
遍历cycle->listening数组并监听(old_cycle中没有监听的)
调用所有模块的init_module
释放残留在old_cycle中的资源: 释放多余的共享内存, 关闭多余的侦听sockets,关闭多余的打开文件等
加载后nginx配置管理结构简化版如下
方案设计
方案难点
基于改造nginx实现server级别配置的增量加载,面临的难点
nginx配置管理实现复杂
4级指针,错综复杂的数据结构关联
nginx配置贯穿请求的整个生命周期,改动影响大、风险高
nginx配置加载大量使用动态数组,不利于实现修改和删除
nginx基于内存池实现的内存管理,不利于实现删除
需要支持的配置加载场景多
server级别、location级别配置的增删改
用户对nginx指令需求多,后续会有持续需求
需要支持server、location级别下任意配置的增量加载
实现场景分析
需要实现场景:除了upstream级别配置变更,还有server级别配置的增删改、location级别的增删改,6种场景
首先简化实现场景,降低方案复杂度
将server、location级别配置变更归类为server级别变更
只需要实现新增server、删除server两种场景
location级别的增删改归类为server级别配置修改,通过新增和删除server实现
再单独梳理http配置块下的server配置加载+配置生效流程, 主要分为下面几步
配置加载: 加载server配置块到nginx动态数组
配置优化: 根据listen配置生成指定ip+port下的server配置的hash表
配置生效: 监听端口+ fork worker
配置查找: worker根据ip+port+domain查找server
根据上诉分析,提出我们的解决方案: 复用+改造nginx配置加载逻辑,实现server配置的增量新增、删除、修改。
新增server
以新增serverx为例,serverx中listen的是ip2的81端口,主要加载流程如下:
配置加载阶段:将serverx配置增量加载到内存,添加到动态数组中
配置优化阶段:更新ip2:81端口下的hash表,关联serverx
这样在配置查找阶段,新的连接就可以查找到serverx,达到新增server的目的
删除server
首先看下实现删除server面临的挑战
Nginx基于内存池实现内存管理
大块申请,避免频繁申请产生内存碎片
内存池释放时统一释放内存,简化实现,避免内存泄露
内存中删除规则需要改造nginx内存管理实现,影响大
Nginx配置管理大量使用动态数组,配置删除改动大
需要移动数组
还涉及大量相关数据结构的改动
难以实现内存中删除,是否可以不删除?
基于不从内存中删除的思路,提出了删除server的解决方案,以删除servern为例,servern listen的是ip2的81端口:
首先在配置加载阶段, 根据server_id在动态数组中查找到servern, 将其置为无效
然后配置优化阶段,会忽略掉无效的server,保证iP2 81端口下的hash表中不包含servern
这样在配置查找阶段,通过查找不到servern,达到删除server的目的。
内存不删除影响
内存占用情况,平均单次加载消耗内存20k,影响非常小
通过增加内存监控,超过阈值通过reload彻底释放内存
修改server:
通过新增+删除实现,已修改serverx为例
配置加载阶段:将serverx_old置位无效,serverx_new配置增量加载到内存,添加到动态数组中
配置优化阶段:更新ip2:81端口下的hash表,忽略掉无效的serverx_old,关联serverx_new
这样在配置查找阶段,新的连接就可以查找到serverx_new,达到新增serverx的目的
agent、master和worker配置一致性
主要流程如下
nginx master收到agent发来dysvr_reload信号
master先执行dysvr_reload流程,如果失败,回写配置文件通知agent使用正常reload方式
master reload成功,则向所有worker发送dysvr_reload信号,通知worker执行相同的dysvr_reload流程,如果失败,通知agent使用正常reload方式
所有worker执行成功后,master再通知agent reload成功
方案的优势和收益
优势
简化了实现场景,降低实现复杂度,以少量的内存占用换取系统的稳定。
通过新增和删除实现server级别配置的增量加载
复用nginx现有配置加载逻辑,可支持server、upstream块下任意nginx配置的增量加载,极大降低后续维护成本
收益
避免fork进程,提高系统稳定性
提升用户体验
7层规则配置加载从5000ms->200ms(5000个server)
用户间规则变更隔离
长连接不断
负载均衡调度准确