給我10分鐘,無(wú)感知搞定Redis集群擴(kuò)縮容
一、背景
攜程Redis集群規(guī)模和數(shù)據(jù)規(guī)模在過去幾年里快速增長(zhǎng),我們通過容器化解決了Redis集群快速部署的問題,并根據(jù)實(shí)際業(yè)務(wù)進(jìn)行的一系列嘗試,比如二次調(diào)度、自動(dòng)化漂移等,在內(nèi)存超分的情況下保證了宿主機(jī)的可靠性。
擴(kuò)縮容方面,我們主要通過垂直擴(kuò)縮容的方式解決Redis集群容量的問題,但隨著集群規(guī)模擴(kuò)大,這種方式逐漸遇到了瓶頸。一方面,單個(gè)Redis實(shí)例過大,會(huì)帶來(lái)較大的運(yùn)維風(fēng)險(xiǎn)和困難;另一方面,宿主機(jī)容量有上限,不能無(wú)止境的擴(kuò)容。考慮到運(yùn)維便利性和資源利用率的平衡,我們希望單個(gè)Redis實(shí)例的上限為15GB。
但實(shí)際操作中卻很難做到:某些業(yè)務(wù)發(fā)展很快,經(jīng)常性需要給Redis進(jìn)行擴(kuò)容,導(dǎo)致單個(gè)實(shí)例大小遠(yuǎn)超15GB;一些業(yè)務(wù)萎縮,實(shí)際使用量遠(yuǎn)低于初始申請(qǐng)的量,造成資源的浪費(fèi)。
如何有效控制Redis實(shí)例大小呢?接下來(lái)本文將帶著這個(gè)問題,逐步講解攜程Redis治理和擴(kuò)縮容方面的演進(jìn)歷程。
二、Redis水平擴(kuò)分拆
在攜程開始使用Redis很長(zhǎng)一段時(shí)間里,一直只有垂直擴(kuò)縮容,原因有兩點(diǎn):
第一,一開始業(yè)務(wù)規(guī)模比較小,垂直擴(kuò)縮容可以滿足需求。垂直擴(kuò)縮容對(duì)于Redis來(lái)說只是Maxmemory的配置更改,對(duì)業(yè)務(wù)透明。
第二,水平拆分/擴(kuò)縮容的實(shí)現(xiàn)難度和成本較高。
之前文章《攜程Redis治理演進(jìn)之路》中已經(jīng)提到,攜程訪問所有的Redis集群使用的是自主研發(fā)的CRedis,而部署在應(yīng)用端的CRedis通過一致性hash來(lái)訪問實(shí)際承載數(shù)據(jù)的Redis實(shí)例。但一致性hash是無(wú)法支持直接水平擴(kuò)縮容的。因?yàn)闊o(wú)論增加一個(gè)節(jié)點(diǎn)或者刪除一個(gè)節(jié)點(diǎn),都會(huì)導(dǎo)致整個(gè)hash環(huán)的調(diào)整。
圖1
如圖所示,假設(shè)原始有4個(gè)分片(圖1)。當(dāng)添加一個(gè)節(jié)點(diǎn)后,它會(huì)導(dǎo)致某一部分的key本來(lái)是寫到nodeC上而現(xiàn)在會(huì)被寫到nodeE上,也就是無(wú)法命中之前的節(jié)點(diǎn)。從客戶端的角度來(lái)看,key就像是丟失了。而變動(dòng)的節(jié)點(diǎn)越多,key丟失的也越多,假設(shè)某個(gè)集群從10分片直接添加到20分片,它直接會(huì)導(dǎo)致50%的key丟失。刪除一個(gè)節(jié)點(diǎn)同理,就不再贅述。
因此盡管一致性hash是個(gè)比較簡(jiǎn)單優(yōu)秀的集群方案,但無(wú)法直接水平擴(kuò)容一直困擾著運(yùn)維和架構(gòu)團(tuán)隊(duì)。為此,CRedis團(tuán)隊(duì)在2019年提出了水平拆分的方案。
CRedis水平分拆的思路比較樸素,因?yàn)樵谝恢滦詇ash同一個(gè)水平位置增加節(jié)點(diǎn)會(huì)導(dǎo)致數(shù)據(jù)丟失,那么不改變?cè)瓉?lái)層次節(jié)點(diǎn)的hash規(guī)則,以某個(gè)節(jié)點(diǎn)為hash的起點(diǎn),再來(lái)進(jìn)行一次一致性hash,演變成樹的結(jié)構(gòu)(圖2)。
圖2
如上圖所示,將樹形結(jié)構(gòu)從一層拓展成二層,如果繼續(xù)拆分新的葉子Group,則可以將樹形結(jié)構(gòu)拓展到三層,拆分方案可以支持到十層。葉子 Group是物理分片,直接對(duì)應(yīng)的 Redis 實(shí)例,分支 Group 是虛擬分片,當(dāng)Hash 命中到分支 Group 后,并沒有找不到對(duì)應(yīng)的Redis實(shí)例,需要再繼續(xù)向下尋找,直到找到葉子 Group 為止。
圖3
CRedis水平分拆上線后,DBA將現(xiàn)存的絕大部分超過15G的實(shí)例都拆分成更小的實(shí)例,在一段時(shí)間內(nèi)緩解了大內(nèi)存實(shí)例的運(yùn)維治理壓力。但隨著Redis規(guī)模的快速增長(zhǎng),不斷有大的實(shí)例集群出現(xiàn),此外CRedis水平分拆的缺點(diǎn)也逐漸暴露出來(lái):
- 持續(xù)的周期很長(zhǎng),對(duì)多個(gè) Group 進(jìn)行拆分的話,每個(gè)Group的數(shù)據(jù)需要同時(shí)復(fù)制幾份同樣的實(shí)例。比如60G的某個(gè)實(shí)例(圖3),如果想拆到5G一個(gè),那么下級(jí)的Group必須有12個(gè),而拆分要先將該實(shí)例的數(shù)據(jù)先同步為12個(gè)60G的實(shí)例,再根據(jù)key的命中規(guī)則清理該12個(gè)60G的實(shí)例中不會(huì)命中的key,最終演變成12個(gè)5G的實(shí)例。一般60G的group實(shí)例拆分需要3個(gè)小時(shí)-6個(gè)小時(shí),如果一個(gè)集群的分片非常多,加上觀察對(duì)業(yè)務(wù)影響的時(shí)間,可能要持續(xù)上幾天或一兩周,并且只能是有人值守的串行操作。
- 拆分過程中需要2次遷移,如上面所說的,拆分中中間態(tài)實(shí)例對(duì)于內(nèi)存的要求是非常大的,拆分完成后對(duì)內(nèi)存的需求會(huì)急劇下降,因此每次拆分都涉及到2次遷移,盡管遷移不會(huì)影響業(yè)務(wù),但對(duì)于執(zhí)行操作拆分的運(yùn)維人員來(lái)說,心智負(fù)擔(dān)比較大,而且一不小心也會(huì)導(dǎo)致線上事故。
- 拆分后無(wú)法還原回去,也就是說假設(shè)業(yè)務(wù)分拆后收縮,對(duì)Redis的需求變小了,但它實(shí)際拆分后的分片還在那邊,所申請(qǐng)的空間還并沒有釋放掉,客觀上浪費(fèi)了資源,降低了Redis總體的利用率。
- 只支持?jǐn)U容,不支持縮容,這點(diǎn)上面也提到了,除了一些集群過大需要分拆外,還有一些申請(qǐng)遠(yuǎn)超需求的實(shí)例需要縮容,而水平分拆對(duì)于這點(diǎn)無(wú)能為力。
- 拆分一次,就多一次的性能損耗,因?yàn)樾枰嘤?jì)算一次hash,雖然耗時(shí)不大,但是對(duì)于性能敏感的業(yè)務(wù)還是有影響。
由此可見,水平分拆的方案雖然解決了實(shí)例過大的問題,但不能縮容的弊端也逐漸凸現(xiàn)了出來(lái)。尤其是在今年因疫情影響需要降本增效的背景下,一方面資源比充足,一方面宿主機(jī)上跑的都是無(wú)法縮容的實(shí)例。那么是否有更好的解決方案呢?答案是有的。
三、Redis水平擴(kuò)縮容
1、設(shè)計(jì)思路
圖4
既然縮分片比較困難,我們首先想到的是業(yè)務(wù)雙寫集群的方法,也就是業(yè)務(wù)同時(shí)雙寫2個(gè)新老集群,新老集群的分片數(shù)是不一樣的,并且大小配置也不一樣。比如之前申請(qǐng)4個(gè)分片現(xiàn)在發(fā)現(xiàn)資源過剩,讓業(yè)務(wù)創(chuàng)新申請(qǐng)一個(gè)新的2個(gè)分片的集群,由業(yè)務(wù)來(lái)控制灰度寫哪個(gè)集群(圖4)。最終會(huì)遷移到新集群上,而新集群大小是滿足當(dāng)前業(yè)務(wù)需求的,從而達(dá)到了縮容的目的。
雙寫集群的方案雖然解決我們部分的問題,但對(duì)于業(yè)務(wù)的侵入比較深,此外由于雙寫集群引入了業(yè)務(wù)配合觀察的時(shí)間,整體流程也比較長(zhǎng)。所以,我們需要尋找更好的解決方案。
既然業(yè)務(wù)雙寫集群可以達(dá)到要求,基礎(chǔ)設(shè)施如果代替業(yè)務(wù)做完這部分豈不是更好?借鑒業(yè)務(wù)雙寫集群的思路和云原生的不可變基礎(chǔ)設(shè)施的理念,我們首先想到的是通過新集群替換老集群而不是原地修改集群;另外,為了在公有云上節(jié)省Redis成本,我們積累了kvrocks的實(shí)踐經(jīng)驗(yàn),兩者相結(jié)合,設(shè)計(jì)了一種高效的水平擴(kuò)縮容的方案。
本方案的核心是引入了一個(gè)基于kvrocks改造的中間態(tài)binlogserver,它既是一個(gè)老集群的Slave節(jié)點(diǎn),又充當(dāng)了新集群的客戶端。一方面,它會(huì)從Redis Master復(fù)制全量和增量數(shù)據(jù);另一方面,它又充當(dāng)客戶端的角色,將復(fù)制來(lái)的數(shù)據(jù)按照新集群的一致性HASH規(guī)則寫往新的集群。大致的步驟如下,具體的步驟流程可以參考下面的圖所示(圖5)。
- 根據(jù)當(dāng)前V1集群的分片啟動(dòng)對(duì)應(yīng)個(gè)數(shù)binlogserver,并獲取V2集群的一致性HASH規(guī)則和group。
- 每個(gè)binlogserver成為V1集群?jiǎn)蝹€(gè)分片中Master的Slave,執(zhí)行salveof后保存V1中Master傳過來(lái)的RDB文件并解析,對(duì)于每個(gè)RDB文件,解析還原成Redis命令,并按CRedis的一致性hash規(guī)則寫入到V2中,對(duì)于后續(xù)V1集群傳播過來(lái)的命令,同樣同步到V2中。
- 當(dāng)這個(gè)過程都完成并且binlog追的差不多的時(shí)候,為了數(shù)據(jù)一致性,可以停止V1的寫(客戶端報(bào)錯(cuò))后由CRedis推送V2的配置或直接推送V2的配置(客戶端不報(bào)錯(cuò)但數(shù)據(jù)可能會(huì)丟或不一致),APP端將會(huì)順序切換到 V2上來(lái);此過程對(duì)用戶完全透明,應(yīng)用端無(wú)需做任何操作。
圖5
通過Redis的水平擴(kuò)縮容方案,我們解決了之前的幾個(gè)痛點(diǎn)問題:
- 持續(xù)時(shí)間大大縮短,基本上跟V1集群最大實(shí)例的大小正相關(guān),因?yàn)槭遣l(fā)執(zhí)行,跟集群分片數(shù)無(wú)關(guān)。根據(jù)實(shí)際的運(yùn)維數(shù)據(jù)來(lái)看,集群?jiǎn)蝹€(gè)實(shí)例為20G,集群擴(kuò)縮容在10分鐘之內(nèi)完成,而低于10G的,5分鐘即可完成,大大縮短了擴(kuò)縮容的周期,并且業(yè)務(wù)在毫無(wú)感知的情況下即可完成擴(kuò)縮容。由于可以做到秒級(jí)切換集群,即使擴(kuò)縮容后對(duì)業(yè)務(wù)有影響也可以快速回退,因?yàn)榛赝艘仓皇歉牧思旱穆酚芍赶颉?/li>
- 擴(kuò)縮容過程只需要1次切換集群指向,0次遷移,沒有中間態(tài),也無(wú)需通過大內(nèi)存宿主機(jī)來(lái)實(shí)現(xiàn)分拆。
- 對(duì)于擴(kuò)容的集群,很方便再來(lái)一次縮容還原回去,縮容同理。對(duì)于那些已經(jīng)水平拆分過的集群,也可以通過這種方式還原回去。
- 既可以擴(kuò)容也可以縮容,甚至還可以不擴(kuò)容也不縮容按集群來(lái)遷移,比如《攜程Cilium+BGP云原生網(wǎng)絡(luò)實(shí)踐》一文中提到的云原生網(wǎng)絡(luò)安全控制試點(diǎn)項(xiàng)目。由于原來(lái)Redis集群下面的實(shí)例可能同時(shí)部署在openstack網(wǎng)絡(luò)和cilium網(wǎng)絡(luò),但云原生安全只能控制cilium網(wǎng)絡(luò)下的實(shí)例,這種情況下就需要遷移Redis實(shí)例。如果按之前的運(yùn)維方式,要按分片來(lái)一組組遷移,整個(gè)工程可能持續(xù)較長(zhǎng)時(shí)間,并且耗費(fèi)較多人力,而水平擴(kuò)縮容可以將一個(gè)集群一次性快速遷移到cilium網(wǎng)絡(luò),省時(shí)省力。
- 擴(kuò)縮容后無(wú)性能損耗。
2、運(yùn)維數(shù)據(jù)
水平擴(kuò)縮容方案上線4個(gè)月來(lái),已經(jīng)成功完成了200多次的擴(kuò)容和縮容。今年某個(gè)業(yè)務(wù)突然請(qǐng)求量暴增十幾倍,相關(guān)集群經(jīng)歷了多次擴(kuò)容,每次擴(kuò)容大多在10分鐘內(nèi)完成,有效地支撐了業(yè)務(wù)發(fā)展。
另一方面,針對(duì)申請(qǐng)分片非常多而大但實(shí)際使用量非常小的集群,我們也借助水平擴(kuò)縮容的能力快速地縮小了分片數(shù)和申請(qǐng)量。通過這些縮容,有效地提升了整體的資源利用率。
3、一些坑
(1)單個(gè)key過大導(dǎo)致key被驅(qū)逐
在實(shí)際水平擴(kuò)縮容過程中,我們發(fā)現(xiàn)有些集群,單個(gè)實(shí)例中可能會(huì)有巨大的key(大于3G),由于V2集群的大小是根據(jù)V1大小實(shí)時(shí)算出來(lái)的平均值,一旦V1中某個(gè)實(shí)例過大,可能會(huì)導(dǎo)致寫到V2中的某個(gè)實(shí)例大小大于預(yù)期的平均值,從而引起某些key被驅(qū)逐。因此,針對(duì)這種情況:
- 加強(qiáng)大key的檢測(cè)邏輯,對(duì)于超過512M的key會(huì)有告警郵件告知所有者。
- V2中所有實(shí)例的maxmemory在分拆之前不設(shè)置限制,統(tǒng)一都調(diào)到60G,防止V2中key分配不均導(dǎo)致key驅(qū)逐。
- 水平擴(kuò)縮容后,在V1和V2切換過程中,檢測(cè)V2中的實(shí)例是否發(fā)生過驅(qū)逐,如果有則默認(rèn)分拆失敗,不進(jìn)行切換。
(2)mget擴(kuò)容后會(huì)導(dǎo)致性能下降
對(duì)于極個(gè)別的場(chǎng)景,我們還發(fā)現(xiàn),mget請(qǐng)求耗時(shí)會(huì)有明顯上升,主要原因還是在于,擴(kuò)容之前mget需要訪問的實(shí)例數(shù)少,而分拆后訪問的實(shí)例數(shù)變多。一般這種情況,我們建議業(yè)務(wù)控制單次mget的key的數(shù)量,或者將string類型改造為hash類型,通過hmget來(lái)訪問數(shù)據(jù),保證每次只會(huì)訪問到一個(gè)實(shí)例,這樣擴(kuò)容后其吞吐量是隨著分片數(shù)量線性增加,而延遲不會(huì)有增加。
四、總結(jié)和未來(lái)規(guī)劃
1、Xpipe支持
目前水平擴(kuò)縮容和漂移以及二次調(diào)度等一系列治理工具和策略組成了一個(gè)比較完善的閉環(huán),有效地支撐了生產(chǎn)幾千臺(tái)宿主機(jī),幾萬(wàn)帶超分能力Redis實(shí)例的運(yùn)維治理。
但目前受制于xpipe的架構(gòu),對(duì)于接入了xpipe的集群,必須先擴(kuò)縮容后再將DR端的xpipe人工補(bǔ)齊,自動(dòng)化程度還不足,而補(bǔ)齊xpipe的時(shí)間比較長(zhǎng),比如之前是就近讀本機(jī)房的Redis集群的APP,在擴(kuò)縮容后可能一段時(shí)間里只能跨機(jī)房讀取,必然導(dǎo)致延遲上升。而這種延遲上升又會(huì)影響我們對(duì)于水平擴(kuò)縮容邏輯是否正確,是否需要回退的判斷。因此后續(xù)我們會(huì)針對(duì)xpipe集群,也做到和普通集群一樣,也就是V2集群在擴(kuò)縮容寫流量之前就是帶DR架構(gòu)的集群。
2、持久化KV存儲(chǔ)的支持
除了Redis本身受業(yè)務(wù)歡迎使用廣泛外,我們還發(fā)現(xiàn)有些業(yè)務(wù)需要相比Redis 更可靠的KV存儲(chǔ)方式,比如數(shù)據(jù)保存在磁盤上而不是保存在內(nèi)存里,再比如業(yè)務(wù)需要支持一些增減庫(kù)存邏輯,對(duì)某個(gè)key的獨(dú)占訪問,實(shí)現(xiàn)語(yǔ)義近似INCRBY操作,但實(shí)際上是對(duì)于一些字符串進(jìn)行merge操作。此外數(shù)據(jù)可靠性要求更高,master宕機(jī)不能丟失數(shù)據(jù)等。針對(duì)這些需求目前我們已經(jīng)也有一些實(shí)踐經(jīng)驗(yàn),將在后續(xù)文章中分享。