Redis 的延遲問題,我找出原因並解決了!

摘要:如果你喜歡性能工程,以及剝離層層抽象深入探究底層子系統,那麼你一定很喜歡本文。

連結:https://about.gitlab.com/blog/2022/11/28/how-we-diagnosed-and-resolved-redis-latency-spikes/?continueFlag=942986d1d503b78fd935ad0b88d007cb

作者 | Matt Smiley

譯者 | 彎月 責編 | 鄭麗媛

本文的背景是一個 Redis 慢性延遲的問題,在本文中,我們將使用 BPF 和分析工具,結合標準指標來揭示系統幕後的一些鮮為人知的行為。

除了工具和技術之外,我們還將使用迭代假設檢驗方法來構建系統動力行為模型,可以通過這個模型了解哪些因素會影響問題的嚴重性和觸發條件。

最終,我們找到了根本原因,相應的補救措施雖然有效,但沒什麼新意。我們發現了一個包含三個階段的環路,它有兩個不同的飽和點,還找到了一個簡單的修復方法來打破這個環路。在此過程中,我們使用了一系列技術來檢查系統行為的各個方面,包括棧取樣剖析、熱圖和火焰圖、實驗性的微調、源程式碼分析和二進位制分析、指令級 BPF 檢測,以及在特定進入和退出條件下的定向延遲注入。

問題介紹:慢性延遲

問題介紹:慢性延遲

GitLab 使用了大量 Redis,我們甚至為特定功能建立了單獨的 Redis 集群。本文介紹的 Redis 實例的用途是 LRU 緩衝。

這個快取有慢性延遲的問題,兩年多前開始間歇性地發生,最近幾個月越來越糟,每隔幾分鐘,就會出現突發性的超高延遲,相應的吞吐量也會下降,導致 SLO(Service Level Objective,服務水平目標)惡化。這些延遲峰值影響了面向使用者的響應時間,並耗費了大量相關功能的錯誤預算,所以我們要設法解決這個問題。

圖:Redis 請求的速率峰值(僅包含響應速度超過1秒的請求),每個峰值對應一次驅逐突發

在之前的工作中,我們已經完成了多項最佳化。之後,情況有所好轉,並持續了一段時間,但後來延遲增長再次浮出水面,成為了一個重要的長期擴展問題。我們還排除了外部觸發的可能性,例如請求氾濫、連接速率峰值、主機資源競爭等。這些延遲峰值是由於記憶體使用量達到驅逐閾值(maxmemory)造成的,與客戶端流量的變化模式或其他與 Redis 競爭 CPU 時間、記憶體頻寬或網路 I/O 的進程無關。

最初,我們以為 Redis 6.2 新推出的驅逐節流機制可以降低我們的驅逐突發開銷。結果卻發現沒有任何幫助,不過該機制解決了另一個問題:防止由 performEvictions 調用運行時間過長導致的停頓。相比之下,在分析過程中,我們發現我們的問題(無論是 Redis 升級之前還是之後)與大量調用導致 Redis 吞吐量降低有關,而不是因為一些調用過慢導致 Redis 完全停止。

為了找出瓶頸以及潛在的解決方案,我們需要調查 Redis 工作負載驅逐爆發期間的行為。

Redis 驅逐的一些背景知識

Redis 驅逐的一些背景知識

當時,快取的訂閱數量超過了預期,導致試圖保存的快取鍵數量超過了 maxmemory 設置的閾值,因此發生 LRU 快取驅逐並不意外,但這種驅逐額外開銷的密集程度還是令人不安。

Redis 本質上是單執行緒的。除了少數例外,「主」執行緒會連續執行所有任務,包括處理客戶端請求和驅逐等。在任務 X 上花費的時間過多,則意味著執行任務 Y 的時間就會減少,類似於隊列的行為。

