Redis Persistent Replication Sentinel Cluster的一些理解

Redis Persistent Replication Sentinel Cluster的一些理解

我喜歡把工做中接觸到的各類數據庫叫作存儲系統,籠統地說:Redis、Mysql、Kafka、ElasticSearch 均可以視爲存儲系統。各個存儲系統在持久化刷盤策略、checkpoint機制、事務機制、數據的可靠性保證、高可用性保證的一些實現細節是深刻理解背後存儲原理的基礎,把它們對比起來看,也能更好地理解。在寫代碼的時候,也許只須要了解它們提供的API就能完成大部分任務了,再加上強大的運維,也許也不用去關注什麼安裝、配置、維護這些"雜事"了, 但這樣當數據量大,出現性能問題的時候,也經常束手無策。html

因爲在工做中使用到Redis的地方也比較簡單,心血來潮的時候,看看某個具體的數據結構的底層實現原理,好比REDIS_ZSET,可是對存儲原理、鍵過時機制、集羣瞭解得少。讀了《Redis 設計與實現》和官方Documentation後,感受它背後到處體現着優化:Redis是單線程的(須要放到特定的場景下的討論)、Redis數據類型的底層實現(object encoding)在數據量少的時候採用一種物理存儲結構,在數據量大的時候自動轉換成另外一種存儲結構、還有一些地方用到了爲了效率而取近似的思想,這些都讓Redis很美好。node

Redis Persistent

先從Redis持久化提及,有兩種:RDB和AOF。因爲Redis是內存數據庫,所以持久化機制比MySQL要"顯眼"一些。其實對於全部的存儲系統來講,持久化機制除了保證數據的可靠性(將數據寫入磁盤這種永久性存儲介質上)以外,它背後反映的是:磁盤與內存這兩種存儲介質的訪問速度的差別。磁盤訪問是百毫秒級、內存訪問多是百微秒級、cpu cache訪問是百納秒級。正是這種差別,在有限的內存下,刷盤策略就是保證:程序運行產生的數據已經在內存中了,何時,以什麼樣的頻率將數據同步到(fsync)磁盤。對於Redis而言,程序所需的數據以某種數據結構保存在內存中了,那它須要的是:如何把內存中的數據"持久化"到磁盤?git

RDB:執行save或者bgsave 命令生成rdb文件,或者以參數配置方式save 60 10000這樣就把全部的數據保存到磁盤上了。可是這樣,仍是存在很大的風險:執行了bgsave後,Client又寫入一些數據到Redis Server上了怎麼辦?或者還沒來得及執行bgsave Redis Server就掛了怎麼辦?程序員

AOF:Client的向Server發送的寫命令,Sever執行寫命令後並將之追加到aof_buf 緩衝區,aof_buf緩衝區以某種方式將數據保存到AOF文件。什麼方式呢?這就與appendfsync參數有關了。github

Redis 決定什麼時候將 aof_buf 中的數據刷新到磁盤AOF文件上與MySQL的redo log buffer 刷盤參數 innodb_flush_log_at_trx_commit 以及ElasticSearch的Translog刷盤參數 index.translog.durability很是類似。本質上都是如何平衡效率和可靠性兩者之間的矛盾:redis

怎樣保證一條數據也不丟失呢?Client寫入一條數據,我就刷一次磁盤算法

頻繁刷新磁盤,影響Client寫入速度sql

RDB和AOF的區別是:數據庫

  • RDB保存的是數據的內容(鍵值對),而AOF保存的是命令(SET KEY VALUE),正是因爲AOF保存的文件命令,針對AOF的優化:AOF重寫功能減小AOF文件體積。數組

  • RDB是 point in time備份,我把它翻譯成定點備份。AOF是根據寫命令持續地備份。

    The RDB persistence performs point-in-time snapshots of your dataset at specified intervals.

    the AOF persistence logs every write operation received by the server, that will be played again at server startup, reconstructing the original dataset.

Redis Replication

Redis Replication是實現Redis Sentinel 和 Cluster的基石。與其餘存儲系統(好比ElasticSearch、Kafka)所不一樣的是,Redis Replication是針對節點而言,從節點執行 SLAVEOF MASTER_IP PORT命令向master異步複製數據。而在ElasticSearch或者Kafka中,它們自然地將數據配置成多副本的形式,ElasticSearch中的主副本叫Primary shard,從副本叫Replica,Primary shard 和 replica 分佈在不一樣的節點上,從而避免了Single Point Of Failure,而多個數據副本之間的讀寫模型又稱爲數據副本模型

