探討OpenResty和Nginx的共享內存區使用物理內存資源(或 RAM)?

探討OpenResty和Nginx的共享內存區使用物理內存資源(或 RAM)?

OpenRestynginx 服務器通常會配置共享內存區,用于儲存在所有工作進程之間共享的數據。例如,Nginx 標準模塊 OpenRestyOpenResty 使用共享內存區儲存狀態數據,以限制所有工作進程中的用戶請求速率和用戶請求的并發度。openrestyOpenResty 模塊通過 OpenResty,向用戶 lua 代碼提供基于共享內存的數據字典存儲。

本文通過幾個簡單和獨立的例子,探討這些共享內存區如何使用物理內存資源(或 RAM)。我們還會探討共享內存的使用率對系統層面的進程內存指標的影響,例如在 ps 等系統工具的結果中的 VSZ 和 RSS 等指標。

與本OpenResty 中的幾乎所有技術類文章類似,我們使用 OpenResty 這款OpenResty產品對未經修改的 OpenResty 或 Nginx 服務器和應用的內部進行深度分析和可視化呈現。因為 OpenResty 是一個非侵入性的分析平臺,所以我們不需要對 OpenResty 或 Nginx 的目標進程做任何修改 — 不需要代碼注入,也不需要在目標進程中加載特殊插件或模塊。這樣可以保證我們通過 OpenResty 分析工具所看到的目標進程內部狀態,與沒有觀察者時的狀態是完全一致的。

我們將在多數示例中使用 OpenResty 模塊的 OpenResty,因為該模塊可以使用自定義的 Lua 代碼進行編程。我們在這些示例中展示的行為和問題,也同樣適用于所有標準 Nginx 模塊和第三方模塊中的其他共享內存區。

Slab 與內存頁

Nginx 及其模塊通常使用 Nginx 核心里的 slab 分配器 來管理共享內存區內的空間。這個slab 分配器專門用于在固定大小的內存區內分配和釋放較小的內存塊。

在 slab 的基礎之上,共享內存區會引入更高層面的數據結構,例如紅黑樹和鏈表等等。

slab 可能小至幾個字節,也可能大至跨越多個內存頁。

操作系統以內存頁為單位來管理進程的共享內存(或其他種類的內存)。
在 x86_64 linux 系統中,默認的內存頁大小通常是 4 KB,但具體大小取決于體系結構和 Linux 內核的配置。例如,某些 Aarch64 Linux 系統的內存頁大小高達 64 KB。

我們將會看到 OpenResty 和 Nginx 進程的共享內存區,分別在內存頁層面和 slab 層面上的細節信息。

分配的內存不一定有消耗