每當 Redis 達到其 maxmemory 閾值時,就會通過驅逐一些鍵來釋放記憶體,直到恢復至 maxmemory 以下。然而,與預期相反,記憶體使用率與逐驅率的指標(如下所示)表明,驅逐率並不是連續或穩定的,而是會突然爆發,並釋放比預期更多的記憶體。每次驅逐爆發後,直到記憶體使用率再次攀升至 maxmemory 閾值,才會再開始驅逐。

圖:Redis 記憶體使用量在每次驅逐突發期間下降 300~500 MB

圖:鍵的驅逐峰值與上面顯示的記憶體使用下降的時間和大小相一致

為什麼會發生這種過量的驅逐?這一直是核心的謎團。最初,我們以為找出這個問題的原因,就能知道怎樣才能平滑驅逐率、分散開銷並避免延遲峰值。結果,我們發現這些爆發是需要避免的互動作用,我們後面會詳細介紹。

驅逐突發導致 CPU 飽和

驅逐突發導致 CPU 飽和

如上所示,我們發現這些延遲峰值完全是由快取驅逐率的峰值引發的,但我們仍然不明白為什麼驅逐會集中在幾秒內持續發生,而且每隔幾分鐘發生一次。

作為第一步,我們需要驗證驅逐突發與延遲峰值之間的因果關係。

為了對此進行測試,我們使用 perf 在 Redis 主執行緒上運行 CPU 取樣剖析。然後對剖析結果進行過濾,找出調用 performEvictions 函數時的樣本。我們使用 flamescope 將剖析的 CPU 使用情況繪製成了亞秒級的偏移熱圖,其中 X 軸上每個柱體表示一秒,分佈在 Y 軸上的格子中,每個格子表示 20 毫秒。這種視覺化風格可以突出顯示亞秒級的活動模式。比較這兩個熱圖可以發現,在驅逐突發期間,CPU 幾乎完全被 performEvictions 佔據,主執行緒上的其他程式碼路徑幾乎沒有佔用任何 CPU 資源。

圖:Redis 主執行緒的 CPU 佔用時間,不包括 performEvictions 的調用

圖:同一份剖析的其餘部分,僅顯示 performEvictions 的調用

這些結果證實,驅逐突發導致主執行緒上的其他任務搶佔不到 CPU 資源,這成為了吞吐量瓶頸,並導致 Redis 的響應時間延遲增加。這些 CPU 利用率爆發通常會持續幾秒鐘,由於持續時間太短,不會觸發警報,但仍然會影響使用者。

作為參考,下面的火焰圖顯示了 performEvictions 消耗 CPU 時間的詳情。注意:

  • performEvictions 的調用與 processCommand(處理所有客戶端請求)同步進行。

  • performEvictions 會開始自己執行刪除。雖然從名稱來看,函數 dbAsyncDelete 是非同步刪除,但它僅在特定條件下將刪除委託給輔助執行緒,而這種情況對於此工作負載來說很少見。

performEvictions 的單次調用速度

Redis 的每個傳入請求都通過調用 processCommand 來處理,並且結束時總是會調用 performEvictions 函數。performEvictions 的調用通常是空操作,在檢查未超過 maxmemory 閾值後立即返回。但是,如果超過閾值,它就會持續驅逐快取鍵,直到達到 mem_tofree 目標值或超過每次調用的時間限制。

前面顯示的 CPU 熱圖證明, performEvictions 調用消耗了大部分 CPU 時間,最多長達幾秒鐘。

作為補充,我們還測量了單詞調用的時鐘時間。

我們使用 funclatency 命令列工具(BPF 工具 BCC 套件的一部分),通過檢測 performEvictions 函數的進入和退出來測量調用持續時間,並將這些測量值以 1 秒為間隔聚合到直方圖中。在沒有發生驅逐時,調用的延遲很低(每次調用 4~7 毫秒)。這是上面介紹的空操作的情況(包括每次調用2.5毫秒的檢測開銷)。但在驅逐爆發期間,結果轉變為雙峰分佈,包括空操作調用(速度非常快)與主動執行驅逐(非常慢)的調用:

$ sudo funclatency-bpfcc --microseconds --timestamp --interval 1 --duration 600 --pid $( pgrep -o redis-server ) '/opt/gitlab/embedded/bin/redis-server:performEvictions'...23:54:03usecs               : count     distribution0 -> 1          : 0        |                                        |2 -> 3          : 576      |************                            |4 -> 7          : 1896     |****************************************|8 -> 15         : 392      |********                                |16 -> 31         : 84       |*                                       |32 -> 63         : 62       |*                                       |64 -> 127        : 94       |*                                       |128 -> 255        : 182      |***                                     |256 -> 511        : 826      |*****************                       |512 -> 1023       : 750      |***************                         |

此次測量還確認並量化了每秒處理的 Redis 請求的吞吐量下降:performEvictions(以及 processCommand)的調用率從驅逐開始前下降到其正常值的 20%,從每秒 2.5 萬次調用下降到5 千次調用。

這對客戶端產生了巨大影響:新請求的到達速度是完成速度的 5 倍。最重要的是,我們很快就會看到這種不對稱是導致驅逐爆發的原因。

實驗:調優能否緩解驅逐導致的 CPU 飽和?

到目前為止的分析表明,驅逐操作消耗了大量 Redis 主執行緒的 CPU 時間。但我們還有一些重要的問題沒有得到解決,但這些資訊足夠我們開展一些實驗來測試潛在的緩解措施:

  • 我們能否分散驅逐開銷,使其花費更長的時間到達目標值,並縮減佔用的主執行緒時間?

  • lazyfree 機制計劃了許多鍵的非同步刪除操作,這是否會導致釋放的記憶體超過預期?Lazyfree 是一項可選功能,允許 Redis 主執行緒將開銷較大的任務委託給非同步輔助執行緒,比如刪除超過 64 個元素的鍵。這些非同步驅逐操作不會被立即計入驅逐循環的記憶體目標,因此如果有很多鍵符合 lazyfree 的條件,就有可能在驅逐循環內進行過多次迭代。

然而,這兩個方法都行不通:

  • 將 maxmemory-eviction-tenacity 降低到最小設定仍然沒能將 performEvictions 的開銷降到足以避免請求累積。它確實提高了響應率,但新請求的到達率仍然遠遠超過了響應率,因此這不是一種有效的緩解措施。

  • 禁用 lazyfree-lazy-eviction 並不能阻止驅逐突發時釋放的記憶體量遠遠超過 maxmemory。這些 lazyfrees 只包含一小部分記憶體回收。這排除了導致記憶體釋放過多的可能原因之一。

在排除了兩種潛在的緩解措施以及一項假設之後,我們回到了核心問題:為什麼在每次驅逐爆發結束時都會額外釋放數百兆位元組的記憶體?

為什麼會出現驅逐突發並釋放過多記憶體?

為什麼會出現驅逐突發並釋放過多記憶體?

每輪驅逐的目的是釋放勉強夠用的記憶體,恢復到 maxmemory 閾值以下。

隨著記憶體分配的需求穩定,驅逐率同樣應該趨於穩定。寫入快取的速率看起來確實很穩定。那麼,為什麼驅逐會密集爆發,而不是平滑地發生?為什麼記憶體使用量突然減少了數百兆位元組,而不是數百位元組?

我們需要探索一些可能性:

  • 驅逐是否僅在大型鍵被逐出時結束,自發地釋放足夠的記憶體,然後停止驅逐一段時間?不,記憶體使用量下降遠大於資料集中最大的鍵。

  • 延遲的 lazyfree 驅逐是否會導致驅逐循環超出其目標,釋放比預期更多的記憶體?不,根據上述實驗,這個假設不成立。

  • 是否有什麼原因導致驅逐循環有時計算出的 mem_tofree 目標是一個超大值?這一點,我們接下來繼續探索。答案是否定的,但這給我們帶來了新的見解。

  • 是否是因為反饋迴路導致驅逐以某種方式自我放大?如果真是這樣,這種狀態發生和停止的條件呢?事實證明這是正確的。

這些都是合理且可檢驗的假設,每個假設都指向解決延遲問題的不同方案。我們已經排除了前兩個假設。