Redis Replication是把數據從master節點複製一份到slave節點上,那麼從哪裏開始複製呢?所以就有了完整重同步(full resynchronization,由SYNC命令觸發)和部分同步(partial resynchronization,由PSYNC觸發)。當新節點開始主從複製時,先執行同步過程(判斷是執行full 仍是 partial 同步),同步完成後進入到命令傳播階段。

這裏提一下Redis partial resynchronization 涉及到的Replication ID,至關於數據版本號,在Redis Replication官方文檔描述:

Every Redis master has a replication ID: it is a large pseudo random string that marks a given story of the dataset.

Each master also takes an offset that increments for every byte of replication stream that it is produced to be sent to slaves, in order to update the state of the slaves with the new changes modifying the dataset.

Slave 連上Master開始同步時,會發送Replication ID,若是一致,就會比較複製偏移量(offset),從而決定是否執行部分重同步。那如何處理某個Slave 晉升成 master 以後,其它Slave鏈接到新的master,其它Slave持有的Replication ID與新master的Replication ID確定是不同的,那如何避免這種狀況可能致使的完整重同步呢?

However it is useful to understand what exctly is the replication ID, and why instances have actually two replication IDs the main ID and the secondary ID.

原來有兩個Replication ID: main ID and the secondary ID,經過 secondary ID(舊的Replication ID),當其餘Slave鏈接到新的master上時,也不須要徹底重同步了:

The reason why Redis instances have two replication IDs is because of slaves that are promoted to masters. After a failover, the promoted slave requires to still remember what was its past replication ID, because such replication ID was the one of the former master. In this way, when other slaves will synchronize with the new master, they will try to perform a partial resynchronization using the old master replication ID.

下面來討論下爲何Redis Replication是異步的?而基於多副本機制的ElasticSearch和Kafka 的Replication機制與Redis差異是挺大的,在實現思路上有很大不一樣。

Redis Replication Documentation有一段,這裏只討論第一種mechanisms

This system works using three main mechanisms:

  1. When a master and a slave instances are well-connected, the master keeps the slave updated by sending a stream of commands to the slave, in order to replicate the effects on the dataset happening in the master side due to: client writes, keys expired or evicted, any other action changing the master dataset.

the master keeps the slave updated by sending a stream of commands to the slave,何時sending呢?或者說sending策略是什麼?這個sending策略,是Redis Replication 被稱爲異步複製的緣由吧。再看這一句due to: client writes, keys expired or evicted, any other action changing,實際上是說:redis 主從複製如何處理Client寫、鍵過時、鍵被Evict了等狀況的?好比說:

  • Client向master節點寫入一個Key後,這個Key會當即同步給Slave成功了,再返回響應給Client?

  • Redis Key過時了怎麼辦?Key被Evicted出去了怎麼辦?expired 與 evicted 仍是有區別的,expired 聚焦於鍵的生存時間(TTL)爲0了以後,如何處理key?鍵過時了,這個鍵仍是可能存在於Redis中的(主動刪除策略 vs 被動刪除策略)。而 evicted 聚焦於:當Redis 內存使用達到了max memory後,根據配置的maxmemory-policy將某個key刪除

  • 在實際應用中,通常採用主從複製實現讀寫分離,Client寫Master,讀Slave。所以,Master上的鍵過時了以後,如何及時地刪除Slave上過時的鍵,使得Client不讀取到已過時的數據?其官方文檔中說:Slave不會expire Key,當master上的Key過時了以後,發送一個DEL命令給Slave,讓Slave也刪除這個過時的鍵。

    so Redis uses three main techniques in order to make the replication of expired keys able to work:

    1. Slaves don't expire keys, instead they wait for masters to expire the keys. When a master expires a key (or evict it because of LRU), it synthesizes a DEL command which is transmitted to all the slaves.

在Redis Replication中還有一個問題:Allow writes only with N attached replicas:只有當至少N個Slave都"存活"時,才能接受Client的寫操做,這是避免單點故障保證數據可靠性的一種方式。

