摘要:有些用戶可能會選擇在自己編譯的 OpenResty 或者 Nginx 中使用第三方內存分配器。這個系列會詳細介紹 OpenResty 和 Nginx 分配和管理內存的細節,以便幫助那些基於這些技術構建的應用能夠有效地優化其內存使用。

OpenResty ® 開源 Web 平臺以 性能 和 內存佔用著稱。我們有一些用戶甚至在嵌入式系統中運行復雜的 OpenResty 應用,比如機器人。也有一些用戶在把他們的應用從其他技術棧(比如 Java,NodeJS 和 PHP)遷移到 OpenResty 之後,觀察到內存使用量上的顯著下降。然而,有時候我們還是需要優化某些 OpenResty 應用的內存使用。這些應用中的 Lua 代碼、NGINX 配置、第三方 Lua 庫或第三方 NGINX 模塊都可能會有 BUG 或者性能問題,從而導致應用佔用過多的內存,甚至存在內存泄露。

爲了有效地調試和優化內存的過度使用或者內存泄漏問題,我們需要了解 OpenResty、Nginx 和 LuaJIT 在內部是如何分配和管理內存的。我們的 OpenResty XRay 商業產品,能夠在不修改目標應用的情況下,自動分析和診斷幾乎所有的內存使用問題,即使是線上的生產應用。我們將撰寫一個系列的文章(本文是第一篇),使用 OpenResty XRay 在真實案例裏獲取到的數據和圖表,來詳細闡述 OpenResty、Nginx 和 LuaJIT 的內存分配和管理機制。

下面我們首先介紹 Nginx 進程在系統層面的內存佔用分佈,然後再逐個介紹應用層面的各種內存分配器。

系統層面

在現代操作系統中,進程在最高層面上申請和使用的內存都是虛擬內存。操作系統爲每個進程分配和管理虛擬內存,並將 實際使用 的虛擬內存頁,映射到物理內存頁上去(比如 DDR4 內存條等設備裏的)。一個很重要的概念是,進程可能會申請 很多 的虛擬內存空間,而實際只使用其中很小一部分。比如,一個進程可以向操作系統申請 2TB 的虛擬內存空間,即使當前系統只有 8GB 的物理內存(RAM)。只要這個進程沒有在這個巨大的虛擬內存空間中讀寫很多內存頁,就不會有任何問題。這部分實際映射到物理內存設備上的虛擬內存空間,纔是我們真正需要關注的。所以不要因爲看到 ps 或者 top 裏顯示佔用了很大的虛擬內存空間(通常叫做 VIRT )而感到驚慌。

實際使用的那一小部分虛擬內存(即讀寫了數據的),通常被叫做 RSS ,即 常駐內存 (resident memory)。當系統的物理內存快耗盡的時候,一部分常駐內存頁裏的數據會被 交換 到硬盤上。這部分被交換出去的內存空間不再是常駐內存的一部分,而是成爲 “交換出去的內存”(簡稱 "swap")。

有很多工具可以提供任意進程(包括 OpenResty 應用的 nginx 工作進程)的虛擬內存佔用、常駐內存佔用和交換出去的內存空間大小。 OpenResty XRay 可以自動分析任意一個正在運行中的 nginx 工作進程,並繪製出很漂亮的內存使用量的分解餅圖:

在這張圖裏,整塊餅代表 Nginx 進程從操作系統申請的全部虛擬內存空間。餅中的 Resident Memory 那一片則代表常駐內存的使用量,即實際使用的內存量。最後, Swap 塊則代表被交換出去的內存(此圖中並沒有出現,這是因爲這個進程並沒有任何被交換出去的內存頁)。

如上所述,我們通常最關心的是 Resident Memory 這一部分。不過如果餅圖中出現了 Swap 組分,也是非常值得注意的,因爲這意味着系統的物理內存已不足,可能會因頻繁換入換出內存頁而過載。另外,我們也需要注意一下圖中 未使用 的虛擬內存空間。這部分可能是因爲應用申請了過多過大的 Nginx 共享內存區域。這些尚未使用的共享內存空間可能在未來某一天被寫滿數據(即它們將轉變成爲 Resident Memory 組分的一部分),從而導致物理內存枯竭。

