當前位置:
首頁 > 科技 > 一個包含10節點的Redis集群實踐案例

一個包含10節點的Redis集群實踐案例

Redis 通常不會被用作主要的數據存儲,但它在存儲和訪問可容忍丟失的臨時數據(如度量指標、會話狀態、緩存)方面卻獨有長處,並且速度非常快,不僅提供了最佳性能,還內置了一組非常有用的數據結構。它是現代技術棧中最常見的主要部件之一。

Stripe(一家做支付的矽谷創業公司)的速率限定器就是基於 Redis 構建的,這些限速器運行在一個 Redis 實例上。Redis 主伺服器有一些用於失效備援的追隨者,不過在任何時候,都只有一個節點在處理讀寫操作。

各種消息來源聲稱,一個 Redis 節點每秒可以處理百萬次操作。儘管我們的操作沒有那麼多,但也不會很少。每個速率限定器都需要運行多個 Redis 命令,而每個 API 請求都要通過很多個速率限定器。所以,每個節點每秒鐘需要處理數萬次到數十萬次的操作。

一個包含10節點的Redis集群實踐案例

如果節點出現飽和,就會不斷出現故障。我們的服務可以容忍 Redis 的不可用,因此大多數情況下是沒有問題的,但在某些情況下,問題的嚴重程度會升級。我們最後通過遷移到包含 10 節點的 Redis 集群來解決這個問題。對性能的影響可以忽略不計,重要的是現在我們可以實現水平可伸縮。

改造前後的錯誤率比較:

一個包含10節點的Redis集群實踐案例

使用 Redis 集群後錯誤率明顯降低

在更換系統之前,應該先了解導致原始故障的原因。

雖說 Redis 使用了單線程模型,但也並非那麼嚴格,因為後台還是使用了其他線程來處理一些操作,比如刪除對象,不過所有正在執行的操作還是會阻塞在單個控制點上。

要理解這些並非難事——Redis 操作(無論是單一命令、MULTI 還是 EXEC)的原子性保證源於它一次只執行一個操作。即便如此,Redis 還是有可能會採用並行機制,FAQ 中的一些內容表明,5.0 之後的版本有可能考慮採用多線程設計。

單線程模型確實是我們的瓶頸所在,在登錄到原始節點時可以看到,單核的使用率達到了 100%。

我們發現,即使開啟了最大容量,Redis 也會自動優雅地降級。主要表現是,與 Redis 發生交互的節點的基線連接性錯誤率在增加——為了容忍發生故障的 Redis,它們受到連接和讀取超時(約 0.1 秒)方面的限制,並且無法在給定時間內建立用於執行操作的連接。

這種情況在大多數時候是沒有問題的。只有當合法用戶成功通過身份驗證並在底層資料庫上進行昂貴的操作時(也就是說,數量級超過允許的範圍),它才會成為問題。這種昂貴的操作是相對而言的——從列表中返回一組對象比用 401 錯誤來拒絕請求或用 429 錯誤來告知超制都要昂貴得多。這些昂貴的操作通常都是因為用戶運行高並發程序而導致的。

這些流量高峰會導致錯誤率成比例增加,並且很多流量將被允許通過限速器,因為在發生錯誤時,限速器默認允許請求通過。這會給後端資料庫帶來更大的壓力,而且這種壓力所帶來的故障不會像 Redis 的過載故障那麼優雅。我們可以看到,分區幾乎完全不可操作,並且大量請求出現超時。

Redis 集群的分片模型

Redis 的核心價值是速度,而 Redis 集群的分散式結構不會對此產生任何影響。與其他分散式模型不同的是,Redis 集群的操作不需要通過多個節點的確認,它看起來更像是一組獨立的 Redis 實例在分擔工作負載。這就是通過犧牲可用性來換取速度——與 Redis 獨立實例相比,Redis 群集操作的額外開銷可以忽略不計。

鍵空間總共被分為 16384 個槽,槽是通過穩定的散列函數計算出來的,所有客戶端都知道該如何使用這個散列函數:

HASH_SLOT=CRC16(key)mod16384

例如,如果我們想執行 GET foo,會得到 foo 的槽號:

HASH_SLOT=CRC16("foo")mod16384=12182

集群中的每個節點將處理 16384 個槽中的一部分,具體取決於節點數量。節點間通過彼此交互來調節槽的數量、進行可用性轉移和再均衡。

一個包含10節點的Redis集群實踐案例

分布在集群各個節點上的槽

客戶端使用 CLUSTER 系列命令來查詢集群的狀態。CLUSTER NODES 是一個常見的操作,用於獲取槽到節點的映射,其結果通常緩存在本地。

127.0.0.1:30002master-014262383162322connected5461-10922

127.0.0.1:30003master-014262383182433connected10923-16383

127.0.0.1:30001myself,master-001connected0-5460

上面的輸出經過了簡化,最重要的部分是第一列的主機地址和最後一列的數字。5461-10922 表示該節點處理從 5461 到 10922 的槽。

MOVED重定向

如果 Redis 群集中的某個節點接收到一個無法處理的命令,並不會嘗試將該命令轉發給其他節點。相反,客戶會被告知向其他節點嘗試發送該命令。這是通過 MOVED 響應來實現的,MOVED 響應消息包含了新的目標地址:

GETfoo

-MOVED3999127.0.0.1:6381

在集群進行再均衡期間,槽從一個節點遷移到另一個節點,而 MOVED 是伺服器用於告訴客戶端,槽到節點的映射已經發生了變化。

一個包含10節點的Redis集群實踐案例

