目錄
前言
客戶端交換時的阻塞
redis 磁盤交換的阻塞
主從節點交互的阻塞
切片集群交互時的阻塞
異步執行的演變
redis 異步執行如何實現的
前言
大家對redis 比較熟悉吧,只要做項目都會用到redis,提高系統的吞吐。小米商城搶購高峰18k的qps,redis 在其中扮演著非常重要的角色。有時我們操作不當,redis 阻塞了,影響了整個業務。我記得2018年的時候,順豐就出現了一個事故,有同學在線上對redis的一個操作,直接阻塞了,影響公司的整個業務,后面這位同學被辭退了。如果他對redis比較了解的話,也不會出現這樣的事故。
redis 為什么會阻塞,與它的本身設計有一定關系。大家都知道redis 是單線程的,這個并完全正確,只是說我們對數據的讀寫是在主線程中完成的。但RDB,aof 重寫,刪除都是在子線程完成的。我們說的阻塞就是阻塞的主線程,redis 為什么會這么設計,抽時間我會詳細講解。這篇文章我主要從客戶端,磁盤,主從節點,切片集群實例 多方面取闡述這個問題。
客戶端交換時的阻塞
客戶端的功能,與redis 服務器建立連接就有對網絡IO的影響,對數據庫的增刪改查操作。redis 采用的是多路復用機制,避免了主線程一直處在等待網絡連接或請求到來的狀態,所以,網絡 IO 不是導致 Redis 阻塞的因素。剩下的也就是增刪改查對redis 的影響,這部分功能也是redis 主要的任務,復雜度高的肯定會阻塞redis。
那么怎么判斷操作復雜度高不高?就是看復雜度的O(N),這個復雜度也可以看作是空間復雜度。N越大復雜度越高,越容易阻塞。我們可以對照redis 官網的命令,進行查看。一般集合的操作都是O(N) 的復雜度。比如HGETALL,SMEMBERS,以及集合的聚合統計操作,交,并,差集。這些操作特殊是集合的全量查詢和聚合操作。
我們看了查詢,刪除會不會造成redis 阻塞了。當然會了,刪除操作的本質是釋放鍵值對占用的內存空間。它不僅釋放內存空間,在Redis 釋放內存時,操作系統會把釋放掉的內存塊插入到一個空閑內存塊鏈表,以便后續進行管理和分配。這個過程需要時間并且會阻塞當前釋放內存的應用程序。元素數量越大,這個操作消耗的時間越多,越容易阻塞,這就是我們所說的bigkey 刪除。
還有就是清空數據庫的操作例如( FLUSHDB 和 FLUSHALL 操作)必然也是一個潛在的阻塞風險,因為它涉及到刪除和釋放所有的鍵值對。
redis 磁盤交換的阻塞
磁盤的操作一直是系統的瓶頸,一次讀盤需要經過尋道,旋轉,傳輸 這么復雜的操作。很多數據庫都用了各種技術來避免經常操作磁盤,比如mysql 用了WAL技術,關于這方面技術描述,可以閱讀我的普通索引和唯一索引詳解。redis 最大的賣點還是還性能,它保證的是操作的高效性,就沒有用這么復雜的技術。redis 與磁盤操作的功能都是放在子線程操作,這樣就避免了主線程的阻塞。redis 生成的 RDB 快照,aof 日志重寫。但是有一個文件比較特殊,aof 日志,會根據不同寫回策略做落盤保存。一個同步寫磁盤的操作的耗時大約是 1~2ms,如果有大量的寫操作需要記錄在 AOF 日志中,并同步寫回的話,就會阻塞主線程了。大家可以看到redis 磁盤交換的阻塞主要發生在aof 日志同步寫。
主從節點交互的阻塞
一般的架構都是一主多從,主節點寫,從節點讀。主從同步的大概過程是,主節點身材RDB 快照,這個操作上面已經講過,是通過子線程生成的,不會阻塞。對于從節點來說就是兩個步驟,一個是FLUSHDB 清空當前數據庫,這個肯定會阻塞,從節點的redis 會回放RDB 的數據,這個操作會阻塞從節點。加載RDB 文件也會阻塞,最終影響的從節點的讀。
切片集群交互時的阻塞
使用 Redis Cluster 作為集群方案,當我們增加實例或刪除實例時,數據會在不同實例進行遷移。一般會是漸進式的遷移,如果遇到bigkey 會阻塞節點
異步執行的演變
綜上所述:我們講到了幾個阻塞點:集合全量查詢和聚合操作、bigkey 刪除、FLUSHDB 和 FLUSHALL 、AOF 日志同步寫,從庫加載RDB 文件。這些都是Redis 的性能的瓶頸,這些也一直困擾著redis 的開發人員。隨著版本的遞進,發生了一些變化。有些已經放在子線程去執行了,就意味著,它并不是 Redis 主線程的關鍵路徑上的操作。
那么什么是主線程的關鍵路徑上的操作:這就是說,客戶端把請求發送給 Redis 后,等著 Redis 返回數據結果的操作,比如獲取數據,進行接下來的業務。
按照這個定義來說的話:集合全量查詢和聚合操作依舊是關鍵路徑上的操作,依舊會阻塞,這個是無法避免的。
bigkey 刪除、FLUSHDB 和 FLUSHALL 并不需要給客戶端返回具體數據結果,不算關鍵路徑上的操作。它們算不算阻塞點呢,這個其實挺復雜的。這個叫做惰性刪除(lazy free),這個功能 Redis 4.0 以后才有的功能,并不是所有的key 都能異步刪除。
關于異步刪除我需要補充幾點,希望大家做個理性的判斷:
-
lazy-free是4.0新增的功能,但是默認是關閉的,需要手動開啟。
-
手動開啟lazy-free時,有4個選項可以控制,分別對應不同場景下,要不要開啟異步釋放內存機制:
a) lazyfree-lazy-expire:key在過期刪除時嘗試異步釋放內存
b) lazyfree-lazy-eviction:內存達到maxmemory并設置了淘汰策略時嘗試異步釋放內存
c) lazyfree-lazy-server-del:執行RENAME/MOVE等命令或需要覆蓋一個key時,刪除舊key嘗試異步釋放內存
d) replica-lazy-flush:主從全量同步,從庫清空數據庫時異步釋放內存
-
即使開啟了lazy-free,如果直接使用DEL命令還是會同步刪除key,只有使用UNLINK命令才會可能異步刪除key 而 FLUSHDB ASYNC、FLUSHALL AYSNC 才會異步清空庫
-
這也是最關鍵的一點,上面提到開啟lazy-free的場景,除了replica-lazy-flush之外,其他情況都只是可能去異步釋放key的內存,并不是每次必定異步釋放內存的。 開啟lazy-free后,Redis在釋放一個key的內存時,首先會評估代價,如果釋放內存的代價很小,那么就直接在主線程中操作了,沒必要放到異步線程中執行(不同線程傳遞數據也會有性能消耗)。 什么情況才會真正異步釋放內存?這和key的類型、編碼方式、元素數量都有關系(詳細可參考源碼中的lazyfreeGetFreeEffort函數):
a) 當Hash/Set底層采用哈希表存儲(非ziplist/int編碼存儲)時,并且元素數量超過64個
b) 當ZSet底層采用跳表存儲(非ziplist編碼存儲)時,并且元素數量超過64個
c) 當List鏈表節點數量超過64個(注意,不是元素數量,而是鏈表節點的數量,List的實現是在每個節點包含了若干個元素的數據,這些元素采用ziplist存儲) 只有以上這些情況,在刪除key釋放內存時,才會真正放到異步線程中執行,其他情況一律還是在主線程操作。 也就是說String(不管內存占用多大)、List(少量元素)、Set(int編碼存儲)、Hash/ZSet(ziplist編碼存儲)這些情況下的key在釋放內存時,依舊在主線程中操作。 可見,即使開啟了lazy-free,String類型的bigkey,在刪除時依舊有阻塞主線程的風險。所以,即便Redis提供了lazy-free,我建議還是盡量不要在Redis中存儲bigkey。
個人理解Redis在設計評估釋放內存的代價時,不是看key的內存占用有多少,而是關注釋放內存時的工作量有多大。從上面分析基本能看出,如果需要釋放的內存是連續的,Redis作者認為釋放內存的代價比較低,就放在主線程做。如果釋放的內存不連續(大量指針類型的數據),這個代價就比較高,所以才會放在異步線程中去執行。
所以我雖然在以后的版本刪除有可能是異步刪除,還是不要存儲bigkey,對bigkey 進行刪除。
如果真的要對bigkey 刪除呢,我給你個小建議:先使用集合類型提供的 SCAN 命令讀取數據,然后再進行刪除。因為用 SCAN 命令可以每次只讀取一部分數據并進行刪除,這樣可以避免一次性刪除大量 key 給主線程帶來的阻塞。
例如,對于 Hash 類型的 bigkey 刪除,你可以使用 HSCAN 命令,每次從 Hash 集合中獲取一部分鍵值對(例如 200 個),再使用 HDEL 刪除這些鍵值對,這樣就可以把刪除壓力分攤到多次操作中,那么,每次刪除操作的耗時就不會太長,也就不會阻塞主線程了。
AOF 日志呢,如果 AOF 日志配置成 everysec 選項后,也不會去阻塞,異步執行。
從庫加載RDB 文件 呢,這個在從庫上需要在主線程執行,這個是不能異步的。為了避免阻塞,在這個地方給大家一個建議:從庫加載 RDB 文件:把主庫的數據量大小控制在 2~4GB 左右,以保證 RDB 文件能以較快的速度加載。
redis 異步執行如何實現的
有些操作需要異步執行,redis 主線程通過一個鏈表形式的任務隊列和子線程進行交互,等到后臺子線程從任務隊列中讀取任務進行操作。
好了就講到這些了,其他的希望大家補充