OpenResty 和 Nginx 的共享內存區是如何消耗物理內存的

OpenResty 和 Nginx 服務器一般會配置共享內存區,用於儲存在全部工做進程之間共享的數據。例如,Nginx 標準模塊 ngx_http_limit_reqngx_http_limit_conn 使用共享內存區儲存狀態數據,以限制全部工做進程中的用戶請求速率和用戶請求的併發度。OpenResty 的 ngx_lua 模塊經過 lua_shared_dict,向用戶 Lua 代碼提供基於共享內存的數據字典存儲。html

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

與本博客網站 中的幾乎全部技術類文章相似,咱們使用 OpenResty XRay 這款動態追蹤產品對未經修改的 OpenResty 或 Nginx 服務器和應用的內部進行深度分析和可視化呈現。由於 OpenResty XRay 是一個非侵入性的分析平臺,因此咱們不須要對 OpenResty 或 Nginx 的目標進程作任何修改 -- 不須要代碼注入,也不須要在目標進程中加載特殊插件或模塊。這樣能夠保證咱們經過 OpenResty XRay 分析工具所看到的目標進程內部狀態,與沒有觀察者時的狀態是徹底一致的。git

咱們將在多數示例中使用 ngx_lua 模塊的 lua_shared_dict,由於該模塊可使用自定義的 Lua 代碼進行編程。咱們在這些示例中展現的行爲和問題,也一樣適用於全部標準 Nginx 模塊和第三方模塊中的其餘共享內存區。github

Slab 與內存頁

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

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

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

操做系統之內存頁爲單位來管理進程的共享內存(或其餘種類的內存)。
x86_64 Linux 系統中,默認的內存頁大小一般是 4 KB,但具體大小取決於體系結構和 Linux 內核的配置。例如,某些 Aarch64 Linux 系統的內存頁大小高達 64 KB。segmentfault

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

分配的內存不必定有消耗

與硬盤這樣的資源不一樣,物理內存(或 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 的統計信息:

dogs區域中空的和已使用的slab

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

咱們能夠經過OpenResty XRay進一步觀察在這個 dogs 區域中各個 slab 的大小分佈狀況:

空白區域的已使用 slab 大小分佈

空的 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 區域內已加載和未加載的內存頁

已加載和未加載內存頁

如今在這個 dogs 共享內存區裏,已加載的內存頁多了不少,而未加載的內存頁也有了相應的顯著減小:

dogs區域中的已加載和未加載內存頁

空的和已使用的 slab

如今 dogs 共享內存區增長了 300,000 個已使用的 slab(除了空的共享內存區中那 3 個老是會預分配的 slab 之外):

dogs非空白區域中的已使用slab

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

空閒 slab 的數量與先前在空的共享內存區中的數量是徹底相同的,即 157 個 slab:

dogs非空白區域的空slab

虛假的內存泄漏

正如咱們上面所演示的,共享內存區在應用實際訪問其內部的內存頁以前,都不會實際耗費物理內存資源。由於這個緣由,用戶可能會觀察到 Nginx 工做進程的常駐內存大小彷佛會持續地增加,特別是在進程剛啓動以後。這會讓用戶誤覺得存在內存泄漏。下面這張圖展現了這樣的一個例子:

process memory growing

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

Memory usage breakdown for huge shm zones

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

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)時,一些常駐內存會被保存和映射到硬盤設備上去。
相關文章
相關標籤/搜索