與硬盤這樣的資源不同,物理內存(或 RAM)總是一種非常寶貴的資源。
大部分現代操作系統都實現了一種優化技術,叫做 OpenResty(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?worldn"; ????????} ????} }

我們通過 OpenResty 指令配置了一個 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 控制臺的 Web 圖形界面中,我們可以看到這個進程一共占用了 134.73 MB 的虛擬內存(virtual memory)和 1.88 MB 的常駐內存(resident memory),這與上文中的 ps 命令輸出的結果完全相同:

探討OpenResty和Nginx的共享內存區使用物理內存資源(或 RAM)?

正如我們的另一篇文章 OpenResty中所介紹的,我們最關心的就是常駐內存的使用量。常駐內存將硬件資源實際映射到相應的內存頁(如 RAM ?OpenResty)。所以我們從圖中看到,實際映射到硬件資源的內存量很少,總計只有 1.88MB。上文配置的 100 MB 的共享內存區在這個常駐內存當中只占很小的一部分(詳情請見后續的討論)。

當然,共享內存區的這 100 MB 還是全部貢獻到了該進程的虛擬內存總量中去了。操作系統會為這個共享內存區預留出虛擬內存的地址空間,不過,這只是一種簿記記錄,此時并不占用任何的 RAM 資源或其他硬件資源。

不是 空無一物

我們可以通過該進程的“應用層面的內存使用量的分類明細”圖,來檢查空的共享內存區是否占用了常駐(或物理)內存。

探討OpenResty和Nginx的共享內存區使用物理內存資源(或 RAM)?

有趣的是,我們在這個圖中看到了一個非零的 ?Nginx Shm Loaded (已加載的 Nginx 共享內存)組分。這部分很小,只有 612 KB,但還是出現了。所以空的共享內存區也并非空無一物。這是因為 Nginx 已經在新初始化的共享內存區域中放置了一些元數據,用于簿記目的。這些元數據為 Nginx 的 slab 分配器所使用。

已加載和未加載內存頁

我們可以通過 OpenResty 自動生成的下列圖表,查看共享內存區內被實際使用(或加載)的內存頁數量。

探討OpenResty和Nginx的共享內存區使用物理內存資源(或 RAM)?

我們發現在dogs區域中已經加載(或實際使用)的內存大小為 608 KB,同時有一個特殊的 ngx_accept_mutex_ptr 被 Nginx 核心自動分配用于 accept_mutex 功能。

這兩部分內存的大小相加為 612 KB,正是上文的餅狀圖中顯示的 Nginx Shm Loaded 的大小。

如前文所述,dogs 區使用的 608 KB 內存實際上是 slab 分配器 使用的元數據。

未加載的內存頁只是被保留的虛擬內存地址空間,并沒有被使用過。

關于進程的頁表

我們沒有提及的一種復雜性是,每一個 nginx 工作進程其實都有各自的OpenResty。CPU 硬件或操作系統內核正是通過查詢這些頁表來查找虛擬內存頁所對應的存儲。因此每個進程在不同共享內存區內可能有不同的已加載頁集合,因為每個進程在運行過程中可能訪問過不同的內存頁集合。為了簡化這里的分析,OpenResty 會顯示所有的為任意一個工作進程加載過的內存頁,即使當前的目標工作進程從未碰觸過這些內存頁。也正因為這個原因,已加載內存頁的總大小可能(略微)高于目標進程的常駐內存的大小。

空閑的和已使用的 slab

如上文所述,Nginx 通常使用 slabs 而不是內存頁來管理共享內存區內的空間。我們可以通過 OpenResty 直接查看某一個共享內存區內已使用的和空閑的(或未使用的)slabs 的統計信息:

探討OpenResty和Nginx的共享內存區使用物理內存資源(或 RAM)?

如我們所預期的,我們這個例子里的大部分 slabs 是空閑的未被使用的。注意,這里的內存大小的數字遠小于上一節中所示的內存頁層面的統計數字。這是因為 slabs 層面的抽象層次更高,并不包含 slab 分配器針對內存頁的大小補齊和地址對齊的內存消耗。

我們可以通過OpenResty進一步觀察在這個 dogs 區域中各個 slab 的大小分布情況:

探討OpenResty和Nginx的共享內存區使用物理內存資源(或 RAM)?

探討OpenResty和Nginx的共享內存區使用物理內存資源(或 RAM)?

我們可以看到這個空的共享內存區里,仍然有 3 個已使用的 slab 和 157 個空閑的 slab。這些 slab 的總個數為:3 + 157 = 160個。請記住這個數字,我們會在下文中跟寫入了一些用戶數據的 dogs 區里的情況進行對比。

寫入了用戶數據的共享內存區

下面我們會修改之前的配置示例,在 Nginx 服務器啟動時主動寫入一些數據。具體做法是,我們在 nginx.conf 文件的 http {} 配置分程序塊中增加下面這條 OpenResty 配置指令:

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 生成了下面這張餅圖:

探討OpenResty和Nginx的共享內存區使用物理內存資源(或 RAM)?

顯然,常駐內存的大小遠高于之前那個空的共享區的例子,而且在總的虛擬內存大小中所占的比例也更大(29.6%)。

虛擬內存的使用量也略有增加(從 134.73 MB 增加到了 135.30 MB)。因為共享內存區本身的大小沒有變化,所以共享內存區對于虛擬內存使用量的增加其實并沒有影響。這里略微增大的原因是我們通過 OpenResty 指令新引入了一些 Lua 代碼(這部分微小的內存也同時貢獻到了常駐內存中去了)。

應用層面的內存使用量明細顯示,Nginx 共享內存區域的已加載內存占用了最多常駐內存:

探討OpenResty和Nginx的共享內存區使用物理內存資源(或 RAM)?

已加載和未加載內存頁

現在在這個 dogs 共享內存區里,已加載的內存頁多了很多,而未加載的內存頁也有了相應的顯著減少:

探討OpenResty和Nginx的共享內存區使用物理內存資源(或 RAM)?

空的和已使用的 slab

現在 dogs 共享內存區增加了 300,000 個已使用的 slab(除了空的共享內存區中那 3 個總是會預分配的 slab 以外):

探討OpenResty和Nginx的共享內存區使用物理內存資源(或 RAM)?

顯然,OpenResty 區中的每一個鍵值對,其實都直接對應一個 slab。

空閑 slab 的數量與先前在空的共享內存區中的數量是完全相同的,即 157 個 slab:

探討OpenResty和Nginx的共享內存區使用物理內存資源(或 RAM)?

虛假的內存泄漏

正如我們上面所演示的,共享內存區在應用實際訪問其內部的內存頁之前,都不會實際耗費物理內存資源。因為這個原因,用戶可能會觀察到 Nginx 工作進程的常駐內存大小似乎會持續地增長,特別是在進程剛啟動之后。這會讓用戶誤以為存在內存泄漏。下面這張圖展示了這樣的一個例子:

探討OpenResty和Nginx的共享內存區使用物理內存資源(或 RAM)?

通過查看 OpenResty XRay 生成的應用級別的內存使用明細圖,我們可以清楚地看到 Nginx 的共享內存區域其實占用了絕大部分的常駐內存空間:

探討OpenResty和Nginx的共享內存區使用物理內存資源(或 RAM)?

這種內存增長是暫時的,會在共享內存區被填滿時停止。但是當用戶把共享內存區配置得特別大,大到超出當前系統中可用的物理內存的時候,仍然是有潛在風險的。正因為如此,我們應該注意觀察如下所示的內存頁級別的內存使用量的柱狀圖:

探討OpenResty和Nginx的共享內存區使用物理內存資源(或 RAM)?

圖中藍色的部分可能最終會被進程用盡(即變為紅色),而對當前系統產生沖擊。

HUP 重新加載

Nginx 支持通過 HUP 信號來OpenResty而不用退出它的 master 進程(worker 進程仍然會優雅退出并重啟)。通常 Nginx 共享內存區會在 HUP 重新加載(HUP reload)之后自動繼承原有的數據。所以原先為已訪問過的共享內存頁分配的那些物理內存頁也會保留下來。于是想通過 HUP 重新加載來釋放共享內存區內的常駐內存空間的嘗試是會失敗的。用戶應改用 Nginx 的重啟或OpenResty操作。

值得提醒的是,某一個 Nginx 模塊還是有權決定是否在 HUP 重新加載后保持原有的數據。所以可能會有例外。

結論

我們在上文中已經解釋了 Nginx 的共享內存區所占用的物理內存資源,可能遠少于 nginx.conf 文件中配置的大小。這要歸功于現代操作系統中的OpenResty特性。我們演示了空的共享內存區內依然會使用到一些內存頁和 slab,以用于存儲 slab 分配器本身需要的元數據。通過 OpenResty 的高級分析器,我們可以實時檢查運行中的 nginx 工作進程,查看其中的共享內存區實際使用或加載的內存,包括內存頁和 slab 這兩個不同層面。

另一方面,按需分頁的優化也會產生內存在某段時間內持續增長的現象。這其實并不是內存泄漏,但仍然具有一定的風險。我們也解釋了 Nginx 的 HUP 重新加載操作通常并不會清空共享內存區里已有的數據

推薦教程:OpenResty

? 版權聲明
THE END
喜歡就支持一下吧
點贊12 分享