Starting with Redis 2.8, it is possible to configure a Redis master to accept write queries only if at least N slaves are currently connected to the master.

但要注意,因爲Redis的異步複製特性,它並不能保證Key必定寫入N個Slave成功了。所以個人理解,這裏的N個Slave,實際上是:只要有N個slave與master節點保存着 正常 鏈接,那麼就能夠寫數據

However, because Redis uses asynchronous replication it is not possible to ensure the slave actually received a given write, so there is always a window for data loss.

它只是一種 儘可能 保證數據安全的機制,主要由2個參數配置:min-slaves-to-writemin-slaves-max-lag

If there are at least N slaves, with a lag less than M seconds, then the write will be accepted.

You may think of it as a best effort data safety mechanism, where consistency is not ensured for a given write, but at least the time window for data loss is restricted to a given number of seconds. In general bound data loss is better than unbound one.

因爲這種異步複製的特徵,在redis sentinel master-slave模型下,若是client發起一個寫操做,而且收到了寫入成功的響應,當發生故障時,這個寫操做寫入的數據也是有可能丟失的!由於異步複製,當client寫入master的數據還沒有來得及同步給slave時,master宕機,sentinel選出一個slave做爲新的master,新master發起主從同步,就會覆蓋到原來master上的部分數據。

Sentinel + Redis distributed system does not guarantee that acknowledged writes are retained during failures, since Redis uses asynchronous replication. However there are ways to deploy Sentinel that make the window to lose writes limited to certain moments, while there are other less secure ways to deploy it.

又或者說:master與其全部的slave鏈接發生了網絡分區,以下圖所示:

master M1已是一個過期的master了,而若是client c1 恰好與這個過期的master在同一個網絡分區下,C1會一直認爲M1是正常的master,那麼c1寫入的數據就會丟失。(詳情可參考:Redis Sentinel Documentation)
那有沒有辦法解決Redis Sentinel這種由於異步複製帶來的數據丟失問題呢?二種解決方案:

  1. Use synchronous replication (and a proper consensus algorithm to run a replicated state machine).
  2. Use an eventually consistent system where different versions of the same object can be merged.

這裏來討論一下第一種方法,使用「同步複製」來保證 acknowledged write 不會丟失。這種方法的理論知識可參考Raft共識算法。在Raft共識算法中提到了複製狀態機,經過它來保證已提交的消息不會丟失。下面我從ElasticSearch的數據副本模型角度以及Kafka的的副本模型來討論一下:在ES和Kafka中是如何保證 acknowledged write 的可靠性(不丟失)的。
當Client 發起一次寫文檔的請求時,ES首先根據 docid 進行哈希,找到該文檔應該寫入到哪一個主分片上,當該文檔寫入到主分片後,將該文檔同步給各個副本分片,這裏的副本分片是同步副本列表(in-sync)集合裏面的副本分片,當同步副本列表集合中的副本分片都寫入該文檔後,就會給主分片返回響應,最終主分片再給Client響應,這就是"同步複製"的acknowledged write。
從上面的流程可看出,因爲同步副本列表集合存在,一篇文檔是寫入了多個副本的,所以能夠避免單點故障致使的數據丟失。另外,當由於網絡分區等緣由致使主分片失效(stale primary shard)時ES的master節點能夠從in-sync集合中選出一個副本分片做爲新的主分片,因爲in-sync集合中的副本分片與主分片的數據是同步的,所以發生這種主分片身份變化的狀況也不會致使acknowledged write丟失。而這就是同步複製能夠保證acknowledged write不丟失的緣由,這是與 Redis Sentinel 異步複製沒法保證acknowledged write 不丟失的區別。另外,值得注意的是,默認狀況下Client向ES寫入一篇文檔時,只要同步副本列表集合中的一個分片寫入成功了(wait_for_active_shards=1),就返回寫成功響應給Client。主分片確定是in-sync集合中的分片。

