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

瞭解更多 LIVE DEMO

記憶體碎片是計算機系統中的一個常見問題,儘管已經存在許多解決這個問題的巧妙演算法。 記憶體碎片會浪費記憶體區中空閒的記憶體塊。這些空閒的記憶體塊無法合併成更大的記憶體區以滿足應用未來對較大記憶體塊的申請,也無法重新釋放到作業系統用於其他用途1。 這會導致記憶體洩露現象,因為對大塊的記憶體申請越來越多,滿足這些請求所需要的總記憶體大小會無限增加。 這種記憶體使用量的無限增加通常不會被視為記憶體洩漏,因為未使用的記憶體塊實際上被釋放並標記為空閒記憶體,只是這些記憶體塊無法被重新用於滿足更大的記憶體塊申請,同時也無法重新被返還給作業系統供其他程序使用。

在 OpenResty 或 Nginx 的共享記憶體區內,如果被申請的記憶體 slab 或記憶體塊的大小差別很大,也容易出現記憶體碎片問題。 例如,ngx_lua 模組的 lua_shared_dict 區域支援寫入任意長度的任意使用者資料項。這些資料項可以小到一個數值,也可以大到很長的字串。 如果不注意的話,使用者的 Lua 程式很容易在共享記憶體區中產生嚴重的記憶體碎片問題,浪費大量的空閒記憶體。 本文中將列舉幾個獨立的例子,來詳細演示這個問題以及相關行為細節。 本文將使用 OpenResty XRay 動態追蹤平臺產品,直接透過生動的視覺化方法來觀察記憶體碎片。 我們將在文章結尾中介紹減少共享記憶體區記憶體碎片問題的最佳實踐。

與本部落格網站中的幾乎所有技術類文章類似,我們使用 OpenResty XRay 這款動態追蹤產品對未經修改的 OpenResty 或 Nginx 伺服器和應用的內部進行深度分析和視覺化呈現。 因為 OpenResty XRay 是一個非侵入性的分析平臺,所以我們不需要對 OpenResty 或 Nginx 的目標程序做任何修改 – 不需要程式碼注入,也不需要在目標程序中載入特殊外掛或模組。 這樣可以保證我們透過 OpenResty XRay 分析工具所看到的目標程序內部狀態,與沒有觀察者時的狀態是完全一致的。

如果您還不熟悉 OpenResty 或 Nginx 共享記憶體區內的記憶體分配和使用,請參閱上一篇部落格文章《OpenResty 和 Nginx 的共享記憶體區是如何消耗實體記憶體的》

空的共享記憶體區

首先,我們以一個沒有寫入使用者資料的空的共享記憶體區為例,透過檢視這個記憶體區內部的 slab 或記憶體塊,瞭解“基準資料”:

master_process on;
worker_processes 1;

events {
    worker_connections 1024;
}

http {
    lua_shared_dict cats 40m;

    server {
        listen 8080;
    }
}

我們在這裡配置了一個 40MB 的共享記憶體區,名為 cats。 我們在配置裡完全沒有觸及這個 cats 區,所以這個區是空的。 但從前一篇部落格文章 《OpenResty 和 Nginx 的共享記憶體區是如何消耗實體記憶體的》 中,我們已經知道空的共享記憶體區依舊有 160 個預分配的 slab,用於存放 slab 分配器使用的後設資料。 下面這張虛擬記憶體空間內 slab 的分佈圖也證實了這一點:

這個圖同樣由 OpenResty XRay 自動生成,用於分析實際執行中的程序。 從圖中可以看到,有 3 個已經使用的 slab,還有 100 多個空閒 slab。

填充類似大小的條目

我們在上面的例子 nginx.conf 中增加下列指令 location = /t

location = /t {
    content_by_lua_block {
        local cats = ngx.shared.cats
        local i = 0
        while true do
            local ok, err = cats:safe_set(i, i)
            if not ok then
                if err == "no memory" then
                    break
                end
                ngx.say("failed to set: ", err)
                return
            end
            i = i + 1
        end
        ngx.say("inserted ", i, " keys.")
    }
}

這裡我們在 location = /t 中定義了一個 Lua 內容處理程式,在 cats 區中寫入很小的鍵值對,直到沒有空閒的記憶體區為止。 因為我們寫入的鍵和值都是數值(number),而 cats 共享記憶體區又很小,所以在這個區中寫入的各個鍵值對的大小應該都是彼此很接近的。在啟動 Nginx 伺服器程序後,我們使用下面的命令查詢這個 /t 位置:

$ curl 'localhost:8080/t'
inserted 255 keys.

從響應體結果中可以看到,我們在這個共享記憶體區中可以寫入了 255 個鍵。

我們可以再次檢視這個共享記憶體區內的 slab 佈局:

如果將這個圖與上文那個空的共享記憶體區分佈圖進行對比,我們可以看到新增的那些更大的 slab 都是紅色的(即表示已被使用)。 有趣的是,中間那些空閒 slab (綠色)無法被更大的 slab 重新使用,儘管兩者彼此接近。 顯然,這些原本預留的空 slab 並不會被自動合併成更大的空閒 slab。

我們再透過 OpenResty XRay 來檢視這些 slab 的大小分佈情況:

我們看到幾乎所有已使用的 slab 的大小都是 128 個位元組。