我們將在後續專門的一篇文章裏展開介紹常駐內存相關的更多有趣問題。下面先讓我們一起看看應用層面的內存使用分解。

應用層面

在應用層面分析內存使用細節往往會更有幫助。我們更關心當前使用的內存空間裏有多少是由 LuaJIT 內存分配器分配的,多少是 Nginx 核心和模塊分配的、而多少又是爲 Nginx 的共享內存區域所佔用的,諸如此類。

比如下面這個新類型的餅圖,是 OpenResty XRay 自動分析一個 OpenResty 應用的 Nginx 工作進程時得到的:

餅圖裏的 Glibc Allocator (Glibc 的分配器)部分是通過 Glibc 庫分配的總內存(Glibc 是 GNU 實現的標準 C 運行時庫)。通常我們在 C 代碼裏調用 malloc()realloc()calloc() 等函數就在使用這個內存分配器。它通常也被稱爲 系統分配器 。Nginx 核心及其模塊也通過這個系統分配器分配內存(有一個例外是 Nginx 的共享內存區域,我們後面會講到)。一些包含 C 組件或者 FFI 調用的 Lua 庫有時也會直接調用這個系統分配器,不過它們更常用的還是 LuaJIT 的內建分配器。當然,有些用戶也會選擇使用其他的標準 C 運行時庫實現,來編譯和構建 OpenResty 或 Nginx ,比如 musl libc 。我們也會在後續專門的文章中展開討論系統分配器和 Nginx 的分配器。

餅圖中的 Nginx Shm Loaded 組分是 Nginx 核心及其模塊分配的共享內存(即 "shm")區域中 實際使用 的那部分空間。這些共享內存是通過 UNIX 系統調用 mmap() 直接分配的,因此完全繞過了標準 C 運行時庫的分配器。

Nginx 共享內存是所有 Nginx 工作進程之間共享的。這些共享內存區域通常是通過標準 Nginx 配置指令來創建的,比如 ssl_session_cacheproxy_cache_pathlimit_req_zonelimit_conn_zone 、和 upstream 的 zone 指令。Nginx 的第三方模塊也可能會創建自己的共享內存區域,比如 OpenResty 的核心組件 ngx_http_lua_module 。OpenResty 應用通常在 Nginx 配置文件中使用 lua_shared_dict 指令來創建自己的共享內存區域。我們近期也會有專門文章更詳細地闡述 Nginx 的共享內存相關的細節。

餅圖中的 HTTP/Stream LuaJIT Allocator 這兩個組分則代表 LuaJIT 的內建分配器分配和管理的內存大小。

其中一個表示 Nginx 的 HTTP 子系統中的 LuaJIT 虛擬機(VM)實例,另外一個代表 Nginx 的 Stream 子系統中的 LuaJIT VM 實例。LuaJIT 有一個編譯選項可以強制使用系統分配器,不過這個選項通常只用於特殊的調試和測試工具(比如 ValgrindAddressSanitizer )。Lua 字符串、表(table)、函數、cdata、userdata、upvalue 等等,都是通過這個分配器來分配的。與之相反,原初類型的 Lua 值,比如整數、浮點數、light userdata 以及布爾值等等,則不需要任何動態內存分配。此外,在 Lua 代碼裏調用 ffi.new() 所分配的 C 級別的內存塊,也是通過 LuaJIT 自己的分配器來分配的。由這個分配器分配的所有內存塊,都由 LuaJIT 的垃圾回收器(GC)來統一管理,因此我們無需主動釋放不再需要的內存塊。這些內存對象也被叫做“GC 對象”。我們將在其他文章裏闡述這個課題。

餅圖裏的 Text Segments 組分則對應所有可執行文件和動態鏈接庫的 .text 段,映射到虛擬內存空間之後的總大小。這些 .text 段通常包含可執行的二進制機器代碼。

