Categories
程式開發

騰訊會議用戶暴漲,Redis集群如何實現無縫擴容?


一、疫情帶來的挑戰

今年疫情帶來的挑戰很明顯,遠程辦公和在線教育用戶暴漲,從1月29到2月6日,日均擴容1.5w台主機。業務7×24小時不間斷服務,遠程辦公和在線教育要求不能停服,停服一分鐘都會影響成百上千萬人的學習和工作,所以這一塊業務對於我們的要求非常高。

在線會議和遠程辦公都大量使用了redis,用戶暴增的騰訊會議背後也有騰訊雲Redis提供支持,同時海量請求對redis的快速擴容能力提出了要求。我們有的業務實例,從最開始的3片一天之內擴容到5片,緊接著發現還是不夠,又擴到12片,第二天繼續擴。

騰訊會議用戶暴漲,Redis集群如何實現無縫擴容? 1

二、開源Redis擴容方案

1. 騰訊雲Redis的集群版架構

騰訊雲Redis跟普遍Redis有差別,我們加入了Proxy,提高了系統的易用性,這個是因為不是所有的語言都支持集群版客戶端。

為了兼容這部分客戶,我們做了很多的兼容性處理,能夠兼容更多普通客戶端使用,像做自動的路由管理,切換的時候可以自由處理MOVE和ASK,增加端到端的慢查詢統計等功能,Redis默認的slowlog只包含命令的運算時間,不包括網絡來回的時間,不包括本地物理機卡頓導致的延時,Proxy可以做端到端的慢日誌統計,更準確反應業務的真實延遲。

對於多帳戶,Redis不支持,現在把這部分功能也挪到Proxy,還有讀寫分離,這個對於客戶非常有幫助,客戶無須敲寫代碼,只需要在平台點一下,我們在Proxy自動實現把讀寫派發上去。

這一塊功能放到Redis也是可以,那為什麼做到Proxy呢?主要是考慮安全性!因為Redis承載用戶數據,如果在Redis做數據會頻繁升級功能迭代,這樣對於用戶數據安全會產生比較大的威脅。

2. 騰訊雲Redis如何擴容

騰訊雲Redis怎麼擴容呢?我們的擴容從三個維度出發,單個節點容量擴容, 比如說三分片,每個片4G,我們可以每節點擴到8G。單節點容量擴容,一般來說只要機器容量足夠,就可以擴容上去。

還有副本擴容,現在客戶使用的是一主一從,有的同學開讀寫分離,把讀全部打到從機,這種情況下,增加讀qps比較簡單的方法就是增加副本的數量,還增加了數據安全性。

最近的實際業務,我們遇到的主要是擴分片。

對於集群分片數,最主要就是CPU的處理能力,擴容分片就是相當於擴展CPU,擴容處理能力也間接擴容內存。

騰訊會議用戶暴漲,Redis集群如何實現無縫擴容? 2

最早騰訊雲做過一個版本,利用開源的原生版的擴容方式擴容。

簡單描述一下操作步驟:首先Proxy是要做slot容量計算,否則一旦搬遷過去,容易把新分片的內存打爆。

計算完每個slot內存後,按照算法分配,決定好目標分片有哪些slot。

先設置目標節點slot 為importing狀態 ,再設置源節點的slot為 migrating狀態。

這裡存在一個小坑,在正常開發中這兩個設置大家感覺順序無關緊要,但是實際上有影響。

如果先設置源節點的slot為migrating,再設置目標節點的slot為importing,那麼在這兩條命令的執行間隙,如果有對應slot的命令打到源節點,而key又恰好不存在,則會重定向到目標節點,由於目標節點此時slot並未來得及設置為importing, 又會把這條命令重定向給源節點,這樣就無限重定向了。

好在常見的客戶端(比如jedis)對重定向次數是有限制的, 一旦打到上限,就會拋出錯誤。

(1)準備

騰訊會議用戶暴漲,Redis集群如何實現無縫擴容? 3

(2)搬遷

設置完了這一步,下一步就是搬遷。從源節點來獲取slot的搬遷,從源進程慢慢逐個搬遷到目標節點。

此操作是同步的,什麼意思呢?在migrate命令結束之前進程不能直接處理客戶請求,實際上是源端臨時創建一個socket,連接目標節點,同步執行命令,確認執行成功了後,把本地的Key刪掉,這個時候源端才可以繼續處理客戶新的請求。

在搬遷過程中,整個集群仍然是可以處理請求的。這一塊開源Redis有考慮,如果這個時候有Key讀請求,剛好這個slot發到源進程,進程可以判斷,如果這個Key在本進程有數據,就會當正常的請求返回給它。

那如果不存在怎麼辦?就會發一個ASK給客戶,用戶收到ASK知道這個數據不在這個進程上,馬上重新發一個ASKING到目標節點,緊接著把命令發到那邊去。

這樣會有一個什麼好處呢?源端的Key的slot只會慢慢減少,不會增加,因為新增加的都發到目標節點去了。隨著搬遷的持續,源端的Key會越來越少,目標端的key逐步增加,用戶感知不到錯誤,只是多了一次轉發延遲,只有零點零幾毫秒,沒有特別明顯的感知。

騰訊會議用戶暴漲,Redis集群如何實現無縫擴容? 4

(3)切換

方案到什麼時候切換呢?就是slot源進程發現這個slot已經不存在數據了,說明所有數據全部搬到目標進程去了。

這個時候怎麼辦呢?先發送set slot給目標,然後給源節點發送set slot命令,最後給集群所有其他節點發送set slot。這是為了更快速把路由更新過去,避免通過自身集群版協議推廣,降低速度。

這裡跟設置遷移前的準備步驟是一樣,也有一個小坑需要注意。

如果先給源節點設置slot,源節點認為這個slot歸屬目標節點,會給客戶返回move,這個是因為源節點認為Key永久歸屬目標進程,客戶端搜到move後,就不會發ASKing給目標,目標如果沒有收到ASK,就會把這個消息重新返回源進程,這樣就和打乒乓球一樣,來來回回無限重複,這樣客戶就會感覺到這裡有錯誤。

騰訊會議用戶暴漲,Redis集群如何實現無縫擴容? 5

三、 無損擴容挑戰

1. 大Key問題

像這樣遷移其實也沒有問題,客戶也不會感覺到正常的訪問請求的問題。

但是依然會面臨一些挑戰,第一就是大Key的問題。前文提到的搬遷內部,由於這個搬遷是同步的搬遷,同步搬遷會卡住,這個卡住時間由什麼決定的?

主要不是網速,而是搬遷Key的大小來決定的,由於搬遷是按照key來進行的,一個list也是一個Key,一個哈希表也是Key,一個list會有上千萬的數據,一個哈希表也​​會有很多的數據。

同步搬遷容易卡非常久,同步搬遷100兆,打包有一兩秒的情況,客戶會覺得卡頓一兩秒,所有訪問都會超時,一般Redis業務設置超時大部分是200毫秒,有的是100毫秒。如果同步搬移Key超過一秒,就會有大量的超時出現,客戶業務就會慢。

如果這個卡時超過15秒,這個時間包括搬遷打包時間、網絡傳輸時間、還有loading時間。超過15秒,甚至自動觸發切換,把Master判死,Redis會重新選擇新的Master,由於migrating狀態是不會同步給slave的,所以slave切換成master後,它身上是沒有migrating狀態的。

然後,正在搬遷的目標節點會收到新的master節點對這個slot的所有權聲明, 由於這個slot是importing的,所以它會拒絕承認新master擁有這個slot。從而在這個節點看來,slot的覆蓋是不全面的, 有的slot無節點提供服務,集群狀態為fail。

一旦出現這種情況,假如客戶繼續在寫,由於沒有migrating標記了,新Key會寫到源節點上,這個key可能在目標節點已經有了,就算人工處理,也會出現哪一邊的數據比較新, 應該用哪一邊的數據, 這樣的一些問題,會影響到用戶的可用性和可靠性。

這是整個開源Redis的核心問題,就是容易卡住,不提供服務,甚至影響數據安全。

開源版如何解決這個問題呢?老規矩:惹不起就躲,如果這個slot有最大Key超過100M或者200M的閾值不搬這個slot。

這個閾值很難設置,由於migrate命令一次遷移很多個key,過​​小的閾值會導致大部分slot遷移不了,過大的閾值還是會導致卡死,所以無論如何對客戶影響都非常大,而且這個影響是不能被預知的,因為這個Key大小可以從幾k到幾十兆,不知道什麼時候搬遷到大key就會有影響,只要搬遷未結束,客戶在相當長時間都心驚膽戰。

騰訊會議用戶暴漲,Redis集群如何實現無縫擴容? 6

2. Lua問題

除了Key的整體搬遷有這樣問題以外,我們還會有一個問題就是Lua。

假如業務啟動的時候通過script load加載代碼,執行的時候使用evalsha來,擴容是新加了一個進程,對於業務是透明,所以按照Redis開源版的辦法搬遷Key,key搬遷到目標節點了,但是lua代碼沒有,只要在新節點上執行evalsha,就會出錯。

這個原因挺簡單,就是Key搬遷不會遷移代碼,而且Redis沒有命令可以把lua代碼搬遷到另外一個進程(除了主從同步)。

這個問題在開源版是無解,最後業務怎麼做才能夠解決這個問題呢?