為了測試後兩個,我們構建了自定義 BPF 工具,在每次調用 performEvictions 開始時檢查 mem_tofree 的計算。

使用 bpftrace 觀察 mem_tofree 的計算

我個人最喜歡此次調查的這一部分,這項實驗讓我們對問題的性質有了新的認識。

如上所述,我們剩下的兩個假設是:

  • mem_tofree 的目標是一個超大值

  • 自我放大反饋迴路

為了甄別二者,我們使用 bpftrace 來檢測 mem_tofree 的計算,檢查其輸入變數和結果。

這組測量檢查的是以下內容:

  • 每次調用 performEvictions 是否真的是為了釋放少量記憶體,大約為每個快取條目的平均大小?如果 mem_tofree 接近數百兆位元組,那將證實第一個假設成立,而且還可以揭示哪部分的計算產生了這麼大的一個值。否則,就會排除第一個假設,說明反饋迴路的假設更有可能發生。

  • 複製緩衝區的大小是否會對反饋機制的 mem_tofree 產生很大影響?每次驅逐都會添加到這個緩衝區中,就像正常寫入一樣。如果這個緩衝區變大(可能部分是由於驅逐),然後突然縮小,就會導致記憶體使用量自動大幅下降,同時驅逐結束並造成記憶體使用量即刻減少。這是驅逐推動反饋循環的一種潛在方式。

為了檢查 mem_tofree 的計算(腳本),我們需要單獨取出 performEvictions 調用函數 getMaxmemoryState 的程式碼,並進行逆向工程,找到正確的指令,並檢查每個源程式碼級的變數。根據這些資料,我們生成了以下變數的直方圖:

mem_reported = zmalloc_used_memory() // All used memory tracked by jemallocoverhead = freeMemoryGetNotCountedMemory() // Replication output buffers + AOF buffermem_used = mem_reported - overhead // Non-exempt used memorymem_tofree = mem_used - maxmemory // Eviction goal

注意:我們自定義的 BPF 檢測只能用於 redis-server 的這個構建,因為檢測會附加到特定的虛擬地址上,而不同的Redis構建中這些地址並不一定相同。但這個方法能夠通用化:利用 BPF 在函數調用過程中檢查源程式碼變數,而無需重構二進位制檔案。因為我們查看的是函數的中間狀態,並且因為編譯器內聯了這個函數調用,所以我們需要通過二進位制分析找到正確的檢測點。通常,查看函數的參數或返回值更容易且更易於移植,但在這種情況下這樣做並不能滿足我們的要求。

結果:

  • 排除第一個假設:每次調用 performEvictions 都會產生一個小目標值 (mem_tofree < 2 MB)。這意味著,每次調用 performEvictions 的開銷都很小。Redis 記憶體使用率快速下降的秘密不可能是由異常大的 mem_tofree 目標值一次性驅逐一大批鍵造成的。相反,肯定有許多調用共同導致記憶體使用量降低。

  • 複製輸出緩衝區始終很小,排除了潛在的反饋循環機制之一。

  • 令人驚訝的是,mem_tofree 的大小通常為 16 KB~64 KB,大於常見的快取條目。這種大小差異表明,快取鍵並不是記憶體壓力的主要來源,一旦開始驅逐爆發就會永久存在。

上述所有結果都符合反饋迴路的假設。

除了回答最初的問題之外,我們還得到了一個額外的結果,同時測量 mem_tofree 和 mem_used 時我們還發現了一個重要情況:記憶體回收是一個完全不同於驅逐爆發的階段。

三階段迴路

三階段迴路

上述結果表明,驅逐和記憶體回收之間存在完全獨立,現在我們可以簡單地繪製出由驅逐引發的延遲峰值循環的三個階段。

圖:比較每個階段記憶體和 CPU 的使用率與請求率和響應率

第 1 階段:不飽和(7~15 分鐘)

第 1 階段:不飽和(7~15 分鐘)

  • 記憶體使用量低於maxmemory。此階段不會發生驅逐。

  • 記憶體使用量有機增長,直到達到 maxmemory,進入下一階段。

