基於 Redis 構建資料服務

來源:互聯網
上載者:User

標籤:

今天我們來聊聊如何基於redis資料庫擴充資料服務,如何?分區(sharding)以及高可用(high availability)。

分布式系統不存在完美的設計,處處都體現了trade off。

因此我們在開始本文前,需要確定後續的討論原則,仍然以分布式系統設計中的CAP原則為例。由於主角是redis,那效能表現肯定是最高設計目標,之後討論過程中的所有抉擇,都會優先考慮CAP中的AP性質。


兩個點按順序來,先看分區。

何謂分區?簡單來說,就是對單機redis做水平擴充。

當然,做遊戲的同學可能要問了,一服一個redis,為什麼需要水平擴充?這個話題我們在之前幾篇文章中都有討論,可以看這裡,或這裡,小說君不再贅述。

如果要實現服務等級的複用,那麼資料服務的定位往往是全域服務。如此僅用單一實例的redis就難以應對多變的負載情況——畢竟redis是單線程的。

從mysql一路用過來的同學這時都會習慣性地水平分割,redis中也是類似的原理,將整體的資料進行切分,每一部分是一個分區(shard),不同的分區維護不同的key集合。

那麼,分區問題的實質就是如何基於多個redis執行個體設計全域統一的資料服務。同時,有一個約束條件,那就是我們無法保證強一致性。

也就是說,資料服務進行分區擴充的前提是,不提供跨分區事務的保障。redis cluster也沒有提供類似支援,因為分散式交易本來就跟redis的定位是有衝突的。

因此,我們的分區方案有兩個限制:

·不同分區中的資料一定是嚴格隔離的,比如是不同組服的資料,或者是完全不相干的資料。要想實現跨分區的資料互動,必須依賴更上層的協調機制保證,資料服務層面不做任何承諾。 而且這樣一來,如果想給應用程式層提供協調機制,只要在每個分區上部署上篇文章介紹的 單一實例簡易鎖機制 即可,簡單明了。

·我們的分區方案無法在分區間做類似分布式儲存系統的資料冗餘機制,換言之,一份資料交叉存在多個分區中。

如何?分區?

首先,我們要確定分區方案需要解決什麼問題。

分區的redis叢集,實際上共同組成了一個具狀態服務(stateful service)。設計具狀態服務,我們通常會從兩點考慮:

cluster membership,系統間各個節點,或者說各個分區的關係是怎樣的。

work distribution,外部請求應該如何、交由哪個節點處理,或者說使用者(以下都簡稱dbClient)的一次讀或寫應該去找哪個分區。

針對第一個問題,解決方案通常有三:

·presharding,也就是sharding靜態配置。

·

·gossip protocol,其實就是redis cluster採用的方案。簡單地說就是叢集中每個節點會由於網路分化、節點抖動等原因而具有不同的叢集全域視圖。節點之間通過gossip protocol進行節點資訊共用。這是業界比較流行的去中心化的方案。

·consensus system,這種方案跟上一種正相反,是依賴外部分布式一致性設施,由其仲裁來決定叢集中各節點的身份。

·需求決定解決方案,小說君認為,對於遊戲服務端以及大多數應用型後端情景,後兩者的成本太高,會增加很多不確定的複雜性,因此兩種方案都不是合適的選擇。 而且,大部分服務通常是可以在設計階段確定每個分區的容量上限的,也不需要太複雜的機制支援。

但是presharding的缺點也很明顯,做不到動態增容減容,而且無法高可用。不過其實只要稍加改造,就足以滿足需求了。

不過,在談具體的改造措施之前,我們先看之前提出的分區方案要解決的第二個問題—— work distribution 。

這個問題實際上是從另一種維度看分區,解決方案很多,但是如果從對架構的影響上來看,大概分為兩種:

·一種是proxy-based,基於額外的轉送 Proxy。例子有twemproxy/Codis。

·一種是client sharding,也就是dbClient(每個對資料服務有需求的服務)維護sharding規則,自助式選擇要去哪個redis執行個體。redis cluster本質上就屬於這種,dblient側緩衝了部分sharding資訊。

第一種方案的缺點顯而易見——在整個架構中增加了額外的間接層,流程中增加了一趟round-trip。如果是像twemproxy或者Codis這種支援高可用的還好,但是github上隨便一翻還能找到特別多的沒法做到高可用的proxy-based方案,無緣無故多個單點,這樣就完全搞不明白sharding的意義何在了。

第二種方案的缺點,小說君能想到的就是叢集狀態發生變化的時候沒法即時通知到dbClient。

第一種方案,我們其實可以直接pass掉了。因為這種方案更適合私人雲端的情景,開發資料服務的部門有可能和業務部門相去甚遠,因此需要統一的轉送 Proxy服務。但是對於一些簡單的應用開發情景,資料服務邏輯服務都是一幫人寫的,沒什麼增加額外中介層的必要。

那麼,看起來只能選擇第二種方案了。

將presharding與client sharding結合起來後,現在我們的成果是:資料服務是全域的,redis可以開多個執行個體,不相干的資料需要到不同的分區上存取,dbClient掌握這個映射關係。


不過目前的方案只能算是滿足了應用對資料服務的基本需求。

遊戲行業中,大部分採用redis的團隊,一般最終會選定這個方案作為自己的資料服務。後續的擴充其實對他們來說不是不可以做,但是可能有維護上的複雜性與不確定性。

但是作為一名有操守的程式員,小說君選擇繼續擴充。

現在的這個方案存在兩個問題:

·首先,雖然我們沒有支援線上資料移轉的必要,但是離線資料移轉是必須得有的,畢竟presharding做不到萬無一失。而在這個方案中,如果用單純的雜湊演算法,增加一個shard會導致原先的key到shard的對應關係變得非常亂,抬高資料移轉成本。

·其次,分區方案固然可以將整個資料服務的崩潰風險分散在不同shard中,比如相比於不分區的資料服務,一台機器掛掉了,隻影響到一部分client。但是,我們理應可以對資料服務做更深入的擴充,讓其可用程度更強。

針對第一個問題,處理方式跟proxy-based採用的處理方式沒太大區別,由於目前的資料服務方案比較簡單,採用一致性雜湊即可。或者採用一種比較簡單的兩段映射,第一段是靜態固定雜湊,第二段是動態可配置map。前者通過演算法,後者通過map配置維護的方式,都能最小化影響到的key集合。

而對於第二個問題,解決方案就是實現高可用。

如何讓資料服務高可用?在 討論這個問題之前,我們首先看redis如何?「 可用性」。

對於redis來說,可用性的本質是什嗎?其實就是redis執行個體掛掉之後可以有後備節點頂上。

redis通過兩種機制支援這一點。

第一種機制是replication。 通常的replication方案主要分為兩種。

·一種是active-passive,也就是active節點先修改自身狀態,然後寫統一持久化log,然後passive節點讀log跟進狀態。

·另一種是active-active,寫請求統一寫到持久化log,然後每個active節點自動同步log進度。

redis的replication方案採用的是一種一致性較弱的active-passive方案。也就是master自身維護log,將log向其他slave同步,master掛掉有可能導致部分log丟失,client寫完master即可收到成功返回,是一種非同步replication。

這個機制只能解決節點資料冗餘的問題,redis要具有可用性就還得解決redis執行個體掛掉讓備胎自動頂上的問題,畢竟由人肉去監控master狀態再人肉切換是不現實的。 因此還需要第二種機制。

第二種機制是redis內建的能夠自動化fail-over的redis sentinel。reds sentinel實際上是一種特殊的redis執行個體,其本身就是一種高可用服務——可以多開,可以自動服務發現(基於redis內建的pub-sub支援,sentinel並沒有禁用掉pub-sub的command map),可以自主leader election(基於 raft演算法 實現,作為 sentinel的一個模組 ),然後在發現master掛掉時由leader發起fail-over,並將掉線後再上線的master降為新master的slave。

redis基於這兩種機制,已經能夠實現一定程度的可用性。


接下來,我們來看資料服務如何高可用。

資料服務具有可用性的本質是什嗎?除了能實現redis可用性的需求——redis執行個體資料冗餘、故障自動切換之外,還需要將切換的訊息通知到每個dbClient。

也就是說把最開始的圖,改成下面這個樣子:

每個分區都要改成主從模式。

如果redis sentinel負責主從切換,拿最自然的想法就是讓dbClient向sentinel請求當前節點主從串連資訊。但是redis sentinel本身也是redis執行個體,數量也是動態,redis sentinel的串連資訊不僅在配置上成了一個難題,動態更新時也會有各種問題。

而且,redis sentinel本質上是整個服務端的static parts(要向dbClient提供服務),但是卻依賴於redis的啟動,並不是特別優雅。另一方面,dbClient要想問redis sentinel要到當前串連資訊,只能依賴其內建的pub-sub機制。redis的pub-sub只是一個簡單的訊息分發,沒有訊息持久化,因此需要輪詢式的請求串連資訊模型。

那麼,我們是否可以以較低的成本定製一種服務,既能取代redis sentinel,又能解決上述問題?

回憶下前文我們解決resharding問題的思路:

1.一致性雜湊。

2.採用一種比較簡單的兩段映射,第一段是靜態固定雜湊,第二段是動態可配置map。前者通過演算法,後者通過map配置維護的方式,都能最小化影響到的key集合。

兩種方案都可以實現動態resharding,dbClient可以動態更新:

·如果採用兩段映射,那麼我們可以動態下發第二段的配置資料。

·如果採用一致性雜湊,那麼我們可以動態下發分區的串連資訊。

再梳理一下,我們要實現的服務(下文簡稱為watcher),至少要實現這些需求 :

·要能夠監控redis的生存狀態。這一點實現起來很簡單,週期性PING redis執行個體即可。需要的資訊以及做出客觀下線和主觀下線的判斷依據都可以直接照搬sentinel實現。

