乐趣区

关于openresty:OpenResty-和-Nginx-的共享内存区是如何消耗物理内存的

OpenResty 和 Nginx 服务器通常会配置共享内存区,用于贮存在所有工作过程之间共享的数据。例如,Nginx 规范模块 ngx_http_limit_req 和 ngx_http_limit_conn 应用共享内存区贮存状态数据,以限度所有工作过程中的用户申请速率和用户申请的并发度。OpenResty 的 ngx_lua 模块通过 lua_shared_dict,向用户 Lua 代码提供基于共享内存的数据字典存储。

本文通过几个简略和独立的例子,探讨这些共享内存区如何应用物理内存资源(或 RAM)。咱们还会探讨共享内存的使用率对系统层面的过程内存指标的影响,例如在 ps 等零碎工具的后果中的 VSZRSS 等指标。

与本博客网站 中的简直所有技术类文章相似,咱们应用 OpenResty XRay 这款动静追踪产品对未经批改的 OpenResty 或 Nginx 服务器和利用的外部进行深度剖析和可视化出现。因为 OpenResty XRay 是一个非侵入性的剖析平台,所以咱们不须要对 OpenResty 或 Nginx 的指标过程做任何批改 — 不须要代码注入,也不须要在指标过程中加载非凡插件或模块。这样能够保障咱们通过 OpenResty XRay 剖析工具所看到的指标过程外部状态,与没有观察者时的状态是完全一致的。

咱们将在少数示例中应用 ngx_lua 模块的 lua_shared_dict,因为该模块能够应用自定义的 Lua 代码进行编程。咱们在这些示例中展现的行为和问题,也同样实用于所有规范 Nginx 模块和第三方模块中的其余共享内存区。

Slab 与内存页

Nginx 及其模块通常应用 Nginx 外围里的 slab 分配器 来治理共享内存区内的空间。这个 slab 分配器专门用于在固定大小的内存区内调配和开释较小的内存块。

在 slab 的根底之上,共享内存区会引入更高层面的数据结构,例如红黑树和链表等等。

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

操作系统以内存页为单位来治理过程的共享内存(或其余品种的内存)。
x86_64 Linux 零碎中,默认的内存页大小通常是 4 KB,但具体大小取决于体系结构和 Linux 内核的配置。例如,某些 Aarch64 Linux 零碎的内存页大小高达 64 KB。

咱们将会看到 OpenResty 和 Nginx 过程的共享内存区,别离在内存页层面和 slab 层面上的细节信息。

调配的内存不肯定有耗费

与硬盘这样的资源不同,物理内存(或 RAM)总是一种十分贵重的资源。
大部分古代操作系统都实现了一种优化技术,叫做 按需分页(demand-paging),用于缩小用户利用对 RAM 资源的压力。具体来说,就是当你调配大块的内存时,操作系统外围会将 RAM 资源(或物理内存页)的理论调配推延到内存页里的数据被理论应用的时候。例如,如果用户过程调配了 10 个内存页,但却只应用了 3 个内存页,则操作系统可能只把这 3 个内存页映射到了 RAM 设施。这种行为同样实用于 Nginx 或 OpenResty 利用中调配的共享内存区。用户能够在 nginx.conf 文件中配置宏大的共享内存区,但他可能会留神到在服务器启动之后,简直没有额定占用多少内存,毕竟通常在刚启动的时候,简直没有共享内存页被理论应用到。

空的共享内存区

咱们以上面这个 nginx.conf 文件为例。该文件调配了一个空的共享内存区,并且从没有应用过它:

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";}
    }
}

咱们通过 lua_shared_dict 指令配置了一个 100 MB 的共享内存区,名为 dogs。并且咱们为这个服务器配置了 2 个工作过程。请留神,咱们在配置里从没有涉及这个 dogs 区,所以这个区是空的。

能够通过下列命令启动这个服务器:

mkdir ~/work/
cd ~/work/
mkdir logs/ conf/
vim conf/nginx.conf  # paste the nginx.conf sample above here
/usr/local/openresty/nginx/sbin/nginx -p $PWD/

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

$ ps aux|head -n1; ps aux|grep nginx
USER       PID %CPU %MEM    VSZ   RSS TTY      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   9360  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 的常驻内存(resident memory),这与上文中的 ps 命令输入的后果完全相同:

正如咱们的另一篇文章《OpenResty 和 Nginx 如何调配和治理内存》中所介绍的,咱们最关怀的就是常驻内存的使用量。常驻内存将硬件资源理论映射到相应的内存页(如 RAM 1)。所以咱们从图中看到,理论映射到硬件资源的内存量很少,总计只有 1.88MB。上文配置的 100 MB 的共享内存区在这个常驻内存当中只占很小的一部分(详情请见后续的探讨)。

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

不是 空无一物

咱们能够通过该过程的“利用层面的内存使用量的分类明细”图,来查看空的共享内存区是否占用了常驻(或物理)内存。

乏味的是,咱们在这个图中看到了一个非零的 Nginx Shm Loaded(已加载的 Nginx 共享内存)组分。这部分很小,只有 612 KB,但还是呈现了。所以空的共享内存区也并非空无一物。这是因为 Nginx 曾经在新初始化的共享内存区域中搁置了一些元数据,用于簿记目标。这些元数据为 Nginx 的 slab 分配器所应用。

已加载和未加载内存页

咱们能够通过 OpenResty XRay 主动生成的下列图表,查看共享内存区内被理论应用(或加载)的内存页数量。

咱们发现在 dogs 区域中曾经加载(或理论应用)的内存大小为 608 KB,同时有一个非凡的 ngx_accept_mutex_ptr 被 Nginx 外围主动调配用于 accept_mutex 性能。

这两局部内存的大小相加为 612 KB,正是上文的饼状图中显示的 Nginx Shm Loaded 的大小。

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

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

对于过程的页表

咱们没有提及的一种复杂性是,每一个 nginx 工作过程其实都有各自的页表。CPU 硬件或操作系统内核正是通过查问这些页表来查找虚拟内存页所对应的存储。因而每个过程在不同共享内存区内可能有不同的 已加载页 汇合,因为每个过程在运行过程中可能拜访过不同的内存页汇合。为了简化这里的剖析,OpenResty XRay 会显示所有的为 任意一个 工作过程加载过的内存页,即便以后的指标工作过程从未碰触过这些内存页。也正因为这个起因,已加载内存页的总大小可能(稍微)高于指标过程的常驻内存的大小。

闲暇的和已应用的 slab

如上文所述,Nginx 通常应用 slabs 而不是内存页来治理共享内存区内的空间。咱们能够通过 OpenResty XRay 间接查看某一个共享内存区内已应用的和闲暇的(或未应用的)slabs 的统计信息:

如咱们所预期的,咱们这个例子里的大部分 slabs 是 闲暇的 未被应用的。留神,这里的内存大小的数字远小于上一节中所示的内存页层面的统计数字。这是因为 slabs 层面的抽象层次更高,并不蕴含 slab 分配器针对内存页的大小补齐和地址对齐的内存耗费。

咱们能够通过 OpenResty XRay 进一步察看在这个 dogs 区域中各个 slab 的大小散布状况:

咱们能够看到这个空的共享内存区里,依然有 3 个已应用的 slab 和 157 个闲暇的 slab。这些 slab 的总个数为:3 + 157 = 160 个。请记住这个数字,咱们会在下文中跟写入了一些用户数据的 dogs 区里的状况进行比照。

写入了用户数据的共享内存区

上面咱们会批改之前的配置示例,在 Nginx 服务器启动时被动写入一些数据。具体做法是,咱们在 nginx.conf 文件的 http {} 配置分程序块中减少上面这条 init_by_lua_block 配置指令:

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

这里在服务器启动的时候,被动对 dogs 共享内存区进行了初始化,写入了 300,000 个键值对。

而后运行下列的 shell 命令以重新启动服务器过程:

kill -QUIT `cat logs/nginx.pid`
/usr/local/openresty/nginx/sbin/nginx -p $PWD/

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

$ ps aux|head -n1; ps aux|grep nginx
USER       PID %CPU %MEM    VSZ   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    13:50   0:00 nginx: worker process

虚拟内存与常驻内存

针对 Nginx 工作过程 29735,OpenResty XRay 生成了上面这张饼图:

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