刪除奇數鍵

接下來,我們嘗試在現有的 Lua 處理程式後面追增下面這個 Lua 程式碼片段,以刪除共享記憶體區內的奇數鍵:

for j = 1, i, 2 do
    cats.delete(j)
end

重啟伺服器後,再次查詢 /t,我們得到了新的 cats 共享記憶體區 slab 佈局圖:

現在,我們有許多不相鄰的空閒記憶體塊,而這些記憶體塊無法合併成大塊,從而無法滿足未來大塊記憶體的申請。 我們嘗試在 Lua 處理程式之後再新增下面的 Lua 程式碼,以插入一個更大的鍵值對條目:

local ok, err = cats:safe_add("Jimmy", string.rep("a", 200))
if not ok then
    ngx.say("failed to add a big entry: ", err)
end

然後我們重啟伺服器程序,並查詢 /t

$ curl 'localhost:8080/t'
inserted 255 keys.
failed to add a big entry: no memory

如我們所預期的,新新增的大的條目有一個 200 個位元組的字串值,所以相對應的 slab 必然大於共享記憶體區中可用的最大的空閒 slab(即我們前面看到的 128 位元組)。 所以如果不強行驅除某些已使用的鍵值對條目,則無法滿足這個記憶體塊請求(例如在記憶體區中記憶體不足時,set() 方法自動刪最冷的已使用的條目資料的行為)。

刪除前半部分的鍵

接下來我們來做一個不同的試驗。我們不刪除上文中的奇數鍵,而是改為新增下列 Lua 程式碼以刪除共享記憶體區前半部分裡的那些鍵:

for j = 0, i / 2 do
    assert(cats:delete(j))
end

重啟伺服器程序並查詢 /t 之後,我們得到了下面這個虛擬記憶體空間裡的 slab 佈局:

可以看到在這個共享記憶體區的中間位置附近,那些相鄰的空閒 slab 被自動合併成了 3 個較大的空閒 slab。 實際上,這是 3 個空閒記憶體頁,每個記憶體頁的大小為 4096 個位元組:

這些空閒記憶體頁可以進一步形成跨越多個記憶體頁的更大的 slab。

下面,我們再次嘗試寫入上文中插入失敗的大條目:

local ok, err = cats:safe_add("Jimmy", string.rep("a", 200))
if not ok then
    ngx.say("failed to add a big entry: ", err)
else
    ngx.say("succeeded in inserting the big entry.")
end

這一回,我們終於成功插入了,因為我們有很大的連續空閒空間,足以容納這個鍵值對:

$ curl 'localhost:8080/t'
inserted 255 keys.
succeeded in inserting the big entry.

現在,新的 slab 分佈圖裡已經明顯可以看到這個新的條目了:

請注意圖中前半部分裡的那條最狹長的紅色方塊。那就是我們的“大條目”。 我們從已使用的 slab 的大小分佈圖中可以看得更清楚些:

從圖中可以看出,“大條目”實際上是一個 512 個位元組的 slab(包含了鍵大小、值大小、記憶體補齊和地址對齊的開銷)。

緩解記憶體碎片

在上文中我們已經看到,分散在共享記憶體區內的小空閒 slab 容易產生記憶體碎片問題,導致未來大塊記憶體的申請無法被滿足,即使所有空閒 slab 加起來的總大小還要大得多。 我們推薦下面兩種方法,可更好地重新利用這些空閒的 slab 空間:

  1. 始終使用大小接近的資料條目,這樣就不再存在需要滿足大得多的記憶體塊申請的問題了。
  2. 讓被刪除的資料條目鄰近,以方便這些條目被自動合併成更大的空閒 slab 塊。

對於方法 1),我們可以把一個統一的共享記憶體區人為分割成多個針對不同資料條目大小分組的共享記憶體區2。例如,我們可以有一個共享記憶體區只存放大小為 0 ~ 128 個位元組的條目,而另一個共享記憶體記憶體區只存放大小為 128 ~ 256 個位元組的條目。

而對於方法 2)我們可以按照條目的過期時間進行分組。比如過期時間較短的資料條目,可以集中存放在一個專門的共享記憶體區,而過期時間較長的條目則可以存放在另一個共享記憶體區。這樣我們可以保證同一個共享記憶體區內的條目都以類似的速度到期,從而提高條目同時到期,以及同時被刪除和合並的機率。

結論

OpenResty 或 Nginx 共享記憶體區內的記憶體碎片問題在缺少工具的情況下,還是很難被觀察和除錯的。 幸運的是,OpenResty XRay 提供了強有力的可觀察性和視覺化呈現,能夠迅速地發現和診斷問題。 本文中我們透過一系列小例子,使用 OpenResty XRay 自動生成的圖表和資料,揭示了背後到底發生了甚麼,演示了記憶體碎片問題以及緩解這個問題的方法。最後,我們介紹了基於 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. 對於 OpenResty 和 Nginx 的共享記憶體區而言,分配和訪問過的記憶體頁在程序退出之前都永遠不會再返還給作業系統。當然,釋放的記憶體頁和記憶體 slab 仍然可以在共享記憶體區內被重新使用。 ↩︎

  2. 有趣的是,Linux 核心的 Buddy 記憶體分配器以及 Memcached 的分配器也使用了類似的策略。 ↩︎