大家好,我是 WeiyiGeek,一名深耕安全运维开发(SecOpsDev)领域的技术从业者,致力于探索DevOps与安全的融合(DevSecOps),自动化运维工具开发与实践,企业网络安全防护,欢迎各位道友一起学习交流、一起进步。
Nginx 连接池与内存池
Nginx 连接池
本小节,将讲述Nginx 连接池的结构与配置,包括连接与事件的对应关系、内存消耗计算、事件结构体成员及其作用,以及内置变量在日志记录中的应用。
如下面的 Nginx 连接池结构,每个?Worker?进程拥有一个独立的?ngx_cycle_t?结构,结构中包含三个主要数组:connections(连接池)、read_events、write_events。
weiyigeek.top-Nginx 连接池结构
其三个数组大小缺省值多大呢,可以参考官方官网中 Core functionality 核心功能查看到 。
原文链接:?https://articles.zsxq.com/id_wxgg5glpimm9.html
connections 数组设置连接池大小,默认为512,其不仅用于客户端连接,也用于上游服务器连接,做反向代理时,每个客户端连接消耗两个connection,所以在并发调优时肯定要考虑内存情况并变更此参数大小,512 确实太小,一个对外的门户网站,通常有成千上万个连接,但 worker_connections 越大,初始化时预分配内存越多。
Syntax: worker_connections number;
Default: worker_connections 512;
Context: events
events 数组,每个连接对应一个读事件和一个写事件,分配的 read_events 和 write_events 数组大小与worker_connections 参数是一致,并通过序号对应连接与事件,例如,第五个连接对应第五个读事件和第五个写事件。
events {
? ? use kqueue; ?# 或者 epoll?
? ? worker_connections 2048;
}
其连接与事件的数据结构体以及占用字节情况如下:
ngx_connection_s 连接结构体:在64位系统中,每个结构体占用约232字节(不同NGX版本可能略有差异)。
ngx_event_s 事件结构体:每个?ngx_connection?对应着读事件和写事件,每个事件结构体占用约 96 字节,所以读、写合计占用192字节。
即:每个连接消耗内存为?232 + 96 × 2 = 424?字节,所以在进行 worker_connections 参数调优时 ,还需注意每个连接及对应事件的内存消耗。
weiyigeek.top-核心数据结构
再来分别关注一下 ngx_event_s 、ngx_connection_s结构体中的一些成员。
例如: ngx_event_s 结构体 成员
# handle 回调方法用于事件处理,第三方模块可自定义实现。
ngx_event_handler_pt handler;
# 基于红黑树(RB Tree)实现, 用于处理读写超时,每个事件的读写事件中都有一个timer节点。
ngx_rbtree_node_t timer;
# queue 队列存放多个请求事件
ngx_queue_t queue;
读写事件中其定时器是可通过模块配置超时时间,例如:client_header_timeout?默认为60秒, 表示在读取请求头时添加60秒的超时定时器。
Syntax: client_header_timeout time;
Default: client_header_timeout 60s;
Context: http, server
例如: ngx_connection_s 结构体 成员
# 分别指向读写事件,引向 ngx_event_s 结构体
ngx_event_t *read;
ngx_event_t *write;
# 抽象的操作系统底层接收与发送方法。
ngx_recv_pt ?recv;
ngx_send_pt ?send;
# 表示该连接已发送的字节数,类型为off_t(可理解为无符号整型), 可通内置变量 bytes_sent 获取到此值,常用于 access_logs 文件中进行调用。
off_t sent;
# 连接内存池
ngx_pool_t *pool;
在 nginx.conf 文件 log_format 指令中定义的 access.log 日志文件内容格式,在其中调用了内置变量 bytes_sent ?获取向客户端发送的字节数。
log_format ?main ?'$remote_addr - $remote_user [$time_local] "$request" '
? ? ? ? ? ? ? ? ??'$status $body_bytes_sent "$http_referer" '
? ? ? ? ? ? ? ? ??'"$http_user_agent" "$http_x_forwarded_for"';
Nginx 内存池
Nginx 内存池在请求场景中,包括连接内存池与请求内存池的使用区别、默认大小及其对内存碎片和性能的影响。内存池的重要性不言而喻,此外 Nginx 第三方模块在C语言开发中无需手动释放内存,都是由内存池自动管理。
由前面的 ngx_connection_s 结构体中的?ngx_pool_t *pool?定义的内存池成员变量,该其中连接内存池大小可通过配置项 connection_pool_size 定义。
内存池设计目的减少内存碎片, 使用 next 指针链式管理索引。提前分配内存,小块内存通过 malloc 大块内存通过操作系统(OS)分配。适用场景优势NGINX处理HTTP请求具有长连接(keep alive)特性,一个TCP连接可承载多个HTTP请求,为连接分配一次内存即可重复使用,连接关闭时统一释放。
内存分配策略,每个HTTP请求开始时分配内存,默认大小为4KB。因URL和Header通常占用较多空间,故需较大内存,连接内存池与请求内存池的区别如下:连接内存池用于整个TCP连接生命周期,连接关闭后释放。
- 
- 连接内存池: 其大小默认值为256或512字节,具体取决于操作系统。超出该大小仍可继续分配,仅影响内存分配次数。
 
 
# 允许精确调整每个连接的内存分配。此指令对性能的影响最小,通常不应使用。
# 默认情况下,该大小在32位平台上等于256字节,在64位平台上等于512字节。
Syntax: connection_pool_size size;
Default: connection_pool_size 256|512;
Context: http, server
- 
- 请求内存池:用于单次HTTP请求,请求结束后即可销毁,其大小默认值为4KB,比连接内存池大8倍。原因为请求需保存更多上下文信息如URL、Header等。
 
 
# 允许精确调整每个请求的内存分配,注意:此值通常不需要进行调整。
Syntax: request_pool_size size;
Default: request_pool_size 4k;
Context: http, server
weiyigeek.top-内存池
综上所述,性能调优建议在极端场景配置调整若URL非常大,可考虑增大请求内存池大小。若URL短小且数量少,可减小请求内存池大小,以降低内存消耗,提升并发能力。
最后,值得注意的是若本应在请求内存池分配的内存误用连接内存池,将导致内存延迟释放,增加Nginx内存占用,开发时建议明确区分请求与连接内存池的使用场景,避免不当分配。
Nginx 共享内存
在之前的文章中,我们讲解到 Nginx 进程间的通讯方式信号,此小节将深入学习进程间的数据同步方式共享内存、Slab内存管理器,以及使用锁的机制防止同时操作同一块内存。
什么是共享内存? 描述: 共享内存是一块被多个worker进程同时访问的内存区域,例如,10MB大小,用于实现数据同步。
共享内存使用的两个核心问题:
- 一是竞争与加锁机制:多个worker进程同时操作共享内存会引发竞争问题,因此需要加锁。
 
早期使用基于信号量(早期Linux比较久远的进程同步方式)的锁,导致进程休眠和主动切换。目前主要使用自旋锁,即在锁未释放时持续尝试获取,不进入休眠。
在使用自旋锁时需特别注意,所有使用共享内存的模块必须快速获取并释放锁。若第三方模块未遵循该规则,可能导致死锁或性能下降。
- 二是内存分配管理的复杂性:共享内存通常被多个对象同时使用,手动分配和管理非常繁琐。
 
为了解决此问题,我们将引入slab内存管理器来解决该问题。
Nginx 中呢些模块使用共享内存呢?
在官方模块中共享内存的典型用途,是限速、流控等场景必须使用共享内存,否则无法跨worker同步状态。例如:limit_connection、limit_request、SSL?、反向代理等模块。
共享内存常用的数据结构:
红黑树(rbtree): 插入和删除效率高,适用于需要快速操作的场景。例如,限速容器中插入或删除客户端。
单链表: 用于将共享元素串起来,例如 stream upstream zone module 等模块。
weiyigeek.top-共享内存使用者
另外,Ngx_http_lua_api 是 OpenResty 的核心模块,在使用 lua_shared_dict 指令出现后,将会分配一块共享内存实现键值对存储。例如?lua_shared_dict dogs 10M?表示 分配10MB共享内存,命名为dogs,之后我们可在配置中 或者 Lua 代码中进行读写操作。
# 基础限流:每个IP每秒30个请求
limit_req_zone?$http_x_forwarded_for?zone=base_limit:10m rate=30r/m;
# lua 申请共享内存
lua_shared_dict dogs 10M
例如,OpenResty使用共享内存代码示例,同时使用了 rbtree 以及 Linked list,其中红黑树用于存储每个键值对(如 key=count,value=8),而链表则用于实现LRU淘汰策略,即当共享内存达到10MB上限时,采用LRU策略淘汰最早未使用的节点。
http {
? ?lua_shared_dict site 10m;
? ?server {
? ? ?listen ? ? ? 80;
? ? ?server_name ?test.weiyigeek.top;
? ? ?location /set?{
? ? ? ? content_by_lua_block {
? ? ? ? ??local?site = ngx.shared.site
? ? ? ? ? site:set("blog","https://weiyigeek.top")
? ? ? ? ? ngx.say("STORED")
? ? ? ? }
? ? ?}
? ? ?location /get {
? ? ? ?content_by_lua_block{?
? ? ? ??local?site = ngx.shared.site
? ? ? ? ngx.say(site:get("blog"))
? ? ? ?}
? ? ?}
? }
}
weiyigeek.top-OpenResty使用共享内存代码示例
总结:共享内存是Nginx实现跨worker进程通信的最有效手段,在需要多个worker进程共享状态的场景(如集群流控)中,必须使用共享内存,而非各自独立的内存空间。
slab 内存管理
上小节讲到,不同的 Nginx 工作进程间需要共享信息时,只能通过共享内存实现,共享内存上可使用链表、红黑树等数据结构,但每个节点都需要分配内存。
那如何使用slab内存分配器将一整块共享内存切割成小块,供红黑树节点使用呢?
slab 内存管理的实现方式:将整块的共享内存被划分为多个页面,每个页面为4KB,每个页面进一步被切分为多个slot,例如:32字节、64字节、128字节、256字节等,可知slot的大小以指数方式增长。假如,此时需分配51字节的内存,将被分配到64字节的slot中。
优缺点:
- 优点:避免小对象,避免碎片、避免重复初始化,提高效率。缺点:分配方式存在浪费的情况,最多浪费可达2倍。
 