·要做到自主服務發現,包括其他watcher的發現與所監控的master-slave組中的新節點的發現。在實現上,前者可以基於訊息佇列的pub-sub功能,後者只要向redis執行個體定期INFO擷取資訊即可。

·要在發現master客觀下線的時候選出leader進行後續的容錯移轉流程。這部分實現起來算是最複雜的部分,接下來會集中討論。

·選出leader之後將一個最合適的slave提升為master,然後等老的master再上線了就把它降級為新master的slave。

解決這些問題,watcher就兼具了擴充性、定製性,同時還提供分區資料服務的部分線上遷移機制。這樣,我們的資料服務也就更加健壯,可用程度更高。

這樣一來,雖然保證了redis每個分區的master-slave組具有可用性,但是因為我們引入了新的服務,那就引入了新的不確定性——如果引入這個服務的同時還要保證資料服務具有可用性,那我們就還得保證這個服務本身是可用的。

說起來可能有點繞,換個說法,也就是服務A藉助服務B實現了高可用,那麼服務B本身也需要高可用。

先簡單介紹一下redis sentinel是如何做到高可用的。同時監控同一組主從的sentinel可以有多個,master掛掉的時候,這些sentinel會根據redis自己實現的一種raft演算法選舉出leader,演算法流程也不是特別複雜,至少比paxos簡單多了。所有sentinel都是follower,判斷出master客觀下線的sentinel會升級成candidate同時向其他follower拉票,所有follower同一epoch內只能投給第一個向自己拉票的candidate。在具體表現中,通常一兩個epoch就能保證形成多數派,選出leader。有了leader,後面再對redis做SLAVEOF的時候就容易多了。


如果想用watcher取代sentinel,最複雜的實現細節可能就是這部分邏輯了。

這部分邏輯說白了就是要在分布式系統中維護一個一致狀態,舉個例子,可以將「誰是leader 」這個概念當作一個狀態量,由分布式系統中的身份相等的幾個節點共同維護,既然誰都有可能修改這個變數,那究竟誰的修改才奏效呢?

幸好,針對這種常見的問題情景,我們有現成的基礎設施抽象可以解決。

這種基礎設施就是分布式系統的協調器組件(coordinator),老牌的有zookeeper(基於對paxos改進過的zab協議,下面都簡稱zk了),新一點的有etcd(這個大家都清楚,基於raft協議)。這種組件通常沒有重複開發的必要,像paxos這種演算法理解起來都得老半天,實現起來的細節數量級更是難以想象。因此很多開源項目都是依賴這兩者實現高可用的,比如codis一開始就是用的zk。

zk解決了什麼問題?

以通用的應用服務需求來說,zk可以用來選leader,還可以用來維護dbClient的配置資料——dbClient直接去找zk要資料就行了。

zk的具體原理小說君就不再介紹了,有時間有精力可以研究下paxos,看看lamport的paper,沒時間沒精力的話搜一下看看zk實現原理的部落格就行了。

簡單介紹下如何基於zk實現leader election。zk提供了一個類似於os檔案系統的目錄結構,目錄結構上的每個節點都有類型的概念同時可以儲存一些資料。zk還提供了一次性觸發的watch機制。

應用程式層要做leader election就可以基於這幾點概念實現。

假設有某個目錄節點「 /election 」,watcher1啟動的時候在這個節點下面建立一個子節點,節點類型是臨時順序節點,也就是說這個節點會隨建立者掛掉而掛掉,順序的意思就是會在節點的名字後面加個數字尾碼,唯一標識這個節點在 「 /election 」 的子節點中的id。

·一個簡單的方案是讓每個watcher都watch 「 /election 」 的所有子節點,然後看自己的id是否是最小的,如果是就說明自己是leader,然後告訴應用程式層自己是leader,讓應用程式層進行後續操作就行了。但是這樣會產生驚群效應,因為一個子節點刪除,每個watcher都會收到通知,但是至多一個watcher會從follower變為leader。

·最佳化一些的方案是每個節點都關注比自己小一個排位的節點。這樣如果id最小的節點掛掉之後,id次小的節點會收到通知然後瞭解到自己成為了leader,避免了驚群效應。

小說君在實踐中發現,還有一點需要注意,臨時順序節點的臨時性體現在一次session而不是一次串連的終止。

例如watcher1每次申請節點都叫watcher1,第一次它申請成功的節點全名假設是watcher10002(後面的是zk自動加的序號),然後下線,watcher10002節點還會存在一段時間,如果這段時間內watcher1再上線,再嘗試建立watcher1就會失敗,然後之前的節點過一會兒就因為session逾時而銷毀,這樣就相當於這個watcher1消失了。

解決方案有兩個,可以建立節點前先顯式delete一次,也可以通過其他機制保證每次建立節點的名字不同,比如guid。

至於配置下發,就更簡單了。配置變更時直接更新節點資料,就能藉助zk通知到關注的dbClient,這種事件通知機制相比於輪詢請求sentinel要配置資料的機制更加優雅。

看下最後的架構圖:


來源:公眾帳號






基於 Redis 構建資料服務

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.