相似地,在Kafka中如何保證生產者的消息必定寫入成功了(已提交),分析的思路也是同樣的。在Kafka中,Producer有個ack參數,可配置爲-1(或者all),Kafka中也有in-sync集合,而且Kafka中也有個參數 min.insync.replicas 參數來控制producer發送的消息要寫入到幾個副本中才能返回寫入成功響應給生產者客戶端。Kafka的 min.insync.replicas參數與ES中的 wait_for_active_shards 參數兩者的原理是同樣的。將Producer的ack參數設置成-1或者all,再配合 min.insync.replicas參數 就能保證一條消息至少會寫入多個副本才返回acknowledged write響應給Client。當主副本發生宕機時,只要是從in-sync集合中選出一個副本做爲新的主副本,那麼就不會出現像 redis sentinel 中那種因異步複製致使的acknowledged write丟失問題,但若是主副本不是從in-sync集合中選出來的,就會出現數據丟失,這也是爲何 Kafka中有個配置參數unclean.leader.election=false 來強制保證kafka只能從in-sync集合中選出副本做爲主副本的緣由。
從ES得Kafka的數據副本同步過程能夠看出,它們與Raft的共識算法中的複製狀態機模型仍是有那麼一點區別的。

在ElasticSearch和Kafka中,也有很是與之相似的機制:ElasticSearch有個參數wait_for_active_shards,它也是在Client向ES 某個 index 寫入一篇文檔時,檢查這個 index 下是否有 wait_for_active_shards 個活躍的分片,若是有的話,就容許寫入,固然了,檢查活躍分片數量並寫入(check-then-act)是兩步操做,並非原子操做,所以ElasticSearch也並不能保證說文檔必定成功地寫入到wait_for_active_shards 個分片中去了。事實上,在返回給Client的ACK響應中,有一個_shard字段標識本次寫操做成功了幾個分片、失敗了幾個分片。另外須要注意的是:ElasticSearch和Kafka針對寫入操做引入一種"同步副本集合"(in-sync replication)機制,Kafka中也有同步副本列表集合,還記得Kafka的 broker 參數min.insync.replicas的做用嗎?它們都是爲了緩解:數據只寫入了一個節點,還沒有來得及複製到其餘節點上,該節點就宕機而致使的數據丟失的風險(這也是常常提到的單點故障SPOF)

ES的5個節點某個索引的分片以下圖:

額外說一下:這裏提到的儘可能保證數據安全,是經過多副本方式/主從複製方式保證數據安全,針對的是跨節點、避免單點故障。還有一種數據安全是針對單機的持久化機制而言的:數據寫入到內存了,產生了dirty page,可是還沒有來得及刷盤,節點就宕機了怎麼辦?所以存儲系統中都有一個WAL(Write Ahead Log 方案),好比MySQL的redo log、ElasticSearch的Translog,其整體思路是:先寫日誌再寫數據,若發生故障,則從日誌文件中恢復還沒有持久化到磁盤上的數據,從而保證了數據安全。

Redis Sentinel vs Redis Cluster

Redis Sentinel是一個Sentinel集羣監控多主從節點,Redis Cluster則是多個master節點組成一個集羣,同時每一個master節點可擁有多個slave節點作數據備份。Sentinel和Cluster都具備高可用性,其背後的實現是經過主從複製,將數據以"副本"形式存儲在多個節點上。

Redis Sentinel provides high availability for Redis. In practical terms this means that using Sentinel you can create a Redis deployment that resists without human intervention to certain kind of failures.

我以爲它們一個主要的區別是在數據分佈上,對於Sentinel而言,是一臺節點存儲全部的數據(全部的鍵值對),這就是所謂的數據分佈方式。若是一臺Redis 存儲不下全部的數據怎麼辦?這就是Redis Cluster須要解決的問題,它是由若干個master節點共同存儲數據,可以線性地擴展到1000+節點。

High performance and linear scalability up to 1000 nodes. There are no proxies, asynchronous replication is used, and no merge operations are performed on values.

在Redis Cluster中,是經過"槽指派"方式對Key進行哈希。整個鍵空間劃分紅16384個固定的槽,每一個節點經過槽指派負責處理哪些槽。採用哈希進行數據分佈的優點是:可以較好地保證Key的分佈是均勻的,均勻地分配在各個master節點上。這裏顯然有一致性哈希的思想在裏面,槽的數量是固定的,只有16384個,可是Redis的節點的數量能夠動態地變化,這時候只須要部分數據遷移。當Client寫入一個Key時,先經過CRC16(kEY)%16383計算出這個Key屬於哪一個槽,而後再查詢slots數組得知這個槽被指派給了哪一個節點,因而就把鍵值對存儲到這個節點上。