weiyigeek.top-slab 内存管理原理图
slab 内存当前主要应用在 OpenResty 以及?limit_connection?和?limit_request?限速策略中。
如何针对 slab 内存做一个监控管理呢?
在 淘宝开源的 tengine 中有一个名为 slab_state (https://tengine.taobao.org/document/ngx_slab_stat.html) 模块用于监控不同slot的使用情况,亦可查看每个slot的大小、已分配数量、使用数量、访问请求次数及失败次数等信息。
由于 slab_state 模块在页面上没提供下载连接,所以我们需下载 Tengine-2.4.1.tar.gz 版本的源代码包解压使用,各位运友可选择 Nginx 或者 OpenResty 进行源码编译安装测试。由于本系列主要讲解的 Nginx ,所以就使用 Nginx 源码进行编译安装 slab_state 模块,配置反向代理缓存,最后查看slab内存分配情况。
cd?/usr/local/src
# 下载源代码
wget https://tengine.taobao.org/download/tengine-2.4.1.tar.gz
tar -zxvf tengine-2.4.1.tar.gz
# 查看模块
./tengine-2.4.1/modules/ngx_slab_stat/
config ?ngx_http_slab_stat_module.c ?README.cn ?README.md ?slab_stat.patch ?t
# 编译构建
wget http://nginx.org/download/nginx-1.28.0.tar.gz
tar -zxvf nginx-1.28.0.tar.gz
cd?/usr/local/src/nginx-1.28.0
./configure --prefix=/usr/local/nginx --user=nginx --group=nginx --sbin-path=/usr/sbin/nginx --conf-path=/usr/local/nginx/conf/nginx.conf --pid-path=/usr/local/nginx/nginx.pid --error-log-path=/var/log/nginx/logs/error.log --http-log-path=/var/log/nginx/logs/access.log --lock-path=/var/run/nginx.lock --modules-path=/usr/local/nginx/modules --with-http_stub_status_module --add-module=../tengine-2.4.1/modules/ngx_slab_stat/
make && make install?
# 编辑配置文件
$vim?/usr/local/nginx/conf/nginx.conf
...
http {
? ? include ? ? ? mime.types;
? ? default_type ?application/octet-stream;
? ??# 以反向代理 grafana 为例
? ? upstream backend {
? ? ? zone http_backend 64k;
? ? ? server 10.20.172.213:3000 weight=5;
? ? }
? ??# 反向代理缓存设置 10m
? ? proxy_cache_path cache_backend keys_zone=cache_backend:10m;
? ? server {
? ? ?listen ? ? ? 80;
? ? ?server_name ?localhost;
? ? ?# 反向代理
? ? ?location / {
? ? ? ?proxy_pass ?http://backend;
? ? ? ?proxy_cache cache_backend;
? ? ?}
? ? ?# 配置 slab_stat 指令
? ? ?location /slab_status {
? ? ? ?slab_stat;
? ? ? ?#stub_status;
? ? ?}
? ? }
}
# 验证配置 & 重启服务
nginx -t
nginx -s reload
浏览器访问?http://10.20.172.214/slab_status?可看到 http_backend 共享内存大小为我们设置的 zone?http_backend 64k,而 cache_backend 共享内存大小则设置的?keys_zone=cache_backend:10m?,其 slot 分配使用情况如下图所示:
weiyigeek.top-slab_status模块调用结果
实际上 Nginx 中原生?ngx_http_status_module?模块支持查看 slab 内存分配使用情况, 只不过是在商业版中才有,开源版本中只能自行引用Tengine中ngx_slab_sta模块源代码进行编译,文档直达:https://nginx.org/en/docs/http/ngx_http_status_module.html
weiyigeek.top-http_status模块
总结:slab内存管理使用best-fit思想,是Linux操作系统常用的内存分配方式,Nginx 在 使用共享内存时,通常需要通过slab分配器分配内存,再由上层数据结构进行对象维护。
								
								
								
916