探讨OpenResty和Nginx的共享内存区使用物理内存资源(或 RAM)?

探讨OpenResty和Nginx的共享内存区使用物理内存资源(或 RAM)?

OpenResty 和 Nginx 服务器通常会配置共享内存区,用于储存在所有工作进程之间共享的数据。例如,Nginx 标准模块 ngx_http_limit_req 和 ngx_htt0 q J ; ( @p_limit_conn 使用共享内存区储存状态数据,以限制所有工作进程中的用户请求速率和用户请求的并发度。Opes V s o k 5 (nResty 的 ngx_lua 模块通过 lua_shared_dict,向用户 Lua 代码提供基于共享内存的数据字典存储。

本文通过几个简单和独立的$ ` D Z例子,探讨这些共享内存区如何使用物理内存资源(或 RAM)。我们还会探讨共享内存的使用率对系统层面的进程内存指标的影响,例如在 ps7 d H 2系统工具的结果中的 VSZRSS 等指标。

与本博客网站 中的几乎所有技术类文章类似,我们使用 OpenResty XRay 这款动态追踪产品对未经修改的 OpenResty 或 Nginx 服务器和应用的内部进行深度分析和可视化呈现。因为 OpenRk f J _esty XRay 是一个非侵入性的A q l E分析平Q ! 5 T u台,所以我们不需要对 OpenResty 或 Nginx 的目标进程做任何修改 --k ! 4 g R e z z 不需要代码注入,也不需要在目标进程中加载特殊插件或模块。这} 4 t样可以保证我们通过 OpenResty XRay 分析工具所看到的目标进程内部状态,与没有观察者时的状态是完全一致的。

我们将在多数示例中使用 nl % c k ] .g@ ? | m Kx_lua 模块的 lua_shared_dict,因为该模块可以使用自定义的 Lua 代码进行编程I 6 } X。我们在这些示例中展示的行为和问题,也同样适用于所有标准, O O B . x Nginx 模块和第三% ( y方模块中的其他共享内u ? q f (存区。

Slab 与内存页

Nginx 及其模块通常使用 Nginl x m u ]x 核心里的 s. h ` Q ulab 分; I = 7 S +配器 来管理共享内存区内的空间。这个slab 分配器专门用于m o ! o W @ 在固定大小的内存区内分配和释放较小的内存块。

在 slab 的基础P M v ! U之上,共享内存区会引入更V Q 7 B B n高层面的数据结构,例如红黑树和链表等O ) ? 3 0 e . = c等。

slab 可能小至几个字节,也可能大至跨越多个内存页。

操作系统以内存页为单位来管理进程的共享内存0 = % a _ )(或其他种类的内存)。
x86_64 Linux 系统中,默认的内存页大小通常是 4 KB,但具体大小取决于体系结N j . @ M R : 7构和 Linux 内核的配置。例如,某些 Aarch64 Linux 系统的内存页大小高达 64 KB。

我们M F D s g 1 a Q将会看到 OpenResty 和 Nginx 进程的共享内存区,分别在内存页层面和 slab 层面上的细节信i S O息。

分配的内存不一定有消耗

与硬盘这样的资源不同,物理内存(或 RAM)总是一种非常宝贵的资源。
大部分现代操作系统都实现了一种优化技术,叫做 按需分页(demand-paging),用于减少用户应用对 RAM 资源的压力。具体来说,就是当你分配大块的内存时,操作系统核心会将 RU + ` * ; ~AM 资源(或物理内存页)的z k ? o y t k k实际分配推迟到内存页里的数据被实际使用的时候。例如,如果用户进程分配了 10 个内存页,但却只使用了 3 个内存页,则操作系统可能只把这 3 个内存页映射到了 RAM 设备。这种行为同样适用于 Nginx 或 Os 5 @ 0 # e =penResty 应用中分配的0 ! 7 { w共享内存区。用户可以在 nginx.conf 文件中配置庞大的共享内存区,但他可能会注意到在服务器启动之后,几乎没有额外占用多少内存,毕竟通常在刚启动的时候,几乎没有共享内存页被实际] o 3 g使用到。

空的共享内存区

我们以下面这个 nginx.coI 9 Gnf 文件为例。该文件分配了一个空的共享内存区,并且从没有使用过它:

master_process on;
worker_processes 2;
events {
worker_connections 1024;
}
http {
lua_shared_dict dogs 100m;
server {
listen 8080;
location = /t {
return 200 "hello world\n";
}
}
}

我们* = D M y W J通过 lua_shared_dict 指令配置了一个 100 MB 的共享内存区,名为 dogs。并且我们为这个服务器配置了 2 个工作进程。请注意,我们在配置里从没有触及这个 dogs 区,所以这个区是空的。

可以通过下列命令启动这个服务器:

mkdir ~/work/
cd ~/work/
mkdir logs/ conf/
vim conf/nginx.conf  # p q ` @ Naste the nginx.conf sample above herN y : ue
/usr/local| z e k -/openresty/nginx/sbin/ngf 2 3inx -p $PWD/

