KV 存儲(chǔ)發(fā)展歷程
美團(tuán)第一代的分布式 KV 存儲(chǔ)如下圖左側(cè)的架構(gòu)所示,相信很多公司都經(jīng)歷過(guò)這個(gè)階段。在客戶端內(nèi)做一致性哈希,在后端部署很多的 Memcached 實(shí)例,這樣就實(shí)現(xiàn)了最基本的 KV 存儲(chǔ)分布式設(shè)計(jì)。但這樣的設(shè)計(jì)存在很明顯的問(wèn)題:比如在宕機(jī)摘除節(jié)點(diǎn)時(shí),會(huì)丟數(shù)據(jù),緩存空間不夠需要擴(kuò)容,一致性哈希也會(huì)丟失一些數(shù)據(jù)等等,這樣會(huì)給業(yè)務(wù)開發(fā)帶來(lái)的很多困擾。
隨著 Redis 項(xiàng)目的成熟,美團(tuán)也引入了 Redis 來(lái)解決我們上面提到的問(wèn)題,進(jìn)而演進(jìn)出來(lái)如上圖右側(cè)這樣一個(gè)架構(gòu)。大家可以看到,客戶端還是一樣,采用了一致性哈希算法,服務(wù)器端變成了 Redis 組成的主從結(jié)構(gòu)。當(dāng)任何一個(gè)節(jié)點(diǎn)宕機(jī),我們可以通過(guò) Redis 哨兵完成 Failover,實(shí)現(xiàn)高可用。但有一個(gè)問(wèn)題還是沒(méi)有解決,如果擴(kuò)縮容的話,一致性哈希仍然會(huì)丟數(shù)據(jù),那么這個(gè)問(wèn)題該如何解決呢?
這個(gè)時(shí)候,我們發(fā)現(xiàn)有了一個(gè)比較成熟的 KV 存儲(chǔ)開源項(xiàng)目:阿里 Tair 。2014年,我們引入了 Tair 來(lái)滿足業(yè)務(wù) KV 存儲(chǔ)方面的需求。Tair 開源版本的架構(gòu)主要分成三部分:上圖下邊是存儲(chǔ)節(jié)點(diǎn),存儲(chǔ)節(jié)點(diǎn)會(huì)上報(bào)心跳到它的中心節(jié)點(diǎn),中心節(jié)點(diǎn)內(nèi)部有兩個(gè)配置管理節(jié)點(diǎn),會(huì)監(jiān)控所有的存儲(chǔ)節(jié)點(diǎn)。當(dāng)有任何存儲(chǔ)節(jié)點(diǎn)宕機(jī)或者擴(kuò)容時(shí),它會(huì)做集群拓?fù)涞闹匦聵?gòu)建。當(dāng)客戶端啟動(dòng)時(shí),它會(huì)直接從中心節(jié)點(diǎn)拉來(lái)一個(gè)路由表。這個(gè)路由表簡(jiǎn)單來(lái)說(shuō)就是一個(gè)集群的數(shù)據(jù)分布圖,客戶端根據(jù)路由表直接去存儲(chǔ)節(jié)點(diǎn)讀寫。針對(duì)之前 KV 的擴(kuò)容丟數(shù)據(jù)問(wèn)題,它也有數(shù)據(jù)遷移機(jī)制來(lái)保證數(shù)據(jù)的完整性。
但是,我們?cè)谑褂玫倪^(guò)程中,還遇到了一些其他問(wèn)題,比如中心節(jié)點(diǎn)雖然是主備高可用的,但實(shí)際上它沒(méi)有類似于分布式仲裁的機(jī)制,所以在網(wǎng)絡(luò)分割的情況下,它是有可能發(fā)生“腦裂”的,這個(gè)也給我們的業(yè)務(wù)造成過(guò)比較大的影響。另外,在容災(zāi)擴(kuò)容時(shí),也遇到過(guò)數(shù)據(jù)遷移影響到業(yè)務(wù)可用性的問(wèn)題。另外,我們之前用過(guò) Redis ,業(yè)務(wù)會(huì)發(fā)現(xiàn) Redis 的數(shù)據(jù)結(jié)構(gòu)特別豐富,而 Tair 還不支持這些數(shù)據(jù)結(jié)構(gòu)。雖然我們用 Tair 解決了一些問(wèn)題,但是 Tair 也無(wú)法完全滿足業(yè)務(wù)需求。畢竟,在美團(tuán)這樣一個(gè)業(yè)務(wù)規(guī)模較大和業(yè)務(wù)復(fù)雜度較高的場(chǎng)景下,很難有開源系統(tǒng)能很好地滿足我們的需求。最終,我們決定在已應(yīng)用的開源系統(tǒng)之上進(jìn)行自研。
剛好在2015 年, Redis 官方正式發(fā)布了集群版本 Redis Cluster。所以,我們緊跟社區(qū)步伐,并結(jié)合內(nèi)部需求做了很多開發(fā)工作,演進(jìn)出了全內(nèi)存、高吞吐、低延遲的 KV 存儲(chǔ) Squirrel。另外,基于 Tair,我們還加入了很多自研的功能,演進(jìn)出持久化、大容量、數(shù)據(jù)高可靠的 KV 存儲(chǔ) Cellar 。因?yàn)?Tair 的開源版本已經(jīng)有四五年沒(méi)有更新了,所以,Cellar 的迭代完全靠美團(tuán)自研,而 Redis 社區(qū)一直很活躍。總的來(lái)說(shuō),Squirrel 的迭代是自研和社區(qū)并重,自研功能設(shè)計(jì)上也會(huì)盡量與官方架構(gòu)進(jìn)行兼容。后面大家可以看到,因?yàn)檫@些不同,Cellar 和 Squirrel 在解決同樣的問(wèn)題時(shí)也選取了不同的設(shè)計(jì)方案。
這兩個(gè)存儲(chǔ)其實(shí)都是 KV 存儲(chǔ)領(lǐng)域不同的解決方案。在實(shí)際應(yīng)用上,如果業(yè)務(wù)的數(shù)據(jù)量小,對(duì)延遲敏感,我們建議大家用 Squirrel ;如果數(shù)據(jù)量大,對(duì)延遲不是特別敏感,我們建議用成本更低的 Cellar 。目前這兩套 KV 存儲(chǔ)系統(tǒng)在美團(tuán)內(nèi)部每天的調(diào)用量均已突破萬(wàn)億,它們的請(qǐng)求峰值也都突破了每秒億級(jí)。
內(nèi)存 KV Squirrel 架構(gòu)和實(shí)踐
在開始之前,本文先介紹兩個(gè)存儲(chǔ)系統(tǒng)共通的地方。比如分布式存儲(chǔ)的經(jīng)典問(wèn)題:數(shù)據(jù)是如何分布的?這個(gè)問(wèn)題在 KV 存儲(chǔ)領(lǐng)域,就是 Key 是怎么分布到存儲(chǔ)節(jié)點(diǎn)上的。這里 Squirrel 跟 Cellar 是一樣的。當(dāng)我們拿到一個(gè) Key 后,用固定的哈希算法拿到一個(gè)哈希值,然后將哈希值對(duì) Slot 數(shù)目取模得到一個(gè)Slot id,我們兩個(gè) KV 現(xiàn)在都是預(yù)分片16384個(gè) Slot 。得到 Slot id 之后,再根據(jù)路由表就能查到這個(gè) Slot 存儲(chǔ)在哪個(gè)存儲(chǔ)節(jié)點(diǎn)上。這個(gè)路由表簡(jiǎn)單來(lái)說(shuō)就是一個(gè) Slot 到存儲(chǔ)節(jié)點(diǎn)的對(duì)照表。
KV 數(shù)據(jù)分布介紹
接下來(lái)講一下對(duì)高可用架構(gòu)的認(rèn)知,個(gè)人認(rèn)為高可用可以從宏觀和微觀兩個(gè)角度來(lái)看。從宏觀的角度來(lái)看,高可用就是指容災(zāi)怎么做。比如說(shuō)掛掉了一個(gè)節(jié)點(diǎn),你該怎么做?一個(gè)機(jī)房或者說(shuō)某個(gè)地域的一批機(jī)房宕機(jī)了,你該怎么做?而從微觀的角度看,高可用就是怎么能保證端到端的高成功率。我們?cè)谧鲆恍┻\(yùn)維升級(jí)或者擴(kuò)縮容數(shù)據(jù)遷移的時(shí)候,能否做到業(yè)務(wù)請(qǐng)求的高可用?本文也會(huì)從宏觀和微觀兩個(gè)角度來(lái)分享美團(tuán)做的一些高可用工作。
Squirrel 架構(gòu)
上圖就是我們的 Squirrel 架構(gòu)。中間部分跟 Redis 官方集群是一致的。它有主從的結(jié)構(gòu), Redis 實(shí)例之間通過(guò) Gossip 協(xié)議去通信。我們?cè)谟疫吿砑恿艘粋€(gè)集群調(diào)度平臺(tái),包含調(diào)度服務(wù)、擴(kuò)縮容服務(wù)和高可用服務(wù)等,它會(huì)去管理整個(gè)集群,把管理結(jié)果作為元數(shù)據(jù)更新到 ZooKeeper。我們的客戶端會(huì)訂閱 ZooKeeper 上的元數(shù)據(jù)變更,實(shí)時(shí)獲取到集群的拓?fù)錉顟B(tài),直接在 Redis 集群進(jìn)行讀寫操作。
Squirrel 節(jié)點(diǎn)容災(zāi)
然后再看一下 Squirrel 容災(zāi)怎么做。對(duì)于 Redis 集群而言,節(jié)點(diǎn)宕機(jī)已經(jīng)有完備的處理機(jī)制了。官方提供的方案,任何一個(gè)節(jié)點(diǎn)從宕機(jī)到被標(biāo)記為 FAIL 摘除,一般需要經(jīng)過(guò) 30 秒。主庫(kù)的摘除可能會(huì)影響數(shù)據(jù)的完整性,所以,我們需要謹(jǐn)慎一些。但是對(duì)于從庫(kù)呢?我們認(rèn)為這個(gè)過(guò)程完全沒(méi)必要。另一點(diǎn),我們都知道內(nèi)存的 KV 存儲(chǔ)數(shù)據(jù)量一般都比較小。對(duì)于業(yè)務(wù)量很大的公司來(lái)說(shuō),它往往會(huì)有很多的集群。如果發(fā)生交換機(jī)故障,會(huì)影響到很多的集群,宕機(jī)之后去補(bǔ)副本就會(huì)變得非常麻煩。為了解決這兩個(gè)問(wèn)題,我們做了 HA 高可用服務(wù)。
它的架構(gòu)如下圖所示,它會(huì)實(shí)時(shí)監(jiān)控集群的所有節(jié)點(diǎn)。不管是網(wǎng)絡(luò)抖動(dòng),還是發(fā)生了宕機(jī)(比如說(shuō) Redis 2 ),它可以實(shí)時(shí)更新 ZooKeeper ,告訴 ZooKeeper 去摘除 Redis 2 ,客戶端收到消息后,讀流量就直接路由到 Redis 3上。如果 Redis 2 只是幾十秒的網(wǎng)絡(luò)抖動(dòng),過(guò)幾十秒之后,如果 HA 節(jié)點(diǎn)監(jiān)控到它恢復(fù)后,會(huì)把它重新加回。
Squirrel—節(jié)點(diǎn)容災(zāi)
如果過(guò)了一段時(shí)間,HA 判斷它屬于一個(gè)永久性的宕機(jī),HA 節(jié)點(diǎn)會(huì)直接從 Kubernetes 集群申請(qǐng)一個(gè)新的 Redis 4 容器實(shí)例,把它加到集群里。此時(shí),拓?fù)浣Y(jié)構(gòu)又變成了一主兩從的標(biāo)準(zhǔn)結(jié)構(gòu),HA 節(jié)點(diǎn)更新完集群拓?fù)渲螅蜁?huì)去寫 ZooKeeper 通知客戶端去更新路由,客戶端就能到 Redis 4 這個(gè)新從庫(kù)上進(jìn)行讀操作。
通過(guò)上述方案,我們把從庫(kù)的摘除時(shí)間從 30 秒降低到了 5 秒。另外,我們通過(guò) HA 自動(dòng)申請(qǐng)容器實(shí)例加入集群的方式,把宕機(jī)補(bǔ)副本變成了一個(gè)分鐘級(jí)的自動(dòng)操作,不需要任何人工的介入。
Squirrel 跨地域容災(zāi)
我們解決了單節(jié)點(diǎn)宕機(jī)的問(wèn)題,那么跨地域問(wèn)題如何解決呢?我們首先來(lái)看下跨地域有什么不同。第一,相對(duì)于同地域機(jī)房間的網(wǎng)絡(luò)而言,跨地域?qū)>€很不穩(wěn)定;第二,跨地域?qū)>€的帶寬是非常有限且昂貴的。而集群內(nèi)的復(fù)制沒(méi)有考慮極端的網(wǎng)絡(luò)環(huán)境。假如我們把主庫(kù)部署到北京,兩個(gè)從庫(kù)部署在上海,同樣一份數(shù)據(jù)要在北上專線傳輸兩次,這樣會(huì)造成巨大的專線帶寬浪費(fèi)。另外,隨著業(yè)務(wù)的發(fā)展和演進(jìn),我們也在做單元化部署和異地多活架構(gòu)。用官方的主從同步,滿足不了我們的這些需求。基于此,我們又做了集群間的復(fù)制方案。
如上圖所示,這里畫出了北京的主集群以及上海的從集群,我們要做的是通過(guò)集群同步服務(wù),把北京主集群的數(shù)據(jù)同步到上海從集群上。按照流程,首先要向我們的同步調(diào)度模塊下發(fā)“在兩個(gè)集群間建立同步鏈路”的任務(wù),同步調(diào)度模塊會(huì)根據(jù)主從集群的拓?fù)浣Y(jié)構(gòu),把主從集群間的同步任務(wù)下發(fā)到同步集群,同步集群收到同步任務(wù)后會(huì)扮成 Redis 的 Slave,通過(guò) Redis 的復(fù)制協(xié)議,從主集群上的從庫(kù)拉取數(shù)據(jù),包括 RDB以及后續(xù)的增量變更。同步機(jī)收到數(shù)據(jù)后會(huì)把它轉(zhuǎn)成客戶端的寫命令,寫到上海從集群的主節(jié)點(diǎn)里。
通過(guò)這樣的方式,我們把北京主集群的數(shù)據(jù)同步到了上海的從集群。同樣的,我們要做異地多活也很簡(jiǎn)單,再加一個(gè)反向的同步鏈路,就可以實(shí)現(xiàn)集群間的雙向同步。
接下來(lái)我們講一下如何做好微觀角度的高可用,也就是保持端到端的高成功率。對(duì)于 Squirrel ,主要講如下三個(gè)影響成功率的問(wèn)題:
數(shù)據(jù)遷移造成超時(shí)抖動(dòng)。
持久化造成超時(shí)抖動(dòng)。
熱點(diǎn) Key 請(qǐng)求導(dǎo)致單節(jié)點(diǎn)過(guò)載。
Squirrel 智能遷移
對(duì)于數(shù)據(jù)遷移,我們主要遇到三個(gè)問(wèn)題:
Redis Cluster 雖然提供了數(shù)據(jù)遷移能力,但是對(duì)于要遷哪些 Slot,Slot 從哪遷到哪,它并不管。
做數(shù)據(jù)遷移的時(shí)候,大家都想越快越好,但是遷移速度過(guò)快又可能影響業(yè)務(wù)正常請(qǐng)求。
Redis 的 Migrate 命令會(huì)阻塞工作線程,尤其在遷移大 Value 的時(shí)候會(huì)阻塞特別久。
為了解決這些問(wèn)題,我們做了全新的遷移服務(wù)。
下面我們按照工作流,講一下它是如何運(yùn)行的。首先生成遷移任務(wù),這步的核心是“就近原則”,比如說(shuō)同機(jī)房的兩個(gè)節(jié)點(diǎn)做遷移肯定比跨機(jī)房的兩個(gè)節(jié)點(diǎn)快。遷移任務(wù)生成之后,會(huì)把任務(wù)下發(fā)到一批遷移機(jī)上。遷移機(jī)遷移的時(shí)候,有這樣幾個(gè)特點(diǎn):
會(huì)在集群內(nèi)遷出節(jié)點(diǎn)間做并發(fā),比如同時(shí)給 Redis 1、Redis 3 下發(fā)遷移命令。
每個(gè) Migrate 命令會(huì)遷移一批 Key。
我們會(huì)用監(jiān)控服務(wù)去實(shí)時(shí)采集客戶端的成功率、耗時(shí),服務(wù)端的負(fù)載、QPS 等,之后把這個(gè)狀態(tài)反饋到遷移機(jī)上。遷移數(shù)據(jù)的過(guò)程就類似 TCP 慢啟動(dòng)的過(guò)程,它會(huì)把速度一直往上加,若出現(xiàn)請(qǐng)求成功率下降等情況,它的速度就會(huì)降低,最終遷移速度會(huì)在動(dòng)態(tài)平衡中穩(wěn)定下來(lái),這樣就達(dá)到了最快速的遷移,同時(shí)又盡可能小地影響業(yè)務(wù)的正常請(qǐng)求。
接下來(lái),我們看一下大 Value 的遷移,我們實(shí)現(xiàn)了一個(gè)異步 Migrate 命令,該命令執(zhí)行時(shí),Redis 的主線程會(huì)繼續(xù)處理其他的正常請(qǐng)求。如果此時(shí)有對(duì)正在遷移 Key 的寫請(qǐng)求過(guò)來(lái),Redis 會(huì)直接返回錯(cuò)誤。這樣最大限度保證了業(yè)務(wù)請(qǐng)求的正常處理,同時(shí)又不會(huì)阻塞主線程。
Squirrel 持久化重構(gòu)
Redis 主從同步時(shí)會(huì)生成 RDB。生成 RDB 的過(guò)程會(huì)調(diào)用 Fork 產(chǎn)生一個(gè)子進(jìn)程去寫數(shù)據(jù)到硬盤,F(xiàn)ork 雖然有操作系統(tǒng)的 COW 機(jī)制,但是當(dāng)內(nèi)存用量達(dá)到 10 G 或 20 G 時(shí),依然會(huì)造成整個(gè)進(jìn)程接近秒級(jí)的阻塞。這對(duì)在線業(yè)務(wù)來(lái)說(shuō)幾乎是無(wú)法接受的。我們也會(huì)為數(shù)據(jù)可靠性要求高的業(yè)務(wù)去開啟 AOF,而開 AOF 就可能因 IO 抖動(dòng)造成進(jìn)程阻塞,這也會(huì)影響請(qǐng)求成功率。對(duì)官方持久化機(jī)制的這兩個(gè)問(wèn)題,我們的解決方案是重構(gòu)持久化機(jī)制。
上圖是我們最新版的 Redis 持久化機(jī)制,寫請(qǐng)求會(huì)先寫到 DB 里,然后寫到內(nèi)存 Backlog,這跟官方是一樣的。同時(shí)它會(huì)把請(qǐng)求發(fā)給異步線程,異步線程負(fù)責(zé)把變更刷到硬盤的 Backlog 里。當(dāng)硬盤 Backlog 過(guò)多時(shí),我們會(huì)主動(dòng)在業(yè)務(wù)低峰期做一次 RDB ,然后把 RDB 之前生成的 Backlog 刪除。
如果這時(shí)候我們要做主從同步,去尋找同步點(diǎn)的時(shí)候,該怎么辦?第一步還是跟官方一樣,我們會(huì)從內(nèi)存 Backlog 里找有沒(méi)有要求的同步點(diǎn),如果沒(méi)有,我們會(huì)去硬盤 Backlog 找同步點(diǎn)。由于硬盤空間很大,硬盤 Backlog 可以存儲(chǔ)特別多的數(shù)據(jù),所以很少會(huì)出現(xiàn)找不到同步點(diǎn)的情況。如果硬盤 Backlog 也沒(méi)有,我們就會(huì)觸發(fā)一次類似于全量重傳的操作,但這里的全量重傳是不需要當(dāng)場(chǎng)生成 RDB 的,它可以直接用硬盤已存的 RDB 及其之后的硬盤 Backlog 完成全量重傳。通過(guò)這個(gè)設(shè)計(jì),我們減少了很多的全量重傳。
另外,我們通過(guò)控制在低峰區(qū)生成 RDB ,減少了很多 RDB 造成的抖動(dòng)。同時(shí),我們也避免了寫 AOF 造成的抖動(dòng)。不過(guò),這個(gè)方案因?yàn)閷?AOF 是完全異步的,所以會(huì)比官方的數(shù)據(jù)可靠性差一些,但我們認(rèn)為這個(gè)代價(jià)換來(lái)了可用性的提升,這是非常值得的。
Squirrel 熱點(diǎn) Key
下面看一下 Squirrel 的熱點(diǎn) Key 解決方案。如下圖所示,普通主、從是一個(gè)正常集群中的節(jié)點(diǎn),熱點(diǎn)主、從是游離于正常集群之外的節(jié)點(diǎn)。我們看一下它們之間怎么發(fā)生聯(lián)系。
當(dāng)有請(qǐng)求進(jìn)來(lái)讀寫普通節(jié)點(diǎn)時(shí),節(jié)點(diǎn)內(nèi)會(huì)同時(shí)做請(qǐng)求 Key 的統(tǒng)計(jì)。如果某個(gè) Key 達(dá)到了一定的訪問(wèn)量或者帶寬的占用量,會(huì)自動(dòng)觸發(fā)流控以限制熱點(diǎn) Key 訪問(wèn),防止節(jié)點(diǎn)被熱點(diǎn)請(qǐng)求打滿。同時(shí),監(jiān)控服務(wù)會(huì)周期性的去所有 Redis 實(shí)例上查詢統(tǒng)計(jì)到的熱點(diǎn) Key。如果有熱點(diǎn),監(jiān)控服務(wù)會(huì)把熱點(diǎn) Key 所在 Slot 上報(bào)到我們的遷移服務(wù)。遷移服務(wù)這時(shí)會(huì)把熱點(diǎn)主從節(jié)點(diǎn)加入到這個(gè)集群中,然后把熱點(diǎn) Slot 遷移到這個(gè)熱點(diǎn)主從上。因?yàn)闊狳c(diǎn)主從上只有熱點(diǎn) Slot 的請(qǐng)求,所以熱點(diǎn) Key的處理能力得到了大幅提升。通過(guò)這樣的設(shè)計(jì),我們可以做到實(shí)時(shí)的熱點(diǎn)監(jiān)控,并及時(shí)通過(guò)流控去止損;通過(guò)熱點(diǎn)遷移,我們能做到自動(dòng)的熱點(diǎn)隔離和快速的容量擴(kuò)充。
持久化 KV Cellar 架構(gòu)和實(shí)踐
下面看一下持久化 KV Cellar 的架構(gòu)和實(shí)踐。下圖是我們最新的 Cellar 架構(gòu)圖。
跟阿里開源的 Tair 主要有兩個(gè)架構(gòu)上的不同。第一個(gè)是OB,第二個(gè)是 ZooKeeper。我們的 OB 跟 ZooKeeper 的 Observer 是類似的作用,提供 Cellar 中心節(jié)點(diǎn)元數(shù)據(jù)的查詢服務(wù)。它可以實(shí)時(shí)與中心節(jié)點(diǎn)的 Master 同步最新的路由表,客戶端的路由表都是從 OB 去拿。這樣做的好處主要有兩點(diǎn),第一,把大量的業(yè)務(wù)客戶端跟集群的大腦 Master 做了天然的隔離,防止路由表請(qǐng)求影響集群的管理。第二,因?yàn)?OB 只供路由表查詢,不參與集群的管理,所以它可以進(jìn)行水平擴(kuò)展,極大地提升了我們路由表的查詢能力。另外,我們引入了 ZooKeeper 做分布式仲裁,解決我剛才提到的 Master、Slave 在網(wǎng)絡(luò)分割情況下的“腦裂”問(wèn)題,并且通過(guò)把集群的元數(shù)據(jù)存儲(chǔ)到 ZooKeeper,我們保證了元數(shù)據(jù)的高可靠。
Cellar 節(jié)點(diǎn)容災(zāi)
介紹完整體的架構(gòu),我們看一下 Cellar 怎么做節(jié)點(diǎn)容災(zāi)。一個(gè)集群節(jié)點(diǎn)的宕機(jī)一般是臨時(shí)的,一個(gè)節(jié)點(diǎn)的網(wǎng)絡(luò)抖動(dòng)也是臨時(shí)的,它們會(huì)很快地恢復(fù),并重新加入集群。因?yàn)楣?jié)點(diǎn)的臨時(shí)離開就把它徹底摘除,并做數(shù)據(jù)副本補(bǔ)全操作,會(huì)消耗大量資源,進(jìn)而影響到業(yè)務(wù)請(qǐng)求。所以,我們實(shí)現(xiàn)了 Handoff 機(jī)制來(lái)解決這種節(jié)點(diǎn)短時(shí)故障帶來(lái)的影響。
如上圖所示 ,如果 A 節(jié)點(diǎn)宕機(jī)了,會(huì)觸發(fā) Handoff 機(jī)制,這時(shí)候中心節(jié)點(diǎn)會(huì)通知客戶端 A節(jié)點(diǎn)發(fā)生了故障,讓客戶端把分片 1 的請(qǐng)求也打到 B 上。B 節(jié)點(diǎn)正常處理完客戶端的讀寫請(qǐng)求之后,還會(huì)把本應(yīng)該寫入 A 節(jié)點(diǎn)的分片 1&2 數(shù)據(jù)寫入到本地的 Log 中。
如果 A 節(jié)點(diǎn)宕機(jī)后 3~5 分鐘,或者網(wǎng)絡(luò)抖動(dòng) 30~50 秒之后恢復(fù)了,A 節(jié)點(diǎn)就會(huì)上報(bào)心跳到中心節(jié)點(diǎn),中心節(jié)點(diǎn)就會(huì)通知 B 節(jié)點(diǎn):“ A 節(jié)點(diǎn)恢復(fù)了,你去把它不在期間的數(shù)據(jù)傳給它。”這時(shí)候,B 節(jié)點(diǎn)就會(huì)把本地存儲(chǔ)的 Log 回寫到 A 節(jié)點(diǎn)上。等到 A 節(jié)點(diǎn)擁有了故障期間的全量數(shù)據(jù)之后,中心節(jié)點(diǎn)就會(huì)告訴客戶端,A 節(jié)點(diǎn)已經(jīng)徹底恢復(fù)了,客戶端就可以重新把分片 1 的請(qǐng)求打回 A 節(jié)點(diǎn)。
通過(guò)這樣的操作,我們可以做到秒級(jí)的快速節(jié)點(diǎn)摘除,而且節(jié)點(diǎn)恢復(fù)后加回,只需補(bǔ)齊少量的增量數(shù)據(jù)。另外如果 A 節(jié)點(diǎn)要做升級(jí),中心節(jié)點(diǎn)先通過(guò)主動(dòng) Handoff 把 A 節(jié)點(diǎn)流量切到 B 節(jié)點(diǎn),A 升級(jí)后再回寫增量 Log,然后切回流量加入集群。這樣通過(guò)主動(dòng)觸發(fā) Handoff 機(jī)制,我們就實(shí)現(xiàn)了靜默升級(jí)的功能。
Cellar 跨地域容災(zāi)
下面我介紹一下 Cellar 跨地域容災(zāi)是怎么做的。Cellar 跟 Squirrel 面對(duì)的跨地域容災(zāi)問(wèn)題是一樣的,解決方案同樣也是集群間復(fù)制。以下圖一個(gè)北京主集群、上海從集群的跨地域場(chǎng)景為例,比如說(shuō)客戶端的寫操作到了北京的主集群 A 節(jié)點(diǎn),A 節(jié)點(diǎn)會(huì)像正常集群內(nèi)復(fù)制一樣,把它復(fù)制到 B 和 D 節(jié)點(diǎn)上。同時(shí) A 節(jié)點(diǎn)還會(huì)把數(shù)據(jù)復(fù)制一份到從集群的 H 節(jié)點(diǎn)。H 節(jié)點(diǎn)處理完集群間復(fù)制寫入之后,它也會(huì)做從集群內(nèi)的復(fù)制,把這個(gè)寫操作復(fù)制到從集群的 I 、K 節(jié)點(diǎn)上。通過(guò)在主從集群的節(jié)點(diǎn)間建立這樣一個(gè)復(fù)制鏈路,我們完成了集群間的數(shù)據(jù)復(fù)制,并且這個(gè)復(fù)制保證了最低的跨地域帶寬占用。同樣的,集群間的兩個(gè)節(jié)點(diǎn)通過(guò)配置兩個(gè)雙向復(fù)制的鏈路,就可以達(dá)到雙向同步異地多活的效果。
Cellar 強(qiáng)一致
我們做好了節(jié)點(diǎn)容災(zāi)以及跨地域容災(zāi)后,業(yè)務(wù)又對(duì)我們提出了更高要求:強(qiáng)一致存儲(chǔ)。我們之前的數(shù)據(jù)復(fù)制是異步的,在做故障摘除時(shí),可能因?yàn)楣收瞎?jié)點(diǎn)數(shù)據(jù)還沒(méi)復(fù)制出來(lái),導(dǎo)致數(shù)據(jù)丟失。但是對(duì)于金融支付等場(chǎng)景來(lái)說(shuō),它們是不容許數(shù)據(jù)丟失的。面對(duì)這個(gè)難題,我們?cè)撛趺唇鉀Q?目前業(yè)界主流的解決方案是基于 Paxos 或 Raft 協(xié)議的強(qiáng)一致復(fù)制。我們最終選擇了 Raft 協(xié)議。主要是因?yàn)?Raft 論文是非常詳實(shí)的,是一篇工程化程度很高的論文。業(yè)界也有不少比較成熟的 Raft 開源實(shí)現(xiàn),可以作為我們研發(fā)的基礎(chǔ),進(jìn)而能夠縮短研發(fā)周期。
下圖是現(xiàn)在 Cellar 集群 Raft 復(fù)制模式下的架構(gòu)圖,中心節(jié)點(diǎn)會(huì)做 Raft 組的調(diào)度,它會(huì)決定每一個(gè) Slot 的三副本存在哪些節(jié)點(diǎn)上。
大家可以看到 Slot 1 在存儲(chǔ)節(jié)點(diǎn) 1、2、4 上,Slot 2 在存儲(chǔ)節(jié)點(diǎn)2、3、4上。每個(gè) Slot 組成一個(gè) Raft 組,客戶端會(huì)去 Raft Leader 上進(jìn)行讀寫。由于我們是預(yù)分配了 16384 個(gè) Slot,所以,在集群規(guī)模很小的時(shí)候,我們的存儲(chǔ)節(jié)點(diǎn)上可能會(huì)有數(shù)百甚至上千個(gè) Slot 。
這時(shí)候如果每個(gè) Raft 復(fù)制組都有自己的復(fù)制線程、 復(fù)制請(qǐng)求和 Log等,那么資源消耗會(huì)非常大,寫入性能會(huì)很差。所以我們做了 Multi Raft 實(shí)現(xiàn), Cellar 會(huì)把同一個(gè)節(jié)點(diǎn)上所有的 Raft 復(fù)制組寫一份 Log,用同一組線程去做復(fù)制,不同 Raft 組間的復(fù)制包也會(huì)按照目標(biāo)節(jié)點(diǎn)做整合,以保證寫入性能不會(huì)因 Raft 組過(guò)多而變差。Raft 內(nèi)部其實(shí)是有自己的選主機(jī)制,它可以控制自己的主節(jié)點(diǎn),如果有任何節(jié)點(diǎn)宕機(jī),它可以通過(guò)選舉機(jī)制選出新的主節(jié)點(diǎn)。
那么,中心節(jié)點(diǎn)是不是就不需要管理 Raft 組了嗎?不是的。這里講一個(gè)典型的場(chǎng)景,如果一個(gè)集群的部分節(jié)點(diǎn)經(jīng)過(guò)幾輪宕機(jī)恢復(fù)的過(guò)程, Raft Leader 在存儲(chǔ)節(jié)點(diǎn)之間會(huì)變得極其不均。而為了保證數(shù)據(jù)的強(qiáng)一致,客戶端的讀寫流量又必須發(fā)到 Raft Leader,這時(shí)候集群的節(jié)點(diǎn)流量會(huì)很不均衡。所以我們的中心節(jié)點(diǎn)還會(huì)做 Raft 組的 Leader 調(diào)度。比如說(shuō) Slot 1 存儲(chǔ)在節(jié)點(diǎn) 1、2、4,并且節(jié)點(diǎn) 1 是 Leader。如果節(jié)點(diǎn) 1 掛了,Raft 把節(jié)點(diǎn) 2 選成了 Leader。然后節(jié)點(diǎn) 1 恢復(fù)了并重新加入集群,中心節(jié)點(diǎn)這時(shí)會(huì)讓節(jié)點(diǎn) 2 把 Leader 還給節(jié)點(diǎn) 1 。這樣,即便經(jīng)過(guò)一系列宕機(jī)和恢復(fù),我們存儲(chǔ)節(jié)點(diǎn)之間的 Leader 數(shù)目仍然能保證是均衡的。
接下來(lái),我們看一下 Cellar 如何保證它的端到端高成功率。這里也講三個(gè)影響成功率的問(wèn)題。Cellar 遇到的數(shù)據(jù)遷移和熱點(diǎn) Key 問(wèn)題與 Squirrel 是一樣的,但解決方案不一樣。這是因?yàn)?Cellar 走的是自研路徑,不用考慮與官方版本的兼容性,對(duì)架構(gòu)改動(dòng)更大些。另一個(gè)問(wèn)題是慢請(qǐng)求阻塞服務(wù)隊(duì)列導(dǎo)致大面積超時(shí),這是 Cellar 網(wǎng)絡(luò)、工作多線程模型設(shè)計(jì)下會(huì)遇到的不同問(wèn)題。
Cellar 智能遷移
上圖是 Cellar 智能遷移架構(gòu)圖。我們把桶的遷移分成了三個(gè)狀態(tài)。第一個(gè)狀態(tài)就是正常的狀態(tài),沒(méi)有任何遷移。如果這時(shí)候要把 Slot 2 從 A 節(jié)點(diǎn)遷移到 B節(jié)點(diǎn),A 會(huì)給 Slot 2 打一個(gè)快照,然后把這個(gè)快照全量發(fā)到 B 節(jié)點(diǎn)上。在遷移數(shù)據(jù)的時(shí)候, B 節(jié)點(diǎn)的回包會(huì)帶回 B 節(jié)點(diǎn)的狀態(tài)。B 的狀態(tài)包括什么?引擎的壓力、網(wǎng)卡流量、隊(duì)列長(zhǎng)度等。A 節(jié)點(diǎn)會(huì)根據(jù) B 節(jié)點(diǎn)的狀態(tài)調(diào)整自己的遷移速度。像 Squirrel 一樣,它經(jīng)過(guò)一段時(shí)間調(diào)整后,遷移速度會(huì)達(dá)到一個(gè)動(dòng)態(tài)平衡,達(dá)到最快速的遷移,同時(shí)又盡可能小地影響業(yè)務(wù)的正常請(qǐng)求。
當(dāng) Slot 2 遷移完后, 會(huì)進(jìn)入圖中 Slot 3 的狀態(tài)。客戶端這時(shí)可能還沒(méi)更新路由表,當(dāng)它請(qǐng)求到了 A 節(jié)點(diǎn),A 節(jié)點(diǎn)會(huì)發(fā)現(xiàn)客戶端請(qǐng)求錯(cuò)了節(jié)點(diǎn),但它不會(huì)返回錯(cuò)誤,它會(huì)把請(qǐng)求代理到 B 節(jié)點(diǎn)上,然后把 B 的響應(yīng)包再返回客戶端。同時(shí)它會(huì)告訴客戶端,需要更新一下路由表了,此后客戶端就能直接訪問(wèn)到 B 節(jié)點(diǎn)。這樣就解決了客戶端路由更新延遲造成的請(qǐng)求錯(cuò)誤。
Cellar 快慢列隊(duì)
下圖上方是一個(gè)標(biāo)準(zhǔn)的線程隊(duì)列模型。網(wǎng)絡(luò)線程池接收網(wǎng)絡(luò)流量解析出請(qǐng)求包,然后把請(qǐng)求放到工作隊(duì)列里,工作線程池會(huì)從工作隊(duì)列取請(qǐng)求來(lái)處理,然后把響應(yīng)包放回網(wǎng)絡(luò)線程池發(fā)出。
我們分析線上發(fā)生的超時(shí)案例時(shí)發(fā)現(xiàn),一批超時(shí)請(qǐng)求當(dāng)中往往只有一兩個(gè)請(qǐng)求是引擎處理慢導(dǎo)致的,大部分請(qǐng)求,只是因?yàn)樵陉?duì)列等待過(guò)久導(dǎo)致整體響應(yīng)時(shí)間過(guò)長(zhǎng)而超時(shí)了。從線上分析來(lái)看,真正的慢請(qǐng)求占超時(shí)請(qǐng)求的比例只有 1/20。
我們的解法是什么樣?很簡(jiǎn)單,拆線程池、拆隊(duì)列。我們的網(wǎng)絡(luò)線程在收到包之后,會(huì)根據(jù)它的請(qǐng)求特點(diǎn),是讀還是寫,快還是慢,分到四個(gè)隊(duì)列里。讀寫請(qǐng)求比較好區(qū)分,但快慢怎么分開?我們會(huì)根據(jù)請(qǐng)求的 Key 個(gè)數(shù)、Value大小、數(shù)據(jù)結(jié)構(gòu)元素?cái)?shù)等對(duì)請(qǐng)求進(jìn)行快慢區(qū)分。然后用對(duì)應(yīng)的四個(gè)工作線程池處理對(duì)應(yīng)隊(duì)列的請(qǐng)求,就實(shí)現(xiàn)了快慢讀寫請(qǐng)求的隔離。這樣如果我有一個(gè)讀的慢請(qǐng)求,不會(huì)影響另外三種請(qǐng)求的正常處理。不過(guò)這樣也會(huì)帶來(lái)一個(gè)問(wèn)題,我們的線程池從一個(gè)變成四個(gè),那線程數(shù)是不是變成原來(lái)的四倍?其實(shí)并不是的,我們某個(gè)線程池空閑的時(shí)候會(huì)去幫助其它的線程池處理請(qǐng)求。所以,我們線程池變成了四個(gè),但是線程總數(shù)并沒(méi)有變。我們線上驗(yàn)證中這樣的設(shè)計(jì)能把服務(wù) TP999 的延遲降低 86%,可大幅降低超時(shí)率。
Cellar 熱點(diǎn) Key
上圖是 Cellar 熱點(diǎn) Key 解決方案的架構(gòu)圖。我們可以看到中心節(jié)點(diǎn)加了一個(gè)職責(zé),多了熱點(diǎn)區(qū)域管理,它現(xiàn)在不只負(fù)責(zé)正常的數(shù)據(jù)副本分布,還要管理熱點(diǎn)數(shù)據(jù)的分布,圖示這個(gè)集群在節(jié)點(diǎn) C、D 放了熱點(diǎn)區(qū)域。我們通過(guò)讀寫流程看一下這個(gè)方案是怎么運(yùn)轉(zhuǎn)的。如果客戶端有一個(gè)寫操作到了 A 節(jié)點(diǎn),A 節(jié)點(diǎn)處理完成后,會(huì)根據(jù)實(shí)時(shí)的熱點(diǎn)統(tǒng)計(jì)結(jié)果判斷寫入的 Key 是否為熱點(diǎn)。
如果這個(gè) Key 是一個(gè)熱點(diǎn),那么它會(huì)在做集群內(nèi)復(fù)制的同時(shí),還會(huì)把這個(gè)數(shù)據(jù)復(fù)制有熱點(diǎn)區(qū)域的節(jié)點(diǎn),也就是圖中的 C、D 節(jié)點(diǎn)。同時(shí),存儲(chǔ)節(jié)點(diǎn)在返回結(jié)果給客戶端時(shí),會(huì)告訴客戶端,這個(gè) Key 是熱點(diǎn),這時(shí)客戶端內(nèi)會(huì)緩存這個(gè)熱點(diǎn) Key。當(dāng)客戶端有這個(gè) Key 的讀請(qǐng)求時(shí),它就會(huì)直接去熱點(diǎn)區(qū)域做數(shù)據(jù)的讀取。通過(guò)這樣的方式,我們可以做到只對(duì)熱點(diǎn)數(shù)據(jù)做擴(kuò)容,不像 Squirrel ,要把整個(gè) Slot 遷出來(lái)做擴(kuò)容。有必要的話,中心節(jié)點(diǎn)也可以把熱點(diǎn)區(qū)域放到集群的所有節(jié)點(diǎn)上,所有的熱點(diǎn)讀請(qǐng)求就能均衡的分到所有節(jié)點(diǎn)上。另外,通過(guò)這種實(shí)時(shí)的熱點(diǎn)數(shù)據(jù)復(fù)制,我們很好地解決了類似客戶端緩存熱點(diǎn) KV 方案造成的一致性問(wèn)題。
發(fā)展規(guī)劃和業(yè)界趨勢(shì)
最后,一起來(lái)看看我們項(xiàng)目的規(guī)劃和業(yè)界的技術(shù)趨勢(shì)。這部分內(nèi)容會(huì)按照服務(wù)、系統(tǒng)、硬件三層來(lái)進(jìn)行闡述。首先在服務(wù)層,主要有三點(diǎn):
Redis Gossip 協(xié)議優(yōu)化。大家都知道 Gossip 協(xié)議在集群的規(guī)模變大之后,消息量會(huì)劇增,它的 Failover 時(shí)間也會(huì)變得越來(lái)越長(zhǎng)。所以當(dāng)集群規(guī)模達(dá)到 TB 級(jí)后,集群的可用性會(huì)受到很大的影響,所以我們后面會(huì)重點(diǎn)在這方面做一些優(yōu)化。
我們已經(jīng)在 Cellar 存儲(chǔ)節(jié)點(diǎn)的數(shù)據(jù)副本間做了 Raft 復(fù)制,可以保證數(shù)據(jù)強(qiáng)一致,后面我們會(huì)在 Cellar 的中心點(diǎn)內(nèi)部也做一個(gè) Raft 復(fù)制,這樣就不用依賴于 ZooKeeper 做分布式仲裁、元數(shù)據(jù)存儲(chǔ)了,我們的架構(gòu)也會(huì)變得更加簡(jiǎn)單、可靠。
Squirrel 和 Cellar 雖然都是 KV 存儲(chǔ),但是因?yàn)樗鼈兪腔诓煌拈_源項(xiàng)目研發(fā)的,所以 API 和訪問(wèn)協(xié)議不同,我們之后會(huì)考慮將 Squirrel 和 Cellar 在 SDK 層做整合,雖然后端會(huì)有不同的存儲(chǔ)集群,但業(yè)務(wù)側(cè)可以用一套 SDK 進(jìn)行訪問(wèn)。
在系統(tǒng)層面,我們正在調(diào)研并去落地一些 Kernel Bypass 技術(shù),像 DPDK、SPDK 這種網(wǎng)絡(luò)和硬盤的用戶態(tài) IO 技術(shù)。它可以繞過(guò)內(nèi)核,通過(guò)輪詢機(jī)制訪問(wèn)這些設(shè)備,可以極大提升系統(tǒng)的 IO 能力。存儲(chǔ)作為 IO 密集型服務(wù),性能會(huì)獲得大幅的提升。
在硬件層面,像支持 RDMA 的智能網(wǎng)卡能大幅降低網(wǎng)絡(luò)延遲和提升吞吐;還有像 3D XPoint 這樣的閃存技術(shù),比如英特爾新發(fā)布的 AEP 存儲(chǔ),其訪問(wèn)延遲已經(jīng)比較接近內(nèi)存了,以后閃存跟內(nèi)存之間的界限也會(huì)變得越來(lái)越模糊;最后,看一下計(jì)算型硬件,比如通過(guò)在閃存上加 FPGA 卡,把原本應(yīng)該 CPU 做的工作,像數(shù)據(jù)壓縮、解壓等,下沉到卡上執(zhí)行,這種硬件能在解放 CPU 的同時(shí),也可以降低服務(wù)的響應(yīng)延遲。
作者簡(jiǎn)介
澤斌,美團(tuán)點(diǎn)評(píng)高級(jí)技術(shù)專家,2014 年加入美團(tuán)。
編輯:hfy
評(píng)論
查看更多