瞭解 OpenResty XRay 是如何做到幫助企業定位應用程式存在的問題以及最佳化其效率的。

瞭解更多 LIVE DEMO

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)。當系統的實體記憶體快耗盡的時候,一部分常駐記憶體頁裡的資料會被 交換 到硬碟上1。這部分被交換出去的記憶體空間不再是常駐記憶體的一部分,而是成為 “交換出去的記憶體”(簡稱 “swap”)。

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

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

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

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

應用層面

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

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

Glibc 分配器

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

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 的共享記憶體相關的細節。

更多細節可以參考另一篇部落格文章,《OpenResty 和 Nginx 的共享記憶體區是如何消耗實體記憶體的》

LuaJIT 分配器

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

程式程式碼段

餅圖裡的 Text Segments 組分則對應所有可執行檔案和動態連結庫的 .text 段,對映到虛擬記憶體空間之後的總大小。 這些 .text 段通常包含可執行的二進位制機器程式碼。

系統執行時棧

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

其他的系統分配器

有些使用者可能會選擇在自己編譯的 OpenResty 或者 Nginx 中使用第三方記憶體分配器。常見的例子是 tcmallocjemalloc,因為它們可以加速系統分配器(比如 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® 專案創始人兼 OpenResty Inc. 公司 CEO 和創始人。

章亦春(Github ID: agentzh),生於中國江蘇,現定居美國灣區。他是中國早期開源技術和文化的倡導者和領軍人物,曾供職於多家國際知名的高科技企業,如 Cloudflare、雅虎、阿里巴巴, 是 “邊緣計算“、”動態追蹤 “和 “機器程式設計 “的先驅,擁有超過 22 年的程式設計及 16 年的開源經驗。作為擁有超過 4000 萬全球域名使用者的開源專案的領導者。他基於其 OpenResty® 開源專案打造的高科技企業 OpenResty Inc. 位於美國矽谷中心。其主打的兩個產品 OpenResty XRay(利用動態追蹤技術的非侵入式的故障剖析和排除工具)和 OpenResty Edge(最適合微服務和分散式流量的全能型閘道器軟體),廣受全球眾多上市及大型企業青睞。在 OpenResty 以外,章亦春為多個開源專案貢獻了累計超過百萬行程式碼,其中包括,Linux 核心、Nginx、LuaJITGDBSystemTapLLVM、Perl 等,並編寫過 60 多個開源軟體庫。

關注我們

如果您喜歡本文,歡迎關注我們 OpenResty Inc. 公司的部落格網站 。也歡迎掃碼關注我們的微信公眾號:

我們的微信公眾號

翻譯

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


  1. 現代安卓(Android)作業系統支援將記憶體頁交換到記憶體,但那些記憶體頁是經過壓縮的,同時可以節約實體記憶體空間。 ↩︎

  2. 這個編譯選項叫做 -DLUAJIT_USE_SYSMALLOC,但千萬別在生產中使用! ↩︎

  3. 通常 LuaJIT 執行時在底層只使用一種數值型別表示,即雙精度浮點數(double),但使用者仍然可以透過傳入編譯選項 -DLUAJIT_NUMMODE=2 來同時啟用 32 位整數的底層表示。 ↩︎

  4. 但是我們仍然有責任確保所有指向那些無用物件的引用都被正確去除。 ↩︎