然后用下列命令查看 nginx 进程是否已在运行:

$ p# V Fs aux|head -n1; ps aux|grepM ? 5 [ 9 , 8 nginx/ H t V ( U Y i
USER       PID %CPU %MEM    VSZ   RSS TTYd X % s e `      STAT START   TIME COMMAND
agentzh   9359  0.0  0.0 137508  1576 ?        Ss   09:10   0:00 nginx: master process /usr/local/openresty/nginx/sbin/nginx -p /home/agentzh/work/
agentzh   931 P c B =60  0.0  0.0 137968  1924 ?        S    09:10   0:00 nginx: worker process
agentzh   9361  0.0  0.0 137968  1920 ?        S    09:10   0:00 nginx: worker process

这两个工作进程占用的内存大小很接近。下面我们重点研究 PID 为 9360 的这个工作进程。在 OpenResty XRay 控制台的 Web 图形界面中,我们可以看到这个进程一共占用了 134.73 MB 的虚拟内存(virtual memory)和 1.88 MB 的常驻内存(V P g * #resident memory),这与上文中的 ps 命令输出的结果完全相同:

探讨OpenResty和Nginx的共享内存区使用物理内存资源(或 RAM)?

正如我们的另一篇文章 《OpenResty 和 Nginx 如何分配和管理内存》中所介绍的,我们最关心的就是常驻内存的使用量。常驻内存将硬件资源g P * L E ! $ h实际映射到相应的内存页(如 RAM 1)。Q 1 ] T M V v所以我们从图中看到,实际映射到硬件资源的内存量很少,总计只有 1.88MB。上文配置的 100 MB 的共享内存区在这个常驻内存当中只占很小的一* ~ * _ $ ~部分(详情请见后续的讨论)。

当然,共享内存区的这 100 MB 还是全部贡献到了该进程的虚拟内存总量中去了。操作系统会为这个共享内存区预留出虚拟内存的地址空间,不过,这只是一种簿记记录,此时并不占用任何的 RAM 资源或其他硬件资源。

不是 空无一物

我们可以通过该进程的“应用层面的内存使用量的分类明细”图,来检查空的共享内存区是否占用了常驻(或物理)内存。

探讨OpenResty和Nginx的共享内存区使用物理内存资源(或 RAM)?

有趣的是,我们在这个图中看到了一个非零的 Nginx Shm Loaded (已加载的 Nginx 共享内存)组分。这部分很小,只有 612 KB,但还是出现了。所以空的共享内存区也并非空无一物。这是因为 Nq ( & ` i ` g iginx 已经! x I S H M a在新初始化的共享内存区域中放置了一些元数据,用于簿记目的。这些元数据为 Nginx 的 slab 分配器所使用。

已加载和未加载内存页

我们可以通过 OpenResty XRay 自动生成n ( F * ) . } T Y的下列图表,查看共享内存区内; k ^ L ( - /被实际使用(或加载)的内存页数量。

探讨OpenResty和Nginx的共享内存区使用物理内存资源(或 RAM)?

我们发现在dogs区域中已经加载(或实际] 0 M 1 ; g O A +使用)的内存大小为 608 KB,p k } j ,同时有一个特殊的 ngx_accept_mutex_ptr 被 Nginx 核心自动分配用于 accep@ g Ot_mutex 功能。

这两部分内存的大小相加为 61U U 3 a2 KB# $ } o @ u $ F,正是上文的饼状图中显示的 Nginx Shm Loaded 的大小。

如前文所述,6 N xdogs 区使用的 608 KB 内存实际上是 slab 分配器 使用的元数据。

未加载的内存页只是被保留的虚拟内存地址空间,并没有被使用过。

关于进程的页表

我们没有提及的一种复杂性是,每一个 nginx 工作进程其实都有各自的页表。CP ^ YU 硬件8 { . [ B 8 2 {或操作系统内核, - 8正是通过查询这些页表来查找虚拟内存页所对应的存储。因此每个进程在不同共享内存区内可能有不同的已加载页集合,因为每个进j N 2 h P ! T &程在运行过程中可能访问过不同的内存页{ g = J ^ Y v集合。为了简化这里的分析,OpenRq Z s i 1 .esty XRay 会显示所有的为任意一个工作进程加载过的内存页,即使当前的目@ ( s M标工作进程从未碰触过这些内存页。也正因为这个原因,已加载内存页的总大小可d w b O E 3能(略微)高于目标进程的常驻内存的大小。

空闲的和已使用的 slab

如上文所述,Nginx 通常使用 slabs 而不是内存页来管理共享内存区内的空间。我们可以通过 OpenResty XRay 直接查看某一个共享内存区内已使用的和空Z j N * : C j / {闲的(或未使用的)slabs 的统计信息:

探讨OpenResty和Nginx的共享内存区使用物理内存资源(或 RAM)?

如我们所预期的,我们这个例子里的大部分 slabs 是空闲的未被使用的。注z [ c R 8意,这里的内存大o C )小的数字远小于上一节中所示的U { 8内存页层面的统计数字。这是因为 slabs 层面的抽象层次更高,并不包含 slab 分配器针对内存页的大小补齐和地址对齐的内存消耗。

我们可以通过OpenResty XRay进一步观察在这个 dogs 区域中各个 slab 的大小分布情况:

探讨OpenResty和Nginx的共享内存区使用物理内存资源(或 RAM)?

探讨OpenResty和Nginx的共享内存区使用物理内存资源(或 RAM)?

我们可以看到这个空的共享内存区里,仍然有 3 个已使用的 slab 和 157 个空闲的 slab。这些 slab 的总个数为:3 + 157 = 160个。请记住这个数字,我们会在下文中跟写入了一些用户数据的 dogs 区里的情况进行对比。

写入了用户数据的共享内* b m M ^存区

下面我们会修改之前的配置示例,在 N} S 4 ^ j , R ]ginx 服务器启动时主v 0 r # h Q * 1 B动写入一些数据。| n z : r具体做法是,我们在 nginx.conf 文件的 http {} 配置分程序块中增加下面这条 init_by_luai I $ 4_block 配Q ( m j z 9 | {置指令:

init_by_lua_block {
for i = 1, 300000 do
ngx.shared.dogs:set("key" .. i, i)
end
}

这里在服务器启动y A f V - 7 H I的时候,主动对 dogs 共享内存区进行了初Y ) *始化,写入了 300,000 个键~ L / + $ 1 %值对。

然后运行下列的L ~ E g J s| x Y k J Zhell 命令以重新启动服务器进程:

kily J K t k # )l -QUIT `cat logs/nginx.pid`
/usr/local/openresty/nginx/sbin/nginx -p $PWD/

新启动的 Nginx 进程如下所示:

$ ps aux|he } ; M x - |ead -n1; ps aux|grepq q f nginx
USER       PID %CPU %MEM    Vw Y O ` $ v !SZ   RSS TTY      STAT START   TIME COMMAND
agentzh  29733  0.0  0.0 137508  1420 ?        Ss   13:50   0:00 nginx: master process /usr/local/openresty/nginx/sbin/nginx -p /home/agentzh/work/
agentzh  29734 32.0  0.5 138544 41168 ?        S    13:50   0:00 nginx: worker process
agentzh  29735 32.0  0.5 138544 41044 ?        S    1` D *3:50   0:00 nginx: worker process

虚拟内存与常驻内存

针对 NgiB x K e ? ~ N -nx 工作进程 29735,OpenResty XRay 生成了下面这张饼图:

探讨OpenResty和Nginx的共享内存区使用物理内存资源(或 RAM)?

显然,常驻内存的大小远高于之前那个空的共享区的例子,而且在总的虚拟内存大小中所占的比例也更大(H B Z f = = %29.6%)。

W z w j #拟内存的使用量也略有增加(从 134.73 MB 增加到了 135.30 MB)。因为共享内存区本身的大小没有变化,所以共享P ) g 8 3 I 9 7 J内存区对于虚拟内存使用量的增加其l W H l F O t - O实并没有影响。这里略微增大的原因是我们通过 init_by_luaH ) o T C _block 指令新引入了一些 Lua 代码(这部分微小的内存也同时贡献到了常驻内存中去了)。

应用层面的内存使用量明细显示,Nginx 共享内存区域的已加载内存占用了最多常驻内存:

探讨OpenResty和Nginx的共享内存区使用物理内存资源(或 RAM)?

已加载和未加载内存页

现在在这个 dogs 共享内存区里,已加载的内存页多了很多,而未加载的内存页也有f ^ T D了相应的m L w % - B ] @显著_ b A 9 a }减少:

探讨OpenResty和Nginx的共享内存区使用物理内存资源(或 RAM)?

空的和已使用的 slab

现在 dogs 共享内存区增加了 300,000 个已使用的 sla? W 4b(除了空的共享内存区中那 3 个总是会预分配的 slab 以外):

探讨OpenResty和Nginx的共享内存区使用物理内存资源(或 RAM)?

显然2 Z + M,lua_shared_dict 区中的每一个键值对,其实都直接对} [ ^ {应一个 slab。

空闲 slab 的数量与先前在/ ` u _ @ 3 _ Y #空的共享u f *内存区中的数量是完全相同的,即 157 个 slab:

探讨OpenResty和Nginx的共享内存区使用物理内存资源(或 RAM)?

虚假的内存泄漏

正如我们上面所演示的,共享内存 I T 3 v D * v区在应用实际访问其内部的内存页之前,都不会实际耗费物理内存资源。因为这个原因,用户可能会观察到 Nginx 工作进程的常驻内存大小似乎会持续地增长,特别是在进程刚启动之后。这会让用户误以为存在内存泄漏。下面这张图展示了这样的一个例子:

探讨OpenResty和Nginx的共享内存区使用物理内存资源(或 RAM)?

通过查看 OpenReZ = H k s _ Psty XRay 生成的应用级别的内存使用明细图,我们可以清楚地看到( 7 Nginx 的共享内存区域其实占用了绝大部分的常驻内存空间:

探讨OpenResty和Nginx的共享内存区使用物理内存资源(或 RAM)?

这种内存增长是暂时的,会在共享内存区被填满时停止。但是当用户把共享内存区配置得特别大,大到超出当前系统中可用的物理内存的时候,仍然是有潜在风险的。正因为如此,我们应该注意观察如下所示的内存页级别的内存使用量的柱状图:

探讨OpenResty和Nginx的共享内存区使用物理内存资源(或 RAM)?

图中蓝色的部分可能最终会被进程用尽(即变为红色),而对当前系统产生冲击。

HUP 重新加载

Nginx 支持通过 HUP 信号来重新加载服务器的配置而不用退出它的 master 进程(workerQ Y E = 进程仍然会优雅退出并重启)。通常 Nginx 共享内存区会在 HUP 重新加载(HUP reload)之后自动继承原有的数据。所以原先R / 5 H P Z e p 为已访问过的共享内存页分配的那些物理内存页也会保留下来。于是想通过 HUP 重新加载来释放共享内存区内的常驻内存空间的尝试是会失败的。用户应Y O 7 ! C D改用 Nginx 的重启或二进制升级操作。

值得提醒的是,某一[ ) J w ? r ! `个 Nginx 模块还是s X ^有权k d m决定是否在 HUP 重新加载后保持原有的数据。所以可能会有例外。

结论

V Z ^ K们在上文中已经解释了 Nginx 的共享内存区所占用的物理内存资源,可能远少于 nginx.conf 文件中配置的大小。这要归功于现代操作系统中的按需分页特性。我们演示了空的共享内存区内依然会使用到一些内存页和 slab,以用于存储 slab 分配器本身需要的元数据。通过 OpenResty XRay 的高级分析器,我们j u R + *可以实时检查运行中的 nginx 工作进程,查看其中的共享内存区实际使用或加载的内存,包括内存页和 slab 这两个不同层面。

另一方面,按需分页的优化也会- 8 M产生内存在某段时间内持续增长的现象。这其实并不是内存泄漏,! 9 U但仍然具有一? 3 x & g Q y Q g定的风险。我们也解释了 NgL g l (inx 的 HUP 重新加载操作通常并不会清空共享内存区里已有的数据

推荐教程:nginx教程

以上就是探讨OpenResty和Nginx的共享内存区使用物理内存资源(或 RAM)?的详细内容。