我們最近使用 OpenResty XRay 幫助一個銷售 CDN 和流量網關服務的企業客户優化了他們的 OpenResty/Nginx 服務器的內存使用。這個客户在他們的 OpenResty/Nginx 配置文件中定義了許多虛擬服務器和 URI location。OpenResty XRay 在客户的生產環境中自動進行了大部分分析,基於分析結果給出的方案讓 nginx 進程的內存佔用減少了大約 30%。
和我們的 OpenResty Edge 的 nginx worker 進程相比顯示, 進一步的優化將會繼續減少約 90%。
OpenResty XRay 是一個動態追蹤產品,它可以自動分析正在運行中的應用程序,以排除性能問題、行為問題和安全漏洞,並提供可行的建議。在底層實現上,OpenResty XRay 由我們的 Y 語言驅動,可以在不同環境下支持多種不同的運行時,如 Stap+, eBPF+, GDB 和 ODB。
挑戰
這個 CDN 供應商使用一個超大的“nginx.conf”配置文件來為他們的 OpenResty 服務器中的近萬個虛擬主機服務。每個 nginx 主進程在啓動後佔用了好幾個 G 的內存,在一次或多次 HUP reload 後,內存幾乎翻倍。從下面 OpenResty XRay 生成的圖中可以看出,最大內存佔用約為 4.60GB。
我們可以從 OpenResty XRay 的應用層面內存使用明細表中看到,Glibc 分配器佔用了大部分常駐內存,有 4.55GB。
而 OpenResty XRay 發現 Nginx cycle pool 佔用了大量的內存:
當 Nginx 加載配置文件時,我們都知道它為這個 cycle pool 內的配置數據分配了數據結構。雖然龐大到有 1.62GB,但遠遠小於上面提到的 4.60GB。
RAM 依然是昂貴和稀缺的硬件資源,特別是在 AWS 和 GCP 這樣的公有云上。客户希望通過降級到內存較小的機器來節約成本。
分析
OpenResty XRay 對客户的在線進程進行了深入分析。它不需要客户的應用程序進行任何協作。
- 沒有額外的插件、模塊或庫。
- 沒有代碼注入或補丁。
- 沒有特殊的編譯或啓動選項。
- 甚至不需要重新啓動應用程序進程。
分析完全是以“事後”的方式進行的。多虧了 Openresty XRay 採用的動態跟蹤技術。
太多的空閒區塊
OpenResty XRay 用 Glibc 內存分配器的分析器自動對在線 nginx 進程進行採樣。分析器生成了以下柱狀圖,顯示了分配器管理的空閒塊的大小是如何分佈的。
Glibc 分配器通常不會立即釋放空閒塊給操作系統(OS)。它可能會保留一些空閒塊,以加快後續的分配速度。但有意保留的通常很小,不可能到上 G 字節。這裏我們看到空閒塊的大小累計已經達到 2.3GB。因此,更常見的原因是內存碎片。
查看普通堆中的內存碎片問題
大多數小的內存分配通過 brk Linux 系統調用,發生在“普通堆”中。這個堆就像一個線性的 "堆“,只能通過移動其”頂部"指針來增加或減少。在堆中間的所有空閒塊不能被釋放給操作系統。直到它們上面的所有塊也變成空閒,它們才會被釋放。
OpenResty XRay 的內存分析器可以幫助我們查看這種堆的狀態。請看下面的堆圖,它是在 nginx 主進程響應 HUP 信號加載新配置後的採樣圖。
我們可以看到,堆是向上增長的,也就是説,向高位內存地址增長。注意 brk top 指針,這是唯一可以移動的東西。綠色框屬於 Nginx 的新 “cycle pool“,而粉色框屬於舊 ”cycle pool”。一個有趣的現象是,Nginx 會保留舊的 cycle pool 或舊的配置數據,直到新的 cycle pool 被成功加載。這種行為是由於 Nginx 的保護機制,當新的配置加載失敗時,會優雅地退回到舊的配置。不幸的是,正如我們在上面看到的,舊的配置數據的盒子(綠色)在新的數據(粉色)下面,因此只有當新的配置數據也被釋放後,它們才能釋放到操作系統。
事實上,在 Nginx 釋放了舊的配置數據和舊的 cycle pool 後,它們原來的位置變成了空閒塊,被卡在新的 cycle pool 的塊下面。
這是一個教科書式的內存碎片化的例子。普通堆只能在頂部釋放內存;因此,它比其他內存分配機制,如 mmap 系統調用,更容易受到內存碎片的影響。但是,mmap 會在這裏拯救我們嗎?不一定。
mmap 的世界
Glibc 分配器也可以通過 mmap 系統調用來分配內存。這些系統調用分配離散的內存塊或內存段,這些內存塊或內存段可能位於進程地址空間的幾乎任何地址,並跨越任何數量的內存頁。
這聽起來是一個緩解上述內存碎片問題的好方法。但是當我們有意阻斷普通堆的增長方式時,根據 OpenResty XRay 的分析器所產生的圖表,類似程度的內存碎片仍然發生。
當應用程序(這裏是 Nginx)請求分配較小內存塊的時候,Glibc 傾向於分配相對較大的內存段,這裏是 1MB。因此,內存碎片仍然會發生在這些 1MB 的 mmap 段內。如果一個小內存塊仍在使用,那麼整個內存段就不會被釋放給操作系統。
在上圖中,我們可以看到舊的 cycle pool 塊(粉紅色)和新的 cycle pool 塊(綠色)仍然在許多 mmap 段中交錯。
解決方案
我們為客户提出了幾個解決方案。
簡單方式
最簡單的方式是直接解決內存碎片的問題。根據上面我們使用 OpenResty XRay 做的分析,我們應該做以下一個或多個變動。
- 避免在“普通堆”中分配 cycle pool 內存(即取消這種分配的
brk系統調用)。 - 要求 Glibc 使用適當的 mmap 段內存大小(不要太大!)來滿足 cycle pool 的內存分配請求。
- 將不同 cycle pool 的內存塊乾淨地分離到不同的 mmap 段中。
我們為 OpenResty XRay 的付費客户提供詳細的優化説明。因此根本就不需要編碼。
更好的方式
是的,還有一個更好的方式。開源的 OpenResty 軟件提供了 Lua APIs 和 Nginx 配置指令,以動態加載(和卸載)Lua 層面的新的配置數據,而不需要通過 Nginx 配置文件機制。這使得使用一個小的恆定大小的內存來處理更多的虛擬 server 和 location 的配置數據成為可能。同時,Nginx 服務器的啓動和重新加載時間也大大縮短(從很多秒到幾乎為零)。事實上,有了動態配置加載,HUP reload 操作本身變得非常罕見。這種方式的一個缺點是,這需要在我們用户側進行一些額外的 Lua 編碼。
我們的 OpenResty Edge 軟件產品以 OpenResty 作者所設想的最佳方式實現了這種動態配置加載和卸載。它不需要用户進行任何編碼。所以這也是一個容易的選項。
結果
這位客户決定先嚐試簡單的方式,結果在幾次 HUP reload 後,總的內存佔用減少了 30%。
仍然有一些剩餘的片段值得進一步關注。但我們的客户已經很滿意了。此外,上面提到的更好的方式可以節省超過 90% 的總內存佔用(就像在我們的 OpenResty Edge 產品中一樣):
關於作者
章亦春是開源 OpenResty® 項目創始人兼 OpenResty Inc. 公司 CEO 和創始人。
章亦春(Github ID: agentzh),生於中國江蘇,現定居美國灣區。他是中國早期開源技術和文化的倡導者和領軍人物,曾供職於多家國際知名的高科技企業,如 Cloudflare、雅虎、阿里巴巴, 是 “邊緣計算“、”動態追蹤 “和 “機器編程 “的先驅,擁有超過 22 年的編程及 16 年的開源經驗。作為擁有超過 4000 萬全球域名用户的開源項目的領導者。他基於其 OpenResty® 開源項目打造的高科技企業 OpenResty Inc. 位於美國硅谷中心。其主打的兩個產品 OpenResty XRay(利用動態追蹤技術的非侵入式的故障剖析和排除工具)和 OpenResty Edge(最適合微服務和分佈式流量的全能型網關軟件),廣受全球眾多上市及大型企業青睞。在 OpenResty 以外,章亦春為多個開源項目貢獻了累計超過百萬行代碼,其中包括,Linux 內核、Nginx、LuaJIT、GDB、SystemTap、LLVM、Perl 等,並編寫過 60 多個開源軟件庫。
關注我們
如果您喜歡本文,歡迎關注我們 OpenResty Inc.
公司的博客網站 。
我們也在 B 站上也有 OpenResty 官方的視頻分享空間,歡迎訂閲。
同時歡迎掃碼關注我們的微信公眾號: