騰訊百億級請求高可用Redis(codis)分佈式集羣實踐

一、Redis有哪些常用的應用場景

1)string | 計數器,用戶信息(id)映射,唯一性(例如用戶資格判斷),bitmap

 

2)hash | 常見場景:存儲對象的屬性信息(用戶資料)

 

3)list | 常見場景:評論存儲,消息隊列

 

4)set | 常見場景:資格判斷(例如用戶獎勵領取判斷),數據去重等

 

5)sorted set | 常見場景:排行榜,延時隊列

 

6)其他 | 分佈式鎖設計,推薦2篇文章:

  • 基於Redis的分佈式鎖到底安全嗎(上)

    http://zhangtielei.com/posts/blog-redlock-reasoning.html

  • 基於Redis的分佈式鎖到底安全嗎(下)

    http://zhangtielei.com/posts/blog-redlock-reasoning-part2.html

二、Redis選型思考

1、時延

時延=後端發起請求db(用戶態拷貝請求到內核態)+ 網絡時延 + 數據庫尋址和讀取

如果想要降低時延,只能減少請求數(合併多個後端請求)和減少數據庫尋址和讀取得時間。從降低時延的角度,基於單線程和內存的Redis,每秒10萬次得讀寫性能肯定遠遠勝過磁盤讀寫性能。

 

2、數據規模

以Redis一組K-V爲例(”hello” -> “world”),一個簡單的set命令最終會產生4個消耗內存的結構。

 

 

關於Redis數據存儲的細節,又要涉及到內存分配器(如jemalloc),簡單說就是存儲170字節,其實內存分配器會分配192字節存儲。

 

 

那麼總的花費就是:

 

  • 一個dictEntry,24字節,jemalloc會分配32字節的內存塊;

  • 一個redisObject,16字節,jemalloc會分配16字節的內存塊;

  • 一個key,5字節,所以SDS(key)需要5+9=14個字節,jemalloc會分配16字節的內存塊;

  • 一個value,5字節,所以SDS(value)需要5+9=14個字節,jemalloc會分配16字節的內存塊。

 

綜上,一個dictEntry需要32+16+16+16=80個字節。

三、三種Redis分佈式解決方案對比

 

基於以上比較,codis作爲開源產品,可以很直觀的展示出codis運維成本低,擴容平滑最核心的優勢。

對於數據安全目前我們基於機器本機48小時滾動備份加上公司備份(每天定時目錄備份的系統)的兜底備份,對於監控,目前接入monitor單機備份和米格監控告警)。

 

四、Redis分佈式解決方案

 

 

 

如上圖所示,codis整體屬於二層架構,proxy+存儲,相對於ckv+無proxy的設計來說整體設計會相對簡單,同時對於客戶端連接數據逐漸增大的情況下,也不用去做數據層的副本擴容,而只需要做proxy層的擴容,從這一點上看,成本會低一些,但是對於連接數不大的情況下,還需要單獨去部署proxy,從這一點上看,成本會高一些。

 

五、Redis瓶頸和優化

1、HGETALL

 

最終存儲到Redis中的數據結構如下圖:

 

 

採用同步的方式對三個月(90天)進行HGETALL操作,每一天花費30ms,90次就是2700ms!Redis操作讀取應該是ns級別的,怎麼會這麼慢?利用多核cpu計算會不會更快?

 

 

常識告訴我,Redis指令執行速度 >> 網絡通信(內網) > read/write等系統調用。因此這裏其實是I/O密集型場景,就算利用多核cpu,也解決不到根本的問題,最終影響redis性能,**其實是網卡收發數據和用戶態內核態數據拷貝**。

 

2、pipeline

 

這個需求qps很小,所以網卡也不是瓶頸了,想要把需求優化到1s以內,減少I/O的次數是關鍵。換句話說,充分利用帶寬,增大系統吞吐量。

 

於是我把代碼改了一版,原來是90次I/O,現在通過redis pipeline操作,一次請求半個月,那麼3個月就是6次I/O。很開心,時間一下子少了1000ms。

 

 

3、pipeline攜帶的命令數

 

代碼寫到這裏,我不經反問自己,爲什麼一次pipeline攜帶15個HGETALL命令,不是30個,不是40個?換句話說,一次pipeline攜帶多少個HGETALL命令纔會發起一次I/O?

 