階段 2:記憶體和 CPU 飽和(6~8 秒)

  • 記憶體使用量達到 maxmemory,驅逐開始。

  • 驅逐只發生在這個階段,而且是間歇性地、頻繁發生。

  • 記憶體的需求常常超過可用容量,反覆將記憶體使用推到 maxmemory 以上。在這個階段,記憶體使用量在 maxmemory 閾值線上來回振盪,一次驅逐少量記憶體,剛好回到 maxmemory 閾值以下。

階段 3:快速回收記憶體(30~60 秒)

  • 此階段不會發生驅逐。

  • 在這個階段,一直持有大量記憶體的進程開始快速穩定地釋放記憶體。

  • 沒有運行驅逐的開銷,CPU 時間再次回到請求處理上(第2個階段積壓的請求)。

  • 記憶體使用量快速穩定地下降。到此階段結束時,已釋放數百兆位元組。接下來,循環回到階段1,重新開始。

從第2個階段向第3個階段過渡時,驅逐突然結束,因為記憶體使用量保持在 maxmemory 閾值以下。

在過渡的某個時間點,記憶體壓力突然降低,這表明,第2個階段消耗記憶體的某個因素開始釋放記憶體,且釋放速度超過了消耗速度,從而降低了前一階段佔用的記憶體空間。

這個神秘的記憶體消費者在第2個階段時的需求不斷膨脹,到了第3個階段卻開始釋放記憶體,它究竟是誰?

謎底揭曉

謎底揭曉

階段轉換建模為我們提供了一些假設必須滿足的約束。這個神秘的記憶體消費者必須滿足以下條件:

  • 在驅逐爆發觸發的條件下,在不到10秒的時間內(第2個階段的持續時間),記憶體的使用量迅速膨脹到數百兆位元組。

  • 在驅逐爆發觸發的條件下,在短短几十秒的時間內(第3個階段的持續時間),快速釋放記憶體。

答案:客戶端輸入/輸出緩衝區滿足這些約束,它就是這個神秘的記憶體消費者。

以下是我們假設的整個經過:

  • 在第1個階段,Redis 主執行緒的 CPU 使用率已很高。進入第2個階段,驅逐開始,驅逐開銷導致主執行緒的 CPU 容量飽和,響應速度迅速下降,且低於請求的傳入速度。

  • 請求的到達速度與響應之間的吞吐量不匹配本身就是導致驅逐突發的放大器。隨著二者的差距擴大,驅逐佔用的時間比例也會增加。

  • 請求積壓造成記憶體需求增長,而積壓的請求會越來越多,直到客戶端停止,請求的到達率下降至與響應率匹配。隨著客戶端停止,請求的到達率下降,記憶體壓力、驅逐率和 CPU 開銷也隨之下降。

  • 當請求的到達率下降匹配響應率的平衡點,記憶體需求得到滿足,並停止驅逐,第2個階段結束。沒有了驅逐的開銷,更多CPU時間用於處理積壓的請求,因此響應率會不斷增加,直到超過請求的到達率。這個恢復階段可以穩步消耗積壓的請求,並逐漸釋放記憶體(第3個階段)。

  • 在請求積壓的問題得到解決後,請求的到達率和響應率會再次匹配。CPU的使用率恢復到第1個階段的標準,記憶體使用量暫時下降,下降速度取決於第2個階段積壓的請求最大值。

我們通過延遲注入實驗證實了這一假設,而且這個結果支持結論:額外的記憶體需求源於響應率低於請求的到達率。

補救措施:如何避免進入驅逐突發循環

補救措施:如何避免進入驅逐突發循環

通過以上調查,我們搞清楚了問題的癥結,下面我們來探索解決方案。

當滿足以下所有條件時,Redis 的驅逐就會自我放大:

  • 記憶體飽和:記憶體使用量達到 maxmemory 限制,導致驅逐開始。

  • CPU 飽和:Redis 主執行緒的正常工作負載消耗的 CPU 接近一個完整的核心,而驅逐開銷將其推向飽和。這將導致響應速率降至請求的到達率以下,請求緩衝的增加導致記憶體需求增加,出現自我放大的效果。

  • 許多活躍的客戶端:只要請求的到達率超過響應率,飽和就會持續。客戶端停止後,請求的到達率不會再增加,但如果 Redis 有許多活動客戶端仍在發送請求,則飽和會持續更長時間並且影響更大。

可行的補救措施包括:

  • 通過以下方式避免記憶體飽和,使記憶體使用量峰值低於 maxmemory 限制:

  • 縮短快取的存活時間(TTL);

  • 增加 maxmemory(並根據需要增加主機的記憶體,但請注意具有多個 NUMA 節點的主機上的 numa_balancing CPU 開銷);

  • 調整客戶端行為,避免寫入不必要的快取條目;

  • 將快取拆分到多個實例上(分片或功能分區,有助於避免記憶體和 CPU 飽和)。

  • 通過以下方式避免 CPU 飽和,使工作負載的 CPU 使用率峰值加上驅逐開銷小於 1 個 CPU 核心:

  • 使用處理單執行緒指令的速度最快的處理器;

  • 將 redis-server 進程(特別是它的主執行緒)與任何其他競爭的 CPU 密集型進程(專用主機、任務集、cpuset)隔離開來;

  • 調整客戶端的行為,避免不必要的快取查找或寫入;

  • 將快取拆分到多個實例上(分片或功能分區,有助於避免記憶體和 CPU 飽和);

  • 減少 Redis 主執行緒的工作(io-threads、lazyfree);

  • 降低驅逐韌性(在我們的實驗中帶來的收益甚微)。

還有一些潛在的補救措施,比如使用 Redis 的新功能。一個思路是,不要將客戶端緩衝區等臨時分配的記憶體計算在 maxmemory 的限制之內,而是隻讓 maxmemory 限制鍵的儲存。還有一種方式,我們可以限制驅逐佔用主執行緒時間的最高比例,這樣主執行緒的大部分時間仍然可用於處理請求,而不是用於驅逐開銷。

不幸的是,這些方法在解決一個故障的同時有可能引發另一個故障,比如降低由於驅逐導致 CPU 飽和的風險,同時有可能導致進程消耗的記憶體增加,從而導致主機或 cgroup 飽和,並引發記憶體不足。兩相權衡下來,孰優孰劣也未可知。

我們的解決方案

我們的解決方案

我們已經最佳化了 CPU 的使用效率,接下來我們的注意力主要放在避免記憶體飽和上。

為了提高快取的記憶體使用效率,我們評估了哪些類型的快取鍵使用的空間最多,以及自最後一次訪問以來它們累積了多少 IDLETIME。根據記憶體使用剖析,我們找到了一些很少使用的快取條目(浪費空間),首先我們來調整處於空閒狀態較多的鍵,並突出顯示一些切入點對快取進行功能分區。

我們決定同時改進多個快取的使用效率。我們的目標是避免慢性記憶體飽和,主要措施包括:

  • 逐步將快取的默認存活時間從 2 周減少到 8 小時(幫助很大!);

  • 將某些快取鍵換到客戶端快取(有效地避免非共享快取條目佔用共享快取空間);

  • 將一組快取鍵分區到一個單獨的 Redis 實例上。

縮減存活時間是最簡單的解決方案,但結果證明幫助很大。對於縮減存活時間,我們最擔心的是快取未命中增加,從而導致基礎設施其他部分的工作量增加。有些快取未命中的開銷特別高,並且我們的指標不夠精細,無法量化每種類型的快取條目未命中時的成本。因此,我們採用迭代的方式,逐步調整存活時間,並嚴格監控 SLO 不達標的情況。幸運的是,我們的推斷是正確的:縮減存活時間並沒有顯著降低快取命中率,快取未命中的增加也沒有對下游子系統造成明顯影響。

