1. 程式人生 > 實用技巧 >開源|如何開發一個高效能的redis cluster proxy?

開源|如何開發一個高效能的redis cluster proxy?

文|曹佳俊

網易智慧企業資深服務端開發工程師

背 景

redis cluster簡介

Redis cluster是redis官方提供叢集方案,設計上採用非中心化的架構,節點之間通過gossip協議交換互相的狀態,redis cluster使用資料分片的方式來構建叢集,叢集內建了16384個雜湊槽,每個key都屬於這16384這個雜湊槽中的一個,通過crc16演演算法計算雜湊值,再取餘可得每個key歸屬的雜湊槽;redis cluster支援動態加入新節點,動態遷移slot,自動的故障轉移等。

Redis cluster的架構要求客戶端需要直接與redis叢集中的每個節點建立連線,並且當出現新增節點加入、節點宕機failover、slot遷移等事件時,客戶端需要能夠通過redis cluster協議去更新本地的slot對映表,並且能處理ASK/MOVE語義,因此,我們一般稱實現了redis cluster協議的客戶端為smart redis client

Redis cluster最多可以構建超過100個主節點的叢集(超過之後gossip協議開銷過大,且可能引起叢集不穩定),按照單節點10G容量(單例項記憶體過大可能導致效能下降),單叢集最多可以支撐1T左右的容量。

問 題

Redis cluster有很多優點(比如可以構建大容量叢集,效能好,擴縮容靈活),但是當一些專案工程期望從redis遷移到redis cluster時,客戶端卻面臨著大量的改造工作,與此同時帶來的是需要大量的測試工作以及引入的新風險,這對於一些穩定執行的線上工程代價無疑是巨大的。

需 求

為了更方便的將業務遷移到redis cluster,最期望的是客戶端SDK的API完全相容redis/redis-cluster,

spring提供的RedisTemplate是一個很好實現,但是對於沒有使用SpringRedisTemplate的專案,很多客戶端實現的redis和redis-cluster訪問API是不一致的(比如Java中流行的Jedis),這無形中提高了遷移工作的工作量和複雜性,此時redis cluster proxy是不錯的選擇,有了proxy,就可以像操作單例項redis一樣操作redis cluster,客戶端程式就不需要做任何的修改。

當然,增加一層proxy,必然會導致效能有一定程度的下降,但是proxy作為無狀態的服務,理論上可以水平擴充套件,並且由於proxy層的存在減少了後端redis server的連線數,在某些極限場景下甚至能提高redis叢集整體的吞吐量。此外,基於proxy,我們還可以做很多額外的事情:

  • 比如可以在proxy層做分片邏輯,這樣當單叢集的redis cluster不滿足需求(記憶體/QPS)時,就可以通過proxy層實現透明的同時訪問多個redis cluster叢集。
  • 再比如可以在proxy層做雙寫邏輯,這樣在遷移或者拆分快取型別的redis時,就不需要使用redis-migrate-tool之類的工具進行全量遷移,而只需要按需雙寫,即可完成遷移。
  • 此外因為proxy實現了redis協議,因此可以在proxy層利用其它儲存介質實現redis相關命令,從而可以模擬成redis對外服務。一個典型的場景就是冷熱分離儲存。

    功 能

介於上述各種原因和需求,我們基於netty開發了camellia-redis-proxy這樣一箇中介軟體,支援如下特性

  • 支援設定密碼
  • 支援代理到普通redis,也支援代理到redis cluster
  • 支援配置自定義的分片邏輯(可以代理到多個redis/redis-cluster叢集)
  • 支援配置自定義的雙寫邏輯(伺服器會識別命令的讀寫屬性,配置雙寫之後寫命令會同時發往多個後端)
  • 支援外部外掛,從而可以複用協議解析模組(當前包括camellia-redis-proxy-hbase外掛,實現了zset命令的冷熱分離儲存)
  • 支援線上變更配置(需引入camellia-dashboard)
  • 支援多個業務邏輯共享一套proxy叢集,如:A業務配置轉發規則1,B業務配置轉發規則2(需要在建立redis連線時通過client命令設定業務型別)
  • 對外提供了一個spring-boot-starter,3行程式碼即可快速搭建一個proxy叢集

如何提升效能?

客戶端向camellia-redis-proxy發起一條請求,到收到請求回包的過程中,依次經歷瞭如下過程:

  • 上行協議解析(IO讀寫)
  • 協議轉發規則匹配(記憶體計算)
  • 請求轉發(IO讀寫)
  • 後端redis回包解包(IO讀寫)
  • 後端redis回包下發到客戶端(IO讀寫)

可以看到作為一個proxy,大量的工作是在進行網路IO的操作,為了提升proxy的效能,做了以下工作:

多執行緒

我們知道redis本身是單執行緒的,但是作為一個proxy,完全可以使用多執行緒來充分利用多核CPU的效能,但是過多的執行緒引起不必要的上下文切換又會引起效能的下降。camellia-redis-proxy使用了netty的多執行緒reactor模型來確保伺服器的處理效能,預設會開啟cpu核心數的work執行緒。 此外,如果伺服器支援網路卡多佇列,開啟它,能避免CPU不同核心之間的load不均衡;如果不支援,那麼將業務程式綁核到非CPU0的其他核心,從而讓CPU0專心處理網路卡中斷而不被業務程式過多的影響。

非同步非阻塞

非同步非阻塞的IO模型一般情況下都是優於同步阻塞的IO模型,上述5個過程中,除了協議轉發規則匹配這樣的記憶體計算,整個轉發流程都是異步非阻塞,確保不會因為個別流程的阻塞影響整個服務。

流水線

我們知道redis協議支援流水線(pipeline),pipeline的使用,可以有效減少網路開銷。camellia-redis-proxy也充分利用了這樣的特性,主要包括兩方面:

  • 上行協議解析時儘可能的一次性解析多個命令,從而進行規則轉發時可以批量進行
  • 往後端redis節點進行轉發時儘可能的批量提交,這裡除了對來自同一個客戶端連線的命令進行聚合,還可以對來自不同客戶端連線,但轉發目標redis相同時,也可以進行命令聚合

當然,所有這些批量和聚合的操作都需要保證請求和響應的一一對應。

TCP分包和大包處理

不管是上行協議解析,還是來自後端redis的回包,特別是大包的場景,在碰到TCP分包時,利用合適的checkpoint的機制可以有效減少重複解包的次數,提升效能

異常處理和異常日誌合併

如果沒有有效的處理各種異常,在異常發生時也會導致伺服器效能迅速下降。想象一個場景,我們配置了90%的流量轉發給A叢集,10%的流量轉發到B叢集,如果B叢集發生了宕機,我們期望的是來自客戶端的90%的請求正常執行,10%的請求失敗,但是實際上卻可能遠遠超過10%的請求都失敗了,原因是多方面的:

  • 後端作業系統層面的突然宕機proxy層可能無法立即感知(沒有收到TCP fin包),導致大量請求在等待回包,雖然proxy層沒有阻塞,但是客戶端表現為請求超時
  • proxy在嘗試轉發請求到B叢集時,針對B叢集的重新連線請求可能拖慢整個流程
  • 宕機導致的大量異常日誌可能會引起伺服器效能下降(這是一個容易忽視的地方)
  • pipeline提交上來的請求,99個請求指向A叢集,1個請求指向B叢集,但是由於B叢集的不可用,導致指向B叢集的請求遲遲不回包或者異常響應過慢,客戶端的最終表現是100個請求全部失敗了

camellia-redis-proxy在處理上述問題時,採取瞭如下策略:

  • 設定對異常後端節點的快速失敗降級策略,避免拖慢整個服務
  • 異常日誌統一管理,合併輸出,在不丟失異常資訊的情況下,減少異常日誌對伺服器效能的影響
  • 增加對後端redis的定時探活探測,避免宕機無法立即感知導致業務長時間異常

    部署架構

proxy作為無狀態的服務,可以做到水平擴充套件,為了服務的高可用,也至少要部署兩個以上的proxy節點,對於客戶端來說,想要像使用單節點redis一樣訪問proxy,可以在proxy層之前設定一個LVS代理服務,此時,部署架構圖如下:

當然,還有另外一個方案,可以將proxy節點註冊到zk/Eureka/Consul等註冊中心,客戶端通過拉取和監聽proxy的列表,然後再向訪問單節點redis一樣訪問每個proxy即可。以Jedis為例,僅需將JedisPool替換為封裝了註冊發現邏輯的RedisProxyJedisPool,即可像訪問普通redis一樣使用proxy了,此時,部署架構圖如下

應用場景

  • 需要從redis遷移到redis-cluster,但是客戶端程式碼不方便修改
  • 客戶端直連redis-cluster,導致cluster伺服器連線過多,導致伺服器效能下降
  • 單個redis/redis-cluster叢集容量/QPS不滿足業務需求,使用camellia-redis-proxy的分片功能
  • 快取類redis/redis-cluster叢集拆分遷移,使用camellia-redis-proxy的雙寫功能
  • 使用雙寫功能進行redis/redis-cluster的災備
  • 混合使用分片和雙寫功能的一些業務場景
  • 基於camellia-redis-proxy的外掛功能,開發自定義外掛

結 語

Redis cluster作為官方推薦的叢集方案,越來越多的專案已經或正在遷移到redis cluster,camellia-redis-proxy正是在這樣的背景下誕生的;特別的,如果你是一個Java開發者,camellia還提供了CamelliaRedisTemplate這樣的方案,CamelliaRedisTemplate擁有和普通Jedis一致的API,提供了mget/mset/pipeline等原生JedisCluster不支援的特性,且提供了和camellia-redis-proxy功能一致的分片/雙寫等特性。

為了回饋社群,camellia已經正式開源了,想詳細瞭解camellia專案的請點選【閱讀原文】訪問github,同時附上地址:

https://github.com/netease-im...

如果你有什麼好的想法或者提案,或者有什麼問題,歡迎提交issue與我們交流!

關於作者

曹佳俊。網易智慧企業資深服務端開發工程師。中科院研究生畢業後加入網易,一直在網易雲信負責IM伺服器相關的開發工作。

作者:網易雲信
連結:https://segmentfault.com/a/1190000023210717
來源:SegmentFault 思否
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。