我使用是golang的redisgo的客戶端,翻閱源碼發現,redisgo執行pipeline邏輯是 把命令和參數寫到golang原生的bufio中,如果超過bufio默認最大值(4096字節),就發起一次I/O,flush到內核態。

 

 

redisgo編碼pipeline規則如下圖,*表示後面參數加命令的個數,$表示後面的字符長度,一條HGEALL命令實際佔45字節。

 

那其實90天數據,一次I/O就可以搞定了(90 * 45 < 4096字節)!

 

 

果然,又快了1000ms,耗費時間達到了1秒以內

 

 

4、對吞吐量和qps的取捨

 

筆者需求任務算是完成了,可是再進一步思考,Redis的pipeline一次性帶上多少HGETALL操作的key纔是合理的呢?換句話說,服務器吞吐量大了,可能就會導致qps急劇下降(網卡大量收發數據和redis內部協議解析,Redis命令排隊堆積,從而導致的緩慢),而想要qps高,服務器吞吐量可能就要降下來,無法很好的利用帶寬。

 

六、Redis高可用及容災處理

作爲codis的實現來講,數據高可靠主要是Redis本身的能力,通常存儲層的數據高可靠,主要是單機數據高可靠+遠程數據熱備+定期冷備歸檔實現的

 

單機數據高可靠主要是藉助於Redis本身的持久化能力,rdb模式(定期dum)與aof模式(流水日誌),這塊可以參考前文所示的2本書來了解,其中aof模式的安全性更高,目前我們線上也是將aof開關打開,在文末也會詳細描述一下。

 

遠程數據熱備主要是藉助於Redis自身具備主從同步的特性,全量同步與增量同步的實現,讓Redis具體遠程熱備的能力

 

定期冷備歸檔由於存儲服務在運行的過程中可能存在人員誤操作數據,機房網絡故障,硬件問題導致數據丟失,因此我們需要一些兜底方案,目前主要是單機滾動備份備份最近48小時的數據以及sng的劉備系統來做冷備,以備非預期問題導致數據丟失,能夠快速恢復。

 

codis的架構本身分成proxy集羣+Redis集羣,proxy集羣的高可用,可以基於zk或者l5來做故障轉移,而Redis集羣的高可用是藉助於redis開源的哨兵集羣來實現,那邊codis作爲非Redis組件,需要解決的一個問題就是如何集成Redis哨兵集羣。下文將該問題分成三部分,介紹Redis哨兵集羣如何保證Redis高可用,codisproxy如何感知Redis哨兵集羣的故障轉移動作,Redis集羣如何降低“腦裂”的發生概率。

 

哨兵集羣如何保證Redis高可用

 

Sentinel(哨崗,哨兵)是Redis的高可用解決方案:由一個或多個Sentinel實例組成的Sentinel系統,可以監視任意多個主服務器,以及這些主服務器屬下的所有的從服務器,並在被監視的主服務器進入下線狀態時,自動將下線主服務器屬下的某個從服務器升級爲新的主服務器,然後由主服務器代替已下線的主服務器繼續處理命令請求。

 

七、Redis腦裂處理

腦裂(split-brain)集羣的腦裂通常是發生在集羣中部分節點之間不可達而引起的。如下述情況發生時,不同分裂的小集羣會自主的選擇出master節點,造成原本的集羣會同時存在多個master節點。,結果會導致系統混亂,數據損壞。

 

 

由於Redis集羣不能單純的依賴過半選舉的模式,因爲redismaster自身沒有做檢測自身健康狀態而降級的動作,所以我們需要一種master健康狀態輔助判斷降級的方式。具體實現爲:

 

1)降級雙主出現的概率,讓Quorums判斷更加嚴格,讓主機下線判斷時間更加嚴格,我們部署了5臺sentinel機器覆蓋各大運營商IDC,只有4臺主觀認爲主機下線的時候才做下線。

 

2)被隔離的master降級,基於共享資源判斷的方式,Redis服務器上agent會定時持續檢測zk是否通常,若連接不上,則向Redis發送降級指令,不可讀寫,犧牲可用性,保證一致性。

 

八、使用Redis趟過的坑

1、主從切換

每次主從切換之後,都確認一下被切的主或者備機上的conf文件都已經rewriteok。