最後,圖中的 System Stacks 組分指的是目標進程裏所有系統棧(或者說 “C 棧”)佔用的總大小。每個操作系統(OS)線程都有自己的系統棧。只有當使用了多線程的時候纔會出現多個系統棧(請注意 OpenResty 中使用 ngx.thread.spawn 創建的 “輕線程” 跟這種系統級別的線程,是完全不同的兩種東西)。Nginx 工作進程通常只有一個系統線程,除非配置了 OS 線程池(通過 aio threads 配置指令)。

有些用戶可能會選擇在自己編譯的 OpenResty 或者 Nginx 中使用第三方內存分配器。常見的例子是 tcmalloc

jemalloc ,因爲它們可以加速系統分配器(比如 malloc )。對於一些 Nginx 第三方模塊、Lua C 模塊或 C 庫(包括 OpenSSL!)中直接調用 malloc() 申請小內存塊的場景,它們確實可以提供比較明顯的加速效果。便是對於那些已經使用了設計良好的分配器(比如 Nginx 的內存池和 LuaJIT 的內建分配器)的部分,使用它們則沒有太多好處。反之,使用這樣的“外掛”分配器的軟件庫,會引入新的複雜性和問題。我們將會在後續文章中更加詳細地闡述。

已用或未用

使用上面介紹的應用級別的內存分解圖,並不太好直接分析哪些虛擬內存頁被實際使用,而哪些並沒有。只有餅圖中的 Nginx Shm Loaded 組分是實際使用的虛擬內存空間,而其他組分則同時包含了使用了的和尚未使用的虛擬內存頁。幸運的是,Glibc 的分配器和 LuaJIT 的分配器分配的內存,經常都會被立即實際使用的,所以絕大多數時候,二者並沒有多少差別。

傳統的 Nginx 服務器

傳統的 Nginx 服務器軟件只是 OpenResty 應用的嚴格 子集 。這些用戶仍會看到系統分配器的內存用量和 Nginx 共享內存區域的使用量,偶爾也會涉及一些其他內存分配器。OpenResty XRay 仍然可以用於直接檢查和分析這些服務器進程,甚至在生產環境。當然,如果你沒有編譯 Lua 模塊進你的 Nginx,那就不會看到任何與 Lua 相關的內存使用。

結論

本文是一個系列文章中的第一篇。這個系列會詳細介紹 OpenResty 和 Nginx 分配和管理內存的細節,以便幫助那些基於這些技術構建的應用能夠有效地優化其內存使用。後續的文章會展開介紹每一個細分的主題,覆蓋各個不同的內存分配器和內存管理機制。敬請期待!

關於 OpenResty XRay

OpenResty XRay 是由 OpenResty Inc. 公司提供的商業產品。我們使用此產品爲我們的文章(比如本文)提供直觀的圖表演示和真實系統內部的統計數據。OpenResty XRay 可以在無需目標程序任何配合的情況下,幫助用戶深入洞察其線上或者線下的各種軟件系統的行爲細節,有效地分析和定位各種性能問題、可靠性問題和安全問題。

關於作者

章亦春是開源項目 OpenResty ® 的創始人,同時也是 OpenResty Inc. 公司的創始人和 CEO。他貢獻了許多 Nginx 的第三方模塊,相當多 Nginx 和 LuaJIT 核心補丁,並且設計了 OpenResty XRay 等產品。

翻譯

我們提供了英文版原文和中譯版(本文),也在 OpenResty Inc. 官方博客 提供和維護 英文版原文中譯版 。我們也歡迎讀者提供其他語言的翻譯版本,只要是全文翻譯不帶省略,我們都將會考慮採用,非常感謝!

  1. 現代安卓(Android)操作系統支持將內存頁交換到內存,但那些內存頁是經過壓縮的,同時可以節約物理內存空間。
  2. 這個編譯選項叫做 -DLUAJIT_USE_SYSMALLOC ,但千萬別在生產中使用!
  3. 通常 LuaJIT 運行時在底層只使用一種數值類型表示,即雙精度浮點數(double),但用戶仍然可以通過傳入編譯選項 -DLUAJIT_NUMMODE=2 來同時啓用 32 位整數的底層表示。
  4. 但是我們仍然有責任確保所有指向那些無用對象的引用都被正確去除。
相關文章