談到數據分佈方式,這個問題的本質是:因爲存儲系統是個集羣,有多臺節點,那麼把數據放到哪一臺節點(實際上是分片/分區所在的節點)上比較合適?這裏須要考慮數據分佈的均勻性,即不能有數據傾斜。在ElasticSearch中,當Client發起索引文檔請求時,若不指定docid,會自動生成docid,而且經過murmur3函數進行哈希,選擇一個合適的分片來存儲該文檔。相似地,在Kafka中,生產者發送消息時,消息是送到Kafka上的哪一個分區(Partition)呢?這就是Kafka的消息分區機制,默認是Round-Robin算法輪詢,固然也能夠自定義分區策略將消息發往指定的分區。

這裏來重點討論高可性下的failover機制:(故障的自動恢復)

無論是Redis Sentinel 仍是 Redis Cluster,若是一臺節點宕機了,要如何自動恢復呢?Redis Sentinel的failover機制是這樣的:

  • 某個Sentinel節點監測到某節點故障了,將之標記爲主觀下線

  • Sentinel節點向其餘Sentinel節點詢問,該節點是否真的已經下線,當收到大多數(quorum)(足夠數量)的Sentinel節點都認爲該節點下線時,將之標記爲客觀下線。這裏的大多數是:sentinel.conf配置文件中的quorum參數的設置,好比:sentinel monitor mymaster 127.0.0.1 6379 2 配置中 quorum=2

  • Sentinel集羣發起選舉,選出一個主Sentinel,由主Sentinel負責故障節點的Failover。選舉算法基於Raft,選舉規則是先到先得:好比說多個源Sentinel(候選Sentinel)向同一個目標Sentinel發起投票請求,誰的請求最早到達,目標Sentinel就把選票投給誰。當候選Sentinel獲取大多數節點的投票時,就成爲主Sentinel。這裏的大多數是針對raft選舉算法而言的。

  • 由主Sentinel負責故障節點的Failover,選擇擁有"最新數據"的Slave節點做爲新的master節點。所謂最新數據的slave節點,其實就是判斷:哪些slave節點最近剛剛與Sentinel節點通訊了、slave節點配置文件優先級、slave節點上的複製偏移量等因素,選出最合適的slave節點做爲新master

須要注意的是,在redis sentinel 檢測出master故障,並進行failover的過程,涉及到了2個"大多數",一個是 sentinel.conf 配置參數 quorum,另外一個是進行sentinel選舉所採用的raft選舉算法中的大多數。這2個仍是有一點區別的,舉例來講:一個5節點的sentinel集羣,quorum參數配置爲2,意味着只要有2臺sentinel節點認爲master宕機了,就可以觸發 failover,既5臺sentinel開始進行sentinel選主,可是選出主sentinel節點是須要大多數節點,也即至少3臺sentinel節點贊成的。當選出主sentinel節點後,就由它負責選擇一臺合適的slave節點做爲新的master。
此外,quorum 參數也可以影響主sentinel選舉的過程,當quorum設置爲一個比 sentinel集羣大多數(根據上面示例,大多數是3) 還要大的值時,也即當 quorum大於3時,好比quorum=5,那麼:sentinel選舉時,須要所有5個sentinel節點一致投票某個sentinel節點做爲主sentinel節點才行。具體可參考:Redis Sentinel Documentation

爲何要經過Sentinel選主這種方式選出主Sentinel,並由主Sentinel負責故障的Failover呢?個人理解是:首先須要對故障節點達成共識,即:一致認爲某個節點確實發生了故障,而後對故障恢復的處理也要達到共識,不能出現:兩個Sentinel節點同時在對同一個故障節點Failover的情形,而要想達成共識,分佈式一致性選舉算法就是解決方案。