grep "Generatedby CONFIG REWRITE" -C 10 {redis_conf路徑}/*.conf

 

2、遷移數據

關鍵操作前,備份數據,若涉及切片信息,備份切片信息。

A遷移B時間過長的命令查看:連上Acodisserver,命令行中執行slotsmgrt-async-status查看正在遷移的分片信息(尤其是大key),做到心中有數。千萬級別的key約20秒左右可以遷移完成。

 

3、異常處理

Redis宕機後重啓,重啓之後加載key快加載完時,頁面上報error。

1)原因

可能是宕機後,Redis命令寫入aof,只寫了命令的部分或者事務提交之後只寫入了事務的部分命令導致啓動失敗,此時日誌會aof的異常。

 

2)修復

  • 第一步:備份aof文件

  • 第二步:執行VIP_CodisAdmin/bin中的redis-check-aof --fix appendonly.aof

  • 第三步:重啓

 

4、客戶端出現大量超時

1)網絡原因,聯繫“連線NOC智能助手”,確認鏈路網絡是否出現擁塞。

 

2)觀察視圖,查看監聽隊列是否溢出。全連接隊列的大小取決於:min(backlog, somaxconn) ,backlog是在socket創建的時候傳入的,somaxconn是一個os級別的系統參數,基於命令ss -lnt,觀察監聽隊列目前的長度是否與預期一致,調整參數:vim /etc/sysctl.conf net.core.somaxconn=1024   sysctl -p

 

3)慢查詢,slowlogget,確認是否有耗時操作執行,現網默認是10ms

slowlog-log-slower-than和slowlog-max-len

 

其中注意:慢查詢不包含請求排隊時間,只包含請求執行時間,所以有可能是Redis本身排隊導致的問題,但通過慢查詢可能查不出來。

 

5、fork耗時高

1)原因:當Redis做RDB或AOF重寫時,一個必不可少的操作就是執行fork操作創建子進程,雖然fork創建的子進程不需要拷貝父進程的物理內存空間,但是會複製父進程的空間內存頁表,可以在info stats統計中查latest_fork_usec指標獲取最近一次fork操作耗時,單位(微秒)。

 

2)改善:

  • 優先使用物理機或者高效支持fork操作的虛擬化技術。

  • 控制Redis單實例的內存大小。fork耗時跟內存量成正比,線上建議每個Redis實例內存控制在10GB以內。

  • 適度放寬AOF rewrite觸發時機,目前線上配置:auto-aof-rewrite-percentage增長100%。

 

3)子進程開銷,監控與優化

①cpu

不要和其他CPU密集型服務部署在一起,造成CPU過度競爭。如果部署多個Redis實例,儘量保證同一時刻只有一個子進程執行重寫工作;1G內存fork時間約20ms。

 

②內存

背景:子進程通過fork操作產生,佔用內存大小等同於父進程,理論上需要兩倍的內存來完成持久化操作,但Linux有寫時複製機制(copy-on-write)。父子進程會共享相同的物理內存頁,當父進程處理寫請求時會把要修改的頁創建副本,而子進程在fork操作過程中共享整個父進程內存快照。

Fork耗費的內存相關日誌:AOF rewrite: 53 MB of memory used by copy-on-write,RDB: 5 MB of memory used by copy-on-write

關閉巨頁,開啓之後,複製頁單位從原來4KB變爲2MB,增加fork的負擔,會拖慢寫操作的執行時間,導致大量寫操作慢查詢。

“sudo echo never>/sys/kernel/mm/transparent_hugepage/enabled

 

③硬盤

不要和其他高硬盤負載的服務部署在一起。如:存儲服務、消息隊列。

 

6、不小心執行了flushdb

如果配置appendonlyno,迅速調大rdb觸發參數,然後備份rdb文件,若備份失敗,趕緊跑路。配置了appedonlyyes, 辦法調大AOF重寫參數auto-aof-rewrite-percentage和auto-aof-rewrite-minsize,或者直接kill進程,讓Redis不能產生AOF自動重寫。·拒絕手動bgrewriteaof。備份aof文件,同時將備份的aof文件中寫入的flushdb命令幹掉,然後還原。若還原不了,則依賴於冷備。

 

7、將rdb模式換成aof模式

切不可,直接修改conf,重啓。

正確方式:備份rdb文件,configset的方式打開aof,同時configrewrite寫回配置,執行bgrewriteof,內存數據備份至文件。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章