事實證明,縮減存活時間足以將記憶體使用量持續降低到其飽和點以下。

剛開始的時候,增加 maxmemory 並沒有任何作用,因為我們預計最初的記憶體需求峰值(在提升效率之前)會超過我們為 Redis 投入的虛擬記憶體。但是,當記憶體需求降低到飽和以下之後,我們就可以為將來的增長情況預留空間,並重新啟用飽和警報。

結果

結果

下圖顯示了 Redis 的記憶體使用擺脫了長期的飽和狀態:

觀察我們調整存活時間的這段時間,可以看到驅逐引發的延遲峰值隨著記憶體使用量降到飽和點以下而消失了:

這些驅逐引發的延遲峰值是導致 Redis 快取異常慢的最大原因。

解決了這個緩慢的根源,使用者體驗顯著改善。下圖中1年的回顧只顯示了改進的長尾部分,未能展示全部收益。每個工作日大約有 200 萬個 Redis 請求的處理時間超過1秒,但在 8 月中旬我們修復這個問題後全面下降:

總結

總結

我們通過不懈的努力,終於解決了一個長期存在的延遲問題,我們在此過程中積累了很多經驗。

總的來說,我們提升了多方面的效率,打破了由快取驅逐引發的一系列週期性的問題。如今,記憶體需求遠低於飽和點,並消除了導致開發團隊預算超標以及使用者經歷間歇性響應延遲峰值。

要點總結

要點總結

下面,總結一下我們學習到的有關 Redis 驅逐行為的知識:

  • 鍵儲存和客戶端連接緩衝區共享同一個記憶體預算(maxmemory)。客戶端連接的緩衝需求激增會計算在maxmemory之內製,方式與插入鍵或鍵大小激增的計算方式相同。

  • Redis 的驅逐在其主執行緒的前臺執行。performEvictions 佔用的是處理客戶端請求的時間,因此,在驅逐突發期間,Redis 的吞吐量上限較低。

  • 如果驅逐開銷導致主執行緒的 CPU 飽和,則響應率會低於請求的到達率,從而導致請求積壓(這會消耗記憶體),而客戶端也會體驗到請求響應減慢。

  • 用於保存待處理請求的記憶體需求增加,導致驅逐爆發,直到大量客戶端停止,請求的到達率回落到響應率以下。當到達平衡點時,驅逐停止,驅逐開銷消失,Redis 快速處理積壓的請求,積壓的請求佔用的記憶體被釋放。

  • 觸發此循環需要滿足以下所有條件:

  • Redis 配置了 maxmemory 限制,且記憶體需求超過了這個限制。記憶體飽和導致驅逐開始。

  • 在正常工作負載下,Redis 主執行緒的 CPU 使用率非常高,驅逐操作使其達到 CPU 飽和。這會導致響應率降低到請求率以下,從而導致請求積壓和高延遲。

  • 許多活動客戶端連接。驅逐突發的持續時間和客戶端連接緩衝區佔用的記憶體大小與活動客戶端的數量成比例增加。

  • 避免記憶體或 CPU 飽和可以防止觸發這種循環。在我們的這個例子中,我們通過縮減存活時間的方式,輕鬆地避免了記憶體飽和的問題。

相關文章

Cluster 集群能支撐的資料有多大?

Cluster 集群能支撐的資料有多大?

作者 | 碼哥位元組 來源 | 碼哥位元組 本文將對集群的節點、槽指派、命令執行、重新分片、轉向、故障轉移、訊息等各個方面進行深入拆解。 目...

GitLab 禁用 Windows!

GitLab 禁用 Windows!

整理 | 鄭麗媛 作為 GitHub 的重要競爭對手,GitLab 自成立以來就一直與其在原始碼庫市場上進行爭奪。尤其當微軟在 2018 年...

CNNVD通報Oracle多個安全漏洞

CNNVD通報Oracle多個安全漏洞

近日,CNNVD通報Oracle多個安全漏洞,其中Oracle產品本身漏洞60個,影響到Oracle產品的其他廠商漏洞247個。包括Orac...