最後來看下,Redis Cluster 是如何進行故障的Failover的?

  • 集羣中的每一個節點按期向其餘節點PING,某個master節點宕機,沒法回覆PONG,被標記爲疑似下線(PFAILED)
  • 當集羣中的大多數master節點都認爲該節點宕機時,該master節點被標記爲下線狀態(FAILED)。將故障節點標記爲FAILED的節點,向集羣廣播一條該master節點已經FAILE的消息。
  • 此時,slave節點已經發現了它所複製的主節點FAIL了,因而發起failover(與Redis Sentinel不一樣的是,這裏的故障failover是由從節點發起的)
  • 集羣中各個master節點向 故障master節點下的 slave 節點投票,獲取大多數master節點投票的slave節點將成爲新的master節點,這裏的選舉算法也是基於Raft實現的。

網絡分區問題(consistency guarantees)

Redis Cluster Tutorial中說Redis集羣沒法保證強一致性,換句話說:Redis Cluster會丟失"寫確認"的數據,即Client向Redis Cluster寫入一個Key,Client收到了這個Key寫入成功的響應,可是這個Key可能因故障而丟失了。致使故障的緣由有2個:Redis的異步複製特性以及網絡分區。

Redis Cluster is not able to guarantee strong consistency. In practical terms this means that under certain conditions it is possible that Redis Cluster will lose writes that were acknowledged by the system to the client.

對於異步複製來講,Redis提供了WAIT命令來緩解這種"寫確認"丟失的問題。WAIT有點相似於同步複製的意味,它都指定一個Key要寫入多少個slave纔會返回確認給Client了,那麼爲何官方文檔中仍是說:WAIT 也不能徹底保證acknowledged write 丟失呢?由於:同步複製是保證寫確認不丟失的其中一個步驟,在failover過程當中,選出的新master並不必定是擁有最新寫入的數據的那臺slave,Kafka中不是也有"不安全的選舉"嗎?(unclean.leader.election參數)
而至於網絡分區致使的寫確認丟失問題,又提供了參數 node timeout 來緩解。舉例來講就是:若是一臺master和某個Client劃分在了同一個網絡分區中,Client能夠持續給將數據寫入這臺master,可是這臺master已經與集羣中的大多數master節點以及它的備份slave失去了聯繫了,這是一臺很不安全的master,應當主動放棄master身份,並拒絕Client的寫入才行。所以,Redis cluster應當有某種機制及時檢測出這種狀況。這就是node timeout參數的意義。可是發生網絡分區後 node timeout 時間內寫入的數據,就有可能丟失了。

After node timeout has elapsed, a master node is considered to be failing, and can be replaced by one of its replicas. Similarly after node timeout has elapsed without a master node to be able to sense the majority of the other master nodes, it enters an error state and stops accepting writes.

那麼:ElasticSearch又是如何處理這種網絡分區的情形的呢?
看ES的data-replication 數據副本模型可知,當primary shard寫入文檔時,會將之同步到 in-sync 集合的replica,而後才返回Cllient acknowledged write。在同步文檔給 in-sync 集合中的replica時,replica會驗證該primary shard是不是過期的(stale),若是replica發現primary shard 已通過時了(至關於redis中的發生網絡分區處於少數派中的那種master節點),就會拒絕該primary shard的同步文檔的請求。

While forwarding an operation to the replicas, the primary will use the replicas to validate that it is still the active primary. If the primary has been isolated due to a network partition (or a long GC) it may continue to process incoming indexing operations before realising that it has been demoted. Operations that come from a stale primary will be rejected by the replicas.

當ES的 primary shard 收到replica的拒絕同步文檔響應後,它本身也會向ES的master節點檢查本身是否已經被master節點"降級爲"普通副本了。因而,它就再也不接受Client的文檔寫入請求,從而過期的primary shard 就不會返回acknowledged write給Client了。

When the primary receives a response from the replica rejecting its request because it is no longer the primary then it will reach out to the master and will learn that it has been replaced. The operation is then routed to the new primary.

至此,Redis Persistent、Redis Replication、Redis Sentinel、Redis Cluster 就大概介紹完了。我發現Redis的Tutorial和Documentation寫得真是好,還有通俗易懂的《Redis設計與實現》,都是理解 Redis 的好材料。最近一直想總結下各個存儲系統背後的原理,無奈技術和時間都不夠,只能寫一點筆記做爲記錄了吧,我想接下來是要去讀一讀系統的源碼,以期有更深刻的認識。

原文連接:http://www.javashuo.com/article/p-rgkiamlb-k.html

參考:程序員的宇宙時間線

相關文章
相關標籤/搜索