一個槽從一個節點遷移到另一個節點

每個節點都知道當前的映射關係,理論上,當一個節點在接收到無法處理的操作時,可以向正確的節點請求結果,並將結果轉發回客戶端,但 MOVED 其實是一種有意的設計。它通過將一些額外的複雜性交給客戶端去實現,以便換取更快的速度。只要客戶端的映射是最新的,請求操作總能在一個 hop 之內完成。由於再均衡相對較少出現,因此在群集的使用期間,花在協調上的開銷可以忽略不計。

除了 MOVED 之外,Redis 集群還有其他一些特定的機制,但為了簡潔起見,我將跳過它們。完整的規範(https://redis.io/topics/cluster-spec)是深入了解 Redis 集群工作原理的重要資源。

客戶端如何發送請求

Redis 客戶端需要一些額外的功能來支持 Redis 群集,其中最重要的是要支持鍵的散列演算法和用於維護槽到節點映射的方案,這樣它們就知道往哪裡發送命令。

一般來說,客戶端會這樣操作:

在啟動時,連接到一個節點並獲得一個 CLUSTER NODES 的映射表。

正常執行命令,根據槽和槽映射定位伺服器。

如果收到 MOVED,返回到第一步。

我們可以在客戶端使用多線程進行優化,在收到 MOVED 時將映射表標記為過時,一些線程向新的伺服器發送命令,同時讓後台線程非同步刷新映射表。實際上,即使發生了再均衡,大多數槽也不需要移動,因此該模型允許大多數命令在沒有額外開銷的情況下繼續執行。

使用散列標籤本地化多鍵操作

在 Redis 中,通過 EVAL 命令和自定義 Lua 腳本來運行多鍵操作是很常見的。這是實現速率限定器的一個特別重要的特性,因為通過單個 EVAL 命令分派的操作是原子性的。我們因此能夠正確計算剩餘配額,即使存在可能會發生衝突的並發操作。

分散式模型會讓這種多鍵操作變得十分困難。由於每個鍵對應的槽都是通過散列來計算的,因此不能保證相關鍵都會被映射到同一個槽。比如,user123.first_name 和 user123.last_name 顯然應該是要放在一起的,但最終可能會分布在兩個完全不同的節點上。

舉例來說,我們有一個 EVAL 操作,將姓和名連接起來組合成一個人的全名:

#Getsthefullnameofauser

EVAL"returnredis.call("GET",KEYS[1])..""..redis.call("GET",KEYS[2])"

2"user123.first_name""user123.last_name"

調用示例:

>SET"user123.first_name"William

>SET"user123.last_name"Adama

>EVAL"..."2"user123.first_name""user123.last_name"

"WilliamAdama"

如果 Redis 集群沒有提供這種方式,該腳本將無法正常運行。幸運的是,我們通過使用哈希標籤來運行腳本。

對於需要跨節點操作的 EVAL,Redis 集群會禁止它們(這樣做也是出於速度方面的考慮)。所以,用戶需要確保 EVAL 中的鍵屬於相同的槽,可以通過散列標籤來獲得鍵的散列值。散列標籤就是鍵名字中的花括弧,表示只有花括弧部分用於散列。

我們對鍵進行重新定義,只對 user123 進行散列處理:

>EVAL"..."2"{user123}.first_name""{user123}.last_name"

計算其中一個槽:

HASH_SLOT=CRC16("{user123}.first_name")mod16384

=CRC16("user123")mod16384

=13438

.first_name 和{user123}.last_name 現在映射到了相同的槽,那麼就可以執行 EVAL 操作了。這是一個簡單的例子,不過相同的概念可被用於實現複雜的速率限定器。

遷移到 Redis 集群非常順利,最困難的部分是如何構建一個可用於生產環境的 Redis 集群客戶端。即使到了今天,Redis 客戶端的質量也是參差不齊,可能是因為 Redis 速度足夠快,以至於大多數人直接使用單個實例。

從設計方面看,Redis 集群的設計有很多值得一提的地方——簡單但功能強大。特別是當涉及到分散式系統時,許多實現過程非常複雜,而在生產環境中遇到極端錯誤時,複雜程度可能是災難性的。Redis 集群具備了可伸縮性,卻沒有那麼多令人難以理解的組件,即使像我這樣的門外漢也能明白它的原理。它的設計文檔也很好理解,很接地氣。

在搭建集群之後的幾個月,儘管每時每刻都有相當大的負載,我也沒有再去碰過它。如此高質量的集群實屬罕見。我們需要更多像 Redis 這樣的構建塊,讓它們做它們該做的事,無需我們多作操心。

如果,Google 早已解決不了你的問題。

如果,你還想知道 Apple、Facebook、IBM、阿里等國內外名企的核心架構設計。

來,我們在深圳準備了 ArchSummit 全球架構師峰會,想和你分享:

微信百億消息背後的萬級機器是怎麼做 AI 調度的

滴滴三核心引擎之一的地圖,如何計算路徑規劃和道路匹配

微博如何做萬億級關係的實時協同推薦

微眾區塊鏈首席架構師的兩個具體案例實操

羅輯思維 Go 語言微服務完整改造全過程

阿里菜鳥全球跨域 RPC 架構設計

前特斯拉視覺深度學習負責人帶來的核心技術解析

微服務楷模 Netflix 在 FaaS 上的最新實踐

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 中國存儲 的精彩文章:

安全產品老大不是防火牆,竟然是它:APS
全速旗艦將至 一加6定於5月17日發布

TAG:中國存儲 |