需要業務那邊改一下代碼,如果發現evalsha執行出現代碼不存在的錯誤,業務要主動執行一個script load,這樣可以規避這個問題。

但是對很多業務是不能接受的。

因為要面臨一個重新加代碼然後再發布這樣一個流程,業務受損時間是非常長的。

騰訊會議用戶暴漲,Redis集群如何實現無縫擴容? 7

3. 多Key命令/Slave讀取

還有一個挑戰,就是多Key命令,這個是比較嚴重的問題,Mget和mset其中一個Key在源進程,另外一個Key根本不存在或者在目標進程,這個時候會直接報錯,很多業務嚴重依賴於mget的準確性,這個時候業務是不能正常工作的。

這也是原生版redis沒有辦法解決的問題,因為只要是Key搬遷,很容易出現mget的一部分key在源端,一部分在目標端。

除了這個問題還有另一個問題,這個問題跟本身分片擴容無關,但是開源版本存在一個bug,就是我們這邊Redis是提供了一個讀寫分離的功能,在Proxy提供這個功能,把所有的命令打到slave,這樣可以降低master的性能壓力。

這個業務用得很方便,業務想起來就可以開啟,發現不行就可以馬上關閉。

這裡比較明顯的問題是:當每個分片數據比較大的時候,舉一個例子20G、30G的數據量的時候,我們剛開始掛slave,slave身份推廣跟主從數據是兩個機制,可能slave已經被集群認可了,但是還在等master的數據,因為20G數據的打包需要幾分鐘(和具體數據格式有關係)。

這個時候如果客戶的讀命令來到這個slave, 會出現讀不到數據返回錯誤, 如果客戶端請求來到的時候rdb已經傳到slave了,slave正在loading, 這個時候會給客戶端回loading錯誤。

這兩個錯誤都是不能接受,客戶會有明顯的感知,擴容副本本來為了提升性能,但是結果一擴反而持續幾分種到十幾分鐘內出現很多業務的錯誤。

這個問題其實是跟Redis的基本機制:身份推廣機制、主從數據同步機制有關,因為這兩個機制是完全獨立的,沒有多少關係,問題的解決也需要我們修改這個狀態來解決,下文會詳細展開。

最後一點就是擴容速度。前文說過,Redis通過搬Key的方式對業務是有影響的,由於同步操作,速度會比較慢,業務會感受到明顯的延時,這樣的延時業務肯定希望越快結束越好。

但是我們是搬遷Key,嚴重依賴Key的速度。因為搬Key不能全速搬,Redis是單線程,基本上線是8萬到10萬之間,如果搬太快,就佔據用戶CPU。

用戶本來因為同步搬遷卡頓導致變慢,搬遷又要佔他CPU,導致雪上加霜,所以一般這種方案是不可能做特別快的搬遷。比如說每次搬一萬Key,相當於占到12.5%,甚至更糟,這對於用戶來說是非常難以接受的。

騰訊會議用戶暴漲,Redis集群如何實現無縫擴容? 8

四、行業其他方案

既然開源版有這麼多問題,為什麼不改呢?不改的原因這個問題比較多。可能改起來不容易,也確實不太容易。

關於搬遷分片擴容是Redis的難點,很多人反饋過,但是目前而言沒有得到作者的反饋,也沒有一個明顯的解決的趨勢,行業內最常見就是DTS方案。

DTS方案可以通過下圖來了解,首先通過DTS建立同步,DTS同步跟Redis-port是類似,會偽裝一個slave,通過sync或者是psync命令從源端slave發起一次全量同步,全量之後再增量, DTS接到這個數據把rdb翻譯成命令再寫入目標端的實例上,這樣就不要求目標和源實例的分片數目一致,dts在中間把這個活給乾了。

騰訊會議用戶暴漲,Redis集群如何實現無縫擴容? 9

等到DTS完全遷移穩定之後,就可以一直同步增量數據,不停從源端push目標端,這時候可以考慮切換。

切換首先觀察是不是所有DTS延遲都在閾值內,這個延遲指的是從這邊Master到那邊Master的中間延遲。如果小於一定的數據量,就可以斷連客戶端,等待一定時間,等目標實例完全追上來了,再把LB指向新實例,再把源實例刪除了。一次擴容就完全實現了,這是行業比較常見的一種方案。

DTS方案解決什麼問題呢?大Key問題得到了解決。因為DTS是通過源進程slave的一個進程同步的。

Lua問題有沒有解決?這個問題也解決了,DTS收到RDB的時候就有lua信息了,可以翻譯成script load命令。多Key命令也得到了解決,正常用戶訪問不受影響,在切換之前對用戶來說無感知。

遷移速度也能夠得到比較好的改善。遷移速度本身是因為原實例通過rdb翻譯,翻譯之後並發寫入目標實例,這樣速度可以很快,可以全速寫。

