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