# MVCC機制遺留的問題 **為什麼在可重覆讀級別下,幻讀沒有產生?** 回想一下在事務隔離級別那篇文章中,可串列化是通過什麼保證的? 對操作的每一行記錄加讀鎖、寫鎖和範圍鎖;任何其他事務都必須等待持有鎖的事務釋放鎖之後才能進行操作; 而可重覆讀級別相比之下唯一少的就是範圍鎖,所以無論你是否瞭解過 ...
背景
線上啟用memcached(以下簡稱mc)作為熱點緩存組件已經多年,其穩定性和性能都經歷住了考驗,這裡記錄一下踩過的幾個坑。
大key存儲
某年某月某日,觀察mysql的讀庫CPU占比有些異常偏高,去check慢查詢log,發現部分應有緩存的慢sql居然存在幾秒執行一次情況,不符合緩存數小時的代碼邏輯。
查看業務log在每次查詢sql之後也確實有將結果set至mc之中:
# python代碼
mc.set(cache_key, v, 3600)
而set返回的取值卻是False而非正常的True,很快想到mc著名的只可存儲不超過1MB大小的key限制,在以往的業務場景中沒有出現過這麼大的key,所以一直沒達到過這個限制,直到這一次撞上。
要解決超過1MB大小的key存儲問題有以下幾個思路:
- 想辦法將cache結果變小
- 換個cache組件
- mc >=1.4.2 版本其實已經支持命令行參數-I指定最大key大小了,線上使用版本支持最小1KB最大128MB的設置
- 將大key拆分為幾個子key,通過set_multi和get_multi實現統一的讀寫。
無論是通過2或3都可以支持更大的key存儲,但是更大的key存儲對於讀寫傳輸其實都更不友好,而思路4需要手動拆分、組裝子key略顯麻煩,所以優先從思路1著手,意外發現python使用的memcached庫其實提供了key壓縮功能,在寫入時指定min_compress_len參數即可:
mc.set(key, v, time=expires, min_compress_len=1024)
如上表示寫入的v對象序列化大小若>=1024則啟用壓縮存儲,庫底層會將其壓縮後再寫入mc,讀取時庫底層也會自動解壓縮後再返回,業務層可以說完全無感,並且壓縮後還能極大降低存儲和傳輸成本。
最終通過min_compress_len參數啟用大key壓縮後,原1MB大小的key直瘦身了4/5。
slab鈣化
啟用大key壓縮後mc度過了好一段歲月靜好的日子,直到某一天...
大規模key分佈變動導致的鈣化
查看zabbix上的相關監控,發現mc的key查詢miss比例居然接近50%!這個緩存命中率著實讓人深思,進一步check後發現同時異常的指標還有evicted items數,日常取值居然可以達到數百/S的級別。
mc官方文檔對evicted items的定義如下:
evicted Number of times an item had to be evicted from the LRU before it expired.
即存儲的key在其實際過期前被從LRU強制清理了,這一般說明mc剩餘可分配記憶體不足了,所以新key寫入時只能先從LRU淘汰一部分key騰出空間後再給新key使用,但是查看mc的記憶體使用率,明明還有超過>2GB的剩餘記憶體可用。
最終調查後真相大白:mc明明剩餘大量記憶體可用,寫入新key卻不斷導致舊key被提前清除的現象其實是mc特有的slab鈣化問題所致:
Memcached採用LRU(Least Recent Used)淘汰演算法,在記憶體容量滿時踢出過期失效和LRU數據,為新數據騰出記憶體空間。不過該淘汰演算法在記憶體空間不足以分配新的Slab情況下,這時只會在同一類Slab內部踢出數據。即當某個Slab容量滿,且不能在記憶體足夠分配新的Slab,只會在相同Slab內部踢出數據,而不會挪用或者踢出其他Slab的數據。這種局部剔除數據的淘汰演算法帶來一個問題:Slab鈣化。
簡單來說memcached 使用的不同尺寸slab一旦分配完成就不可變了,所以如果某類slab已用盡,即便其他slab剩餘大量空閑記憶體也無法再對其加以利用。
業務這邊之前對使用mc的部分緩存key進行了整合優化,在優化之前單mc的全部5GB記憶體均已根據key存儲情況分配給了特定的slab,而優化之後大大降低了小key的數量,取而代之的是相對更緊湊的大key,key的數量和大小分佈都發生了顯著的變化,於是原有的適用於大量小key的slab分配就無法滿足優化後的key存儲了。
最終體現為,中等大小的slab記憶體已被耗盡,每次寫入新key只能先通過LRU淘汰部分舊key騰出空間,體現為evicted數異常偏高,並且直接影響了緩存命中率,而小尺寸的slab卻長期大量空閑,體現為mc記憶體使用剩餘空間一直充足。
網上檢索解決鈣化問題有三個辦法:
1) 重啟Memcached實例,簡單粗暴,啟動後重新分配Slab class,但是如果是單點可能造成大量請求訪問資料庫,出現雪崩現象,沖跨資料庫。
2) 隨機過期:過期淘汰策略也支持淘汰其他slab class的數據,twitter工程師採用隨機選擇一個Slab,釋放該Slab的所有緩存數據,然後重新建立一個合適的Slab。
3) 通過slab_reassign、slab_authmove參數控制。
方法2看上去應是twitter的定製版mc Twemcache的特有功能,方法3則是線上mc已支持的方案,但首次接觸也不敢貿然直接線上上使用。
考慮到mc僅作為熱點緩存其數據可丟失,且部署有多台分攤壓力,直接採用低峰時段分別重啟單個mc的策略解決,重啟後evicted item直接降為0,cache命中率升至90%上下。
少量大key變動導致的鈣化
首次鈣化之後又是一段歲月靜好,直到...
某段時間開始一個主要介面偶發耗時會突然飆升一下,對應機器的CPU使用也會瞬間飈高一小陣,查看zabbix監控時,發現mc的 evicted items>0已持續好一段時間,但一直是個位數/S的級別,看著影響不大。
進一步執行stats items
命令,發現發生key evict的是最大的chunk_size=1048576 的slab 42,這也就是說存在大小在512KB~1MB之間的大key,同時當前mc分配的1MB slab個數已無法滿足其存儲,也無法再分配出新的1MB大小的slab,最終體現為對於大key的再次鈣化。
由於slab鈣化大key會被頻繁evict,對應緩存機制基本失效,所幸server端針對該類大key的讀取還做了一個短期的本地cache,避免了每次請求都穿透到db。
在某些特定時刻,當mc中對應大key失效且本地cache失效,對應請求又較多的時候,多個獨立的請求都會穿透到db獲取數據,而後再寫入mc,無論是穿透到db獲取數據後本地進行相應的數據組裝處理邏輯,還是讀寫mc的壓縮、解壓縮數據操作,都比較耗CPU,最終會體現為api耗時增加,且CPU使用率也存在飈高的現象。
近期並沒有涉及大key讀寫的改動,那這次的大key slab鈣化又是怎麼來的?進一步探查原因:觸發evict的大key近期確實無相關邏輯改動,但該部分舊key的大小和運營放出的資源多少直接相關,近一段時間放出的資源一直持續增加,舊key原本大小是<512KB,所以使用的是512KB的slab 41,近期持續增大為>512KB後,就只能使用1MB的slab 42存儲了,對於slab 42來說相當於在原有支持的大key數量基礎上又新的大key存儲需要支持,又由於slab鈣化無法再分配新的slab 42,最終觸發evict,cache命中率降低,api偶發耗時上升。
最終解決方案:還是在業務低峰期逐個重啟mc,觸發slab重分配即可。
總結
memcached作為一個開源的純記憶體kv緩存組件,上手簡單、性能、穩定性都有足夠保證,但是實際使用時也不可掉以輕心,對其相關監控與關註不能少,對於其特有的最大key存儲限制、slab鈣化問題要有一定的認識並能及時處理。
轉載請註明出處,原文地址:https://www.cnblogs.com/AcAc-t/p/memcached_large_key_slab_calcification.html
參考
https://github.com/memcached/memcached/blob/master/doc/protocol.txt#L637
https://github.com/memcached/memcached/wiki/ReleaseNotes142#configurable-maximum-item-size
https://www.jianshu.com/p/b91a45711460
https://blog.twitter.com/engineering/en_us/a/2012/caching-with-twemcache
https://www.cnblogs.com/AcAc-t/p/memcached_large_key_slab_calcification.html
https://bugwz.com/2020/05/24/memcached-slab-calcification/#2-2-2、Rebalance執行邏輯
https://www.cnblogs.com/Leo_wl/p/3310294.html