這個速度一定比開源版key搬遷更快,因為目標實例在切換前不對外工作,可以全速寫入,遷移速度也是得到保證。

遷移中的HA和可用性和可靠性也都還可以。當然中間可用性要斷連30秒到1分鐘,這個時間用戶不可用,非常小的時間影響用戶的可用性。

騰訊會議用戶暴漲,Redis集群如何實現無縫擴容? 10

DTS有沒有缺點?有!首先是其複雜度,這個遷移方案依賴於DTS組件,需要外部組件才能實現,這個組件比較複雜,容易出錯。

其次是可用性,前文提到步驟裡面有一個踢掉客戶端的情況,30秒到1分鐘這是一般的經驗可用性影響,完全不可訪問。

還有成本問題,遷移過程中需要保證全量的2份資源,這個資源量保證在遷移量比較大的情況下,是非常大的。

如果所有的客戶同時擴容1分片,需要整個倉庫2倍的資源, 否則很多客戶會失敗,這個問題很致命,意味著我要理論上要空置一半的資源來保證擴容的成功, 對雲服務商來說是不可接受的,基於以上原因我們最後沒有採用DTS方案。

騰訊會議用戶暴漲,Redis集群如何實現無縫擴容? 11

五、騰訊雲Redis擴容方案

我們採用方案是這樣的,我們的目標是首先不依賴第三方組件,通過命令行也可以遷。第二是我們資源不要像DTS那樣遷移前和遷移後兩份資源都要保留,這個對於我們有相當大的壓力。最後用的是通過slot搬遷的方案。具體步驟如下:

首先還是計算各slot內存大小,需要計算具體搬遷多少slot。分配完slot之後,還要計算可分配到目標節點的slot。

跟開源版不一樣,不需要設置源進程的migrating狀態,源進程設置migrating是希望新Key自動寫入到目標進程,但是我們這個方案是不需要這樣做。

騰訊會議用戶暴漲,Redis集群如何實現無縫擴容? 12

再就是在目標進程發起slot命令,這個命令執行後,目標節點根據slot區間自動找到進程,然後對它發起sync命令(帶slot的sync),源進程收到這個sync命令,執行一個fork,將所有同步的slot區間所有的數據生成rdb,同步給目標進程。

每一個slot有哪一些Key在源進程是有記錄的,這裡遍歷將每一個slot的key生成rdb傳輸給目標進程,目標進程接受rdb開始loading,然後接受aof,這個aof也是接受跟slot相關的區間數據,源進程也不會把不屬於這個slot的數據給目標進程。

騰訊會議用戶暴漲,Redis集群如何實現無縫擴容? 13

一個目標進程可以從一兩個源點建立這樣的連接,一旦全部建立連接,並且同步狀態正常後,當offset足夠小的時候,就可以發起failover操作。

和Redis官方主動failover機制一樣。在failover之前,目標節點是不提供服務的,這個和開源版有巨大的差別。

騰訊會議用戶暴漲,Redis集群如何實現無縫擴容? 14

通過這個方案,大Key問題得到了解決。因為我們是通過fork進程解決的,而不是源節點搬遷key。切換前不對外提供服務,所以loading一兩分鐘沒有關係,客戶感知不到這個節點在loading。

還有就是Lua問題也解決了,新節點接受的是rdb數據,rdb包含了Lua信息在裡面。還有多Key命令也是一樣,因為我們完全不影響客戶正常訪問,多Key的命令以前怎麼訪問現在還是怎麼訪問。遷移速度因為是批量slot打包成rdb方式,一定比單個Key傳輸速度快很多。

關於HA的影響,遷移中有一個節點掛了會不會有影響?開源版會有影響,如果migrating節點掛了集群會有一個節點是不能夠對外提供服務。

但我們的方案不存在這個問題,切換完了依然可以提供服務。因為我們本來目標節點在切換之前就是不提供服務的。

還有可用性問題,我們方案不用斷客戶端連接,客戶端從頭到尾沒有受到任何影響,只是切換瞬間有小影響,毫秒級的影響。

成本問題有沒有解決?這個也得到解決,因為擴容過程中,只創建最終需要的節點,不會創建中間節點,零損耗。

騰訊會議用戶暴漲,Redis集群如何實現無縫擴容? 15

作者介紹

伍旭飛,騰訊雲高級工程師,騰訊雲Redis技術負責人,有多年和遊戲和數據庫開發應用實踐經驗, 聚焦於遊戲開發和NOSQL數據庫在各個領域的應用實踐。

本文轉載自公眾號雲加社區(ID:QcloudCommunity)。

原文鏈接

https://mp.weixin.qq.com/s/nKCw_a5mU9sn7SPKmCn-OQ