最近我們在redis集群中發現了乙個有趣的問題。在花費大量時間進行除錯和測試後,通過更改key過期,我們可以將某些集群中的redis記憶體使用量減少25%。
twitter內部執行著多個快取服務。其中乙個是由redis實現的。我們的redis集群中儲存了一些twitter重要的用例資料,例如展示和參與度資料、廣告支出計數和直接訊息。
早在2023年初,twitter的cache團隊就對redis集群的架構進行了大量更新。redis發生了一些變化,其中包括從redis 2.4版到3.2版的更新。在此更新後,出現了幾個問題,例如使用者開始看到記憶體使用與他們的預期或準備使用的記憶體不一致、延遲增加和key清除問題。key的清除是乙個很大的問題,這可能導致本應持久化的資料可能被刪除了,或者請求傳送到資料原始儲存。
受影響的團隊和快取團隊開始進行初步的調查。我們發現延遲增加與現在正在發生的key清除有關。當redis收到寫入請求但沒有記憶體來儲存寫入時,它將停止正在執行的操作,清除key然後儲存新key。但是,我們仍然需要找出導致這些新清除的記憶體使用量增加的原因。
我們懷疑記憶體中充滿了過期但尚未刪除的key。有人建議使用掃瞄,掃瞄的方法會讀取所有的key,並且讓過期的key被刪除。
在redis中,key有兩種過期方式,主動過期和被動過期。掃瞄將觸發key的被動過期,當讀取key時, ttl將會被檢查,如果ttl已過期,ttl會被刪除並且不返回任何內容。redis文件中描述了版本3.2中的key的主動過期。key的主動過期以乙個名為activeexpirecycle的函式開始。它以每秒執行幾次的頻率,執行在乙個稱為cron的內部計時器上。activeexpirecycle函式的作用是遍歷每個金鑰空間,檢查具有ttl集的隨機kry,如果滿足過期kry的百分比閾值,則重複此過程直到滿足時間限制。
這種掃瞄所有kry的方法是有效的,當掃瞄完成時,記憶體使用量也下降了。似乎redis不再有效地使key過期了。但是,當時的解決方案是增加集群的大小和更多的硬體,這樣key就會分布得更多,就會有更多的可用記憶體。這是令人失望的,因為前面提到的公升級redis的專案通過提高集群的效率降低了執行這些集群的規模和成本。
redis版本2.4和3.2之間,activeexpirecycle的實現發生了變化。在redis 2.4中,每次執行時都會檢查每個資料庫,在redis3.2中,可以檢查的資料庫數量達到了最大值。版本3.2還引入了檢查資料庫的快速選項。「slow」在計時器上執行,「fast」 執行在檢查事件迴圈上的事件之前。快速到期週期將在某些條件下提前返回,並且它還具有較低的超時和退出功能閾值。時間限制也會被更頻繁地檢查。總共有100行**被新增到此函式中。
最近我們有時間回過頭來重新審視這個記憶體使用問題。我們想探索為什麼會出現regression,然後看看我們如何才能更好地實現key expiration。我們的第乙個想法是,在redis中有很多的key,只取樣20是遠遠不夠的。我們想研究的另一件事是redi 3.2中引入資料庫限制的影響。
縮放和處理shard的方式使得在twitter上執行redis是獨一無二的。我們有包含數百萬個key的key空間。這對於redis使用者來說並不常見。shard由key空間表示,因此redis的每個例項都可以有多個shard。我們redis的例項有很多key空間。sharding與twitter的規模相結合,建立了具有大量key和資料庫的密集後端。
每個迴圈上取樣的數字由變數
active_expire_cycle_lookups_per_loop
配置。我決定測試三個值,並在其中乙個有問題的集群中執行這三個值,然後進行掃瞄,並測量記憶體使用前後的差異。如果記憶體使用前後的差異較大,表明有大量過期資料等待收集。這項測試最初在記憶使用方面有積極的結果。該測試有乙個控制項和三個測試例項,可以對更多key進行取樣。500和200是任意的。值300是基於統計樣本大小的計算器的輸出,其中總key數是總體大小。在上面的圖表中,即使只看測試例項的初始數量,也可以清楚地看出它們的效能更好。這個與執行掃瞄的百分比的差異表明,過期key的開銷約為25%。
雖然對更多key進行取樣有助於我們找到更多過期key,但負延遲效應超出了我們的承受能力。
上圖顯示了99.9%的延遲(以毫秒為單位)。這表明延遲與取樣的key的增加相關。橙色代表值500,綠色代表300,藍色代表200,控制為黃色。這些線條與上表中的顏色相匹配。
在看到延遲受到樣本大小影響後,我想知道是否可以根據有多少key過期來自動調整樣本大小。當有更多的key過期時,延遲會受到影響,但是當沒有更多的工作要做時,我們會掃瞄更少的key並更快地執行。
這個想法基本上是可行的,我們可以看到記憶體使用更低,延遲沒有受到影響,乙個度量跟蹤樣本量顯示它隨著時間的推移在增加和減少。但是,我們沒有採用這種解決方案。這種解決方案引入了一些在我們的控制項例項中沒有出現的延遲峰值。**也有點複雜,難以解釋,也不直觀。我們還必須針對每個不理想的群集進行調整,因為我們希望避免增加操作複雜性。
我們還想調查redis版本之間的變化。redis新版本引入了乙個名為cron_dbs_per_call的變數。這個變數設定了每次執行此cron時要檢查的最大資料庫數量。為了測試這種變數的影響,我們簡單地注釋掉了這些行。
//if (dbs_per_call > server.dbnum || timelimit_exit)dbs_per_call = server.dbnum;複製**
這會比較每次執行時具有限制的,和沒有限制的檢查所有資料庫兩個方法之間的效果。我們的基準測試結果十分令人興奮。但是,我們的測試例項只有乙個資料庫,從邏輯上講,這行**在修改版本和未修改版本之間沒有什麼區別。變數始終都會被設定。
99.9%的以微秒為單位。未修改的redis在上面,修改的redis在下面。
我們開始研究為什麼注釋掉這一行會產生如此巨大的差異。由於這是乙個if語句,我們首先懷疑的是分支**。我們利用
gcc』s__builtin_expect
來改變**的編譯方式。但是,這對效能沒有任何影響。接下來,我們檢視生成的程式集,以了解究竟發生了什麼。
我們將if語句編譯成三個重要指令mov、cmp和jg。mov將載入一些記憶體到暫存器中,cmp將比較兩個暫存器並根據結果設定另乙個暫存器,jg將根據另乙個暫存器的值執行條件跳轉。跳轉到的**將是if塊或else塊中的**。我取出if語句並將編譯後的程式集放入redis中。然後我通過注釋不同的行來測試每條指令的效果。我測試了mov指令,看看是否存在載入記憶體或cpu快取方面的效能問題,但沒有發現區別。我測試了cmp指令也沒有發現區別。當我使用包含的jg指令執行測試時,延遲會回公升到未修改的級別。在找到這個之後,我測試了它是否只是乙個跳轉,或者是乙個特定的jg指令。我新增了非條件跳轉指令jmp,跳轉然後跳回到**執行,期間沒有出現效能損失。
我們花了一些時間檢視不同的效能指標,並嘗試了cpu手冊中列出的一些自定義指標。關於為什麼一條指令會導致這樣的效能問題,我們沒有任何結論。當執行跳轉時,我們有一些與指令快取緩衝區和cpu行為相關的想法,但是時間不夠了,可能的話,我們會在將來再回到這一點。
既然我們已經很好地理解了問題的原因,那麼我們需要選擇乙個解決這個問題的方法。我們的決定是進行簡單的修改,以便能夠在啟動選項中配置穩定的樣本量。這樣,我們就能夠在延遲和記憶體使用之間找到乙個很好的平衡點。即使刪除if語句引起了如此大幅度的改進,如果我們不能解釋清楚其原因,我們也很難做出改變。
此圖是部署到的第乙個集群的記憶體使用情況。頂線(粉紅色)隱藏在橙色後面,是集群記憶體使用的中值。橙色的頂行是乙個控制項例項。圖表的中間部分是新變化的趨勢。第三部分顯示了乙個正在重新啟動的控制項例項,與淡黃色進行比較。重新啟動後,控制項的記憶體使用量迅速增加。
這是乙個包括工程師和多個團隊的相當大的調查,減少25%的集群大小是乙個非常好的結果,從中我們學到了很多!我們想再看一看這段**,看看在關注效能和調優的其他團隊的幫助下,我們可以進行哪些優化。
其他對這項研究做出重大貢獻的工程師還有mike barry,rashmi ramesh和bart robinson。
- end -
翻譯:許曄
**於:
原創 談談redis的熱key問題如何解決
講了幾天的資料庫系列的文章,大家一定看煩了,其實還沒講完。以下省略一萬字 今天我們換換口味,來寫redis方面的內容,談談熱key問題如何解決。其實熱key問題說來也很簡單,就是瞬間有幾十萬的請求去訪問redis上某個固定的key,從而壓垮快取服務的情情況。其實生活中也是有不少這樣的例子。比如xx明...
如何解決過擬合問題?
如何降低過擬合?這是深度學習中非常重要的問題。關於過擬合的相關概念和過擬合帶來的危害,可參考筆者之前的部落格 過擬合與欠擬合簡要總結。如何解決過擬合?1.獲取和使用更多的資料集 對於解決過擬合的辦法就是給與足夠多的資料集,讓模型在更可能多的資料上進行 觀察 和擬合,從而不斷修正自己。然而事實上,收集...
如何解決過擬合(overfitting)問題?
為了得到一致假設而使假設變得過度嚴格稱為過擬合。過擬合的模型一般對訓練資料表現很好,而對測試資料表現很差。early stopping 可以設定乙個迭代截斷的閾值,到了這個閾值迭代終止 也可以設定兩次迭代之間的accuracy提高很小的時候,停止迭代。l1 l2正則化 使整體cost取較大值,避免某...