OpenResty 和 Nginx 伺服器通常會配置共享記憶體區,用於儲存在所有工作進程之間共享的資料。例如,Nginx 標準模組 ngx_http_limit_req 和 ngx_http_limit_conn 使用共享記憶體區儲存狀態數據,以限制所有工作進程中的使用者請求速率和使用者請求的並發度。 OpenResty 的 ngx_lua 模組透過 lua_shared_dict,提供使用者 Lua 程式碼基於共享記憶體的資料字典儲存。
本文透過幾個簡單且獨立的例子,探討這些共享記憶體區如何使用實體記憶體資源(或 RAM)。我們也會探討共享記憶體的使用率對系統層面的進程記憶體指標的影響,例如在ps
等系統工具的結果中的VSZ
和RSS
等指標。
與本部落格網站 中的幾乎所有技術類文章類似,我們使用OpenResty XRay 這款動態追蹤產品對未經修改的OpenResty 或Nginx 伺服器和應用的內部進行深度分析和視覺化呈現。因為OpenResty XRay 是一個非侵入性的分析平台,所以我們不需要對OpenResty 或Nginx 的目標進程做任何修改-- 不需要程式碼注入,也不需要在目標進程中載入特殊外掛程式或模組。這樣可以保證我們透過 OpenResty XRay 分析工具所看到的目標進程內部狀態,與沒有觀察者時的狀態是完全一致的。
我們將在多數範例中使用 ngx_lua 模組的 lua_shared_dict,因為該模組可以使用自訂的 Lua 程式碼進行程式設計。我們在這些範例中所展示的行為和問題,也同樣適用於所有標準 Nginx 模組和第三方模組中的其他共享記憶體區。
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 的共享内存区域其实占用了绝大部分的常驻内存空间:
这种内存增长是暂时的,会在共享内存区被填满时停止。但是当用户把共享内存区配置得特别大,大到超出当前系统中可用的物理内存的时候,仍然是有潜在风险的。正因为如此,我们应该注意观察如下所示的内存页级别的内存使用量的柱状图:
图中蓝色的部分可能最终会被进程用尽(即变为红色),而对当前系统产生冲击。
Nginx 支持通过 HUP 信号来重新加载服务器的配置而不用退出它的 master 进程(worker 进程仍然会优雅退出并重启)。通常 Nginx 共享内存区会在 HUP 重新加载(HUP reload)之后自动继承原有的数据。所以原先为已访问过的共享内存页分配的那些物理内存页也会保留下来。于是想通过 HUP 重新加载来释放共享内存区内的常驻内存空间的尝试是会失败的。用户应改用 Nginx 的重启或二进制升级操作。
值得提醒的是,某一个 Nginx 模块还是有权决定是否在 HUP 重新加载后保持原有的数据。所以可能会有例外。
我们在上文中已经解释了 Nginx 的共享内存区所占用的物理内存资源,可能远少于 nginx.conf
文件中配置的大小。这要归功于现代操作系统中的按需分页特性。我们演示了空的共享内存区内依然会使用到一些内存页和 slab,以用于存储 slab 分配器本身需要的元数据。通过 OpenResty XRay 的高级分析器,我们可以实时检查运行中的 nginx 工作进程,查看其中的共享内存区实际使用或加载的内存,包括内存页和 slab 这两个不同层面。
另一方面,按需分页的优化也会产生内存在某段时间内持续增长的现象。这其实并不是内存泄漏,但仍然具有一定的风险。我们也解释了 Nginx 的 HUP 重新加载操作通常并不会清空共享内存区里已有的数据
推荐教程:nginx教程
以上是探討OpenResty和Nginx的共享記憶體區使用實體記憶體資源(或 RAM)?的詳細內容。更多資訊請關注PHP中文網其他相關文章!