虚拟内存的使用量也略有减少(从 134.73 MB 减少到了 135.30 MB)。因为共享内存区自身的大小没有变动,所以共享内存区对于虚拟内存使用量的减少其实并没有影响。这里稍微增大的起因是咱们通过 init_by_lua_block 指令新引入了一些 Lua 代码(这部分渺小的内存也同时奉献到了常驻内存中去了)。

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

已加载和未加载内存页

当初在这个 dogs 共享内存区里,已加载的内存页多了很多,而未加载的内存页也有了相应的显著缩小:

空的和已应用的 slab

当初 dogs 共享内存区减少了 300,000 个已应用的 slab(除了空的共享内存区中那 3 个总是会预调配的 slab 以外):

显然,lua_shared_dict 区中的每一个键值对,其实都间接对应一个 slab。

闲暇 slab 的数量与先前在空的共享内存区中的数量是完全相同的,即 157 个 slab:

虚伪的内存透露

正如咱们下面所演示的,共享内存区在利用理论拜访其外部的内存页之前,都不会理论消耗物理内存资源。因为这个起因,用户可能会察看到 Nginx 工作过程的常驻内存大小仿佛会继续地增长,特地是在过程刚启动之后。这会让用户误以为存在内存透露。上面这张图展现了这样的一个例子:

通过查看 OpenResty XRay 生成的利用级别的内存应用明细图,咱们能够分明地看到 Nginx 的共享内存区域其实占用了绝大部分的常驻内存空间:

这种内存增长是临时的,会在共享内存区被填满时进行。然而当用户把共享内存区配置得特地大,大到超出以后零碎中可用的物理内存的时候,依然是有潜在危险的。正因为如此,咱们应该留神察看如下所示的内存页级别的内存使用量的柱状图:

![Loaded and unloaded memory pages in shared memory zones](/img/bVbK7iK
“Loaded and unloaded memory pages in shared memory zones”)

图中蓝色的局部可能最终会被过程用尽(即变为红色),而对以后零碎产生冲击。

HUP 从新加载

Nginx 反对通过 HUP 信号来从新加载服务器的配置而不必退出它的 master 过程(worker 过程依然会优雅退出并重启)。通常 Nginx 共享内存区会在 HUP 从新加载(HUP reload)之后主动继承原有的数据。所以原先为已拜访过的共享内存页调配的那些物理内存页也会保留下来。于是想通过 HUP 从新加载来开释共享内存区内的常驻内存空间的尝试是会失败的。用户应改用 Nginx 的重启或二进制降级操作。

值得揭示的是,某一个 Nginx 模块还是有权决定是否在 HUP 从新加载后放弃原有的数据。所以可能会有例外。

论断

咱们在上文中曾经解释了 Nginx 的共享内存区所占用的物理内存资源,可能远少于 nginx.conf 文件中配置的大小。这要归功于古代操作系统中的按需分页个性。咱们演示了空的共享内存区内仍然会应用到一些内存页和 slab,以用于存储 slab 分配器自身须要的元数据。通过 OpenResty XRay 的高级分析器,咱们能够实时查看运行中的 nginx 工作过程,查看其中的共享内存区理论应用或加载的内存,包含内存页和 slab 这两个不同层面。

另一方面,按需分页的优化也会产生内存在某段时间内持续增长的景象。这其实并不是内存透露,但依然具备肯定的危险。咱们也解释了 Nginx 的 HUP 从新加载操作通常并不会清空共享内存区里已有的数据。

咱们将在本博客网站后续的文章中,持续探讨共享内存区中应用的高级数据结构,例如红黑树和队列,以及如何剖析和缓解共享内存区内的内存碎片的问题。

对于作者

章亦春是开源我的项目 OpenResty® 的创始人,同时也是 OpenResty Inc. 公司的创始人和 CEO。他奉献了许多 Nginx 的第三方模块,相当多 Nginx 和 LuaJIT 外围补丁,并且设计了 OpenResty XRay 等产品。

关注咱们

如果您感觉本文有价值,十分欢送关注咱们 OpenResty Inc. 公司的博客网站。也欢送扫码关注咱们的微信公众号:

翻译

咱们提供了英文版原文和中译版(本文)。咱们也欢送读者提供其余语言的翻译版本,只有是全文翻译不带省略,咱们都将会思考采纳,非常感谢!


  1. 当产生替换(swapping)时,一些常驻内存会被保留和映射到硬盘设施下来。↩
退出移动版