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


遠程辦公期間,在線會議用戶需求激增,騰訊會議8天完成100萬核雲服務器擴展,Redis集群僅在半小時以內就高效完成了數十倍規模的擴容,單集群的擴容流程后台處理時間不超過30分鍾。在這背后,騰訊雲Redis是如何做到的呢?本文是伍旭飛老師在「雲加社區沙龍online」的分享整理,詳細闡述了騰訊雲Redis無損擴容的實踐和挑戰。

在線用戶暴漲,騰訊雲Redis集群如何實現無縫擴容?_騰訊視頻​

一、疫情帶來的挑戰

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

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

二、開源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,擴容處理能力也間接擴容內存。

最早騰訊雲做過一個版本,利用開源的原生版的擴容方式擴容。簡單描述一下操作步驟:

首先Proxy是要做slot容量計算,否則一旦搬遷過去,容易把新分片的內存打爆。計算完每個slot內存后,按照算法分配,決定好目標分片有哪些slot。先設置目標節點slot 為importing狀態 ,再設置源節點的slot為 migrating狀態。

這里存在一個小坑,在正常開發中這兩個設置大家感覺順序無關緊要,但是實際上有影響。如果先設置源節點的slot為migrating,再設置目標節點的slot為importing,那么在這兩條命令的執行間隙,如果有對應slot的命令打到源節點,而key又恰好不存在,則會重定向到目標節點,由於目標節點此時slot並未來得及設置為importing, 又會把這條命令重定向給源節點,這樣就無限重定向了。

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

(1)准備

(2)搬遷

設置完了這一步,下一步就是搬遷。從源節點來獲取slot的搬遷,從源進程慢慢逐個搬遷到目標節點。此操作是同步的,什么意思呢?

在migrate命令結束之前進程不能直接處理客戶請求,實際上是源端臨時創建一個socket,連接目標節點,同步執行命令,確認執行成功了后,把本地的Key刪掉,這個時候源端才可以繼續處理客戶新的請求。在搬遷過程中,整個集群仍然是可以處理請求的。

這一塊開源Redis有考慮,如果這個時候有Key讀請求,剛好這個slot發到源進程,進程可以判斷,如果這個Key在本進程有數據,就會當正常的請求返回給它。

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

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

(3)切換

方案到什么時候切換呢?就是slot源進程發現這個slot已經不存在數據了,說明所有數據全部搬到目標進程去了。這個時候怎么辦呢?先發送set slot給目標,然后給源節點發送set slot命令,最后給集群所有其他節點發送set slot。

這是為了更快速把路由更新過去,避免通過自身集群版協議推廣,降低速度。這里跟設置遷移前的准備步驟是一樣,也有一個小坑需要注意。

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

三、 無損擴容挑戰

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就會有影響,只要搬遷未結束,客戶在相當長時間都心驚膽戰。

2. Lua問題

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

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

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

這個問題在開源版是無解,最后業務怎么做才能夠解決這個問題呢?需要業務那邊改一下代碼,如果發現evalsha執行出現代碼不存在的錯誤,業務要主動執行一個script load,這樣可以規避這個問題。但是對很多業務是不能接受的。因為要面臨一個重新加代碼然后再發布這樣一個流程,業務受損時間是非常長的。

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的難點,很多人反饋過,但是目前而言沒有得到作者的反饋,也沒有一個明顯的解決的趨勢,行業內最常見就是DTS方案。

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

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

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

DTS方案解決什么問題呢?大Key問題得到了解決。因為DTS是通過源進程slave的一個進程同步的。Lua問題有沒有解決?這個問題也解決了,DTS收到RDB的時候就有lua信息了,可以翻譯成script load命令。多Key命令也得到了解決,正常用戶訪問不受影響,在切換之前對用戶來說無感知。遷移速度也能夠得到比較好的改善。遷移速度本身是因為原實例通過rdb翻譯,翻譯之后並發寫入目標實例,這樣速度可以很快,可以全速寫。這個速度一定比開源版key搬遷更快,因為目標實例在切換前不對外工作,可以全速寫入,遷移速度也是得到保證。遷移中的HA和可用性和可靠性也都還可以。當然中間可用性要斷連30秒到1分鍾,這個時間用戶不可用,非常小的時間影響用戶的可用性。

DTS有沒有缺點?有!首先是其復雜度,這個遷移方案依賴於DTS組件,需要外部組件才能實現,這個組件比較復雜,容易出錯。其次是可用性,前文提到步驟里面有一個踢掉客戶端的情況,30秒到1分鍾這是一般的經驗可用性影響,完全不可訪問。還有成本問題,遷移過程中需要保證全量的2份資源,這個資源量保證在遷移量比較大的情況下,是非常大的。如果所有的客戶同時擴容1分片,需要整個倉庫2倍的資源, 否則很多客戶會失敗,這個問題很致命,意味着我要理論上要空置一半的資源來保證擴容的成功, 對雲服務商來說是不可接受的,基於以上原因我們最后沒有采用DTS方案。

五、騰訊雲Redis擴容方案

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

首先還是計算各slot內存大小,需要計算具體搬遷多少slot。分配完slot之后,還要計算可分配到目標節點的slot。跟開源版不一樣,不需要設置源進程的migrating狀態,源進程設置migrating是希望新Key自動寫入到目標進程,但是我們這個方案是不需要這樣做。

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

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

一個目標進程可以從一兩個源點建立這樣的連接,一旦全部建立連接,並且同步狀態正常后,當offset足夠小的時候,就可以發起failover操作。和Redis官方主動failover機制一樣。在failover之前,目標節點是不提供服務的,這個和開源版有巨大的差別。

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

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

關於HA的影響,遷移中有一個節點掛了會不會有影響?開源版會有影響,如果migrating節點掛了集群會有一個節點是不能夠對外提供服務。但我們的方案不存在這個問題,切換完了依然可以提供服務。因為我們本來目標節點在切換之前就是不提供服務的。

還有可用性問題,我們方案不用斷客戶端連接,客戶端從頭到尾沒有受到任何影響,只是切換瞬間有小影響,毫秒級的影響。成本問題有沒有解決?這個也得到解決,因為擴容過程中,只創建最終需要的節點,不會創建中間節點,零損耗。

六、Q&A

Q:Cluster數量會改變槽位的數量嗎?

A:不會改變槽位數量,一直是16814,這個跟開源版是一致的。

Q:遷移之前怎么評估新slot接觸數據不會溢出?

A:根據前文所述,我們有一個准備階段,計算所有slot各自內存大小,怎么計算我們會在slave直接執行一次掃描計算,基本上能夠計算比較准確。這里沒有用前一天的備份數據,而是采用slave實時計算,CPU里有相應控制,這樣可以計算出slot總量大小。我們會預定提前量(1.3倍),用戶還在寫,保證目標不會遷移中被寫爆,萬一寫爆了也只是流程失敗,用戶不會受到影響。

Q:對於 redis 擴容操作會發生的問題,你們有采用什么備用方案,和緊急措施嗎?

A:新的擴容方案,如果出現問題,不會影響客戶的使用,只會存在多余的資源,目前這塊依賴工具來處理。數據的安全性本身除了主從,還依賴每日備份來保證。

Q:請問老師,高峰期需要擴容,但總有回歸正常請求量的時候,此時的擴容顯得有些冗余,怎么樣讓Redis集群能夠既能夠快速回收多余容量,同時又能方便下一次高峰請求的再次擴容呢?

A:可能你想了解的是serverless,按需付費自動擴展的模式,目前的數據庫大多是提供的PAAS服務,PASS層的數據庫將過去的資源手動管理變成了半自動化,擴縮容還是需要運維參與。Serverless形式的服務會是下一步的形態,但是就算是serverless可能也會面臨着需要資源手動擴展的問題,特別是對超大規模的運算服務。

Q:slot來源於多個分片,同時和多個節點同步進行嗎?

A:是,確實會這樣做的。

Q:slot產生過程中產生新數據怎么同步?

A:類似aof概念的機制同步到目標進程。這個aof跟普通aof傳輸到slave有區別,只會將跟目標slot相關的數據同步過去,而不會同步別的。

Q:還在招人嗎?

A:騰訊雲Redis還在招人,歡迎大家投簡歷,簡歷請投至郵箱:feiwu@tencent.com。大家還可以選擇在官網上投遞簡歷,或者搜索關注騰訊雲數據庫的公眾號,查看21日的推送文章,我們貼上了招聘職位的鏈接,也可以@社群小助手,小助手會收集上來發到我這邊。

講師簡介

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

關注雲加社區公眾號,回復“線上沙龍”,即可獲取老師演講PPT


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM