1. 程式人生 > 其它 >Redis複習筆記-分散式篇

Redis複習筆記-分散式篇

Redis複習筆記-分散式篇

Redis主從複製(replication)

配置

  1. 配置檔案中新增:
replicaof [host ip] [port]
//新增在每個slave節點中

從節點啟動後,會自動連線到master節點,開始同步資料。如果master節點更改了,比如原來的master節點宕機了,選舉了新的master節點,這個配置項就會被重寫。

  1. 在啟動伺服器時,通過引數直接指定master節點:
./redis-server --slaveof [host ip] [port]
  1. 在客戶端直接執行slaveof [host ip] [port],使該Redis例項成為從節點。

從節點也可以是其他節點的主節點,形成級聯複製的關係。可以通過info replication 檢視叢集狀態。從節點是隻讀的,不能執行寫操作,執行寫操作命令會報錯。資料在主節點寫入後,slave節點會自動從主節點同步資料。

取消主從關係:

  1. 將配置檔案中的replicaof 指令去除,此時從節點會變為主節點,不再複製資料。
  2. 直接執行指令slaveof no one斷開連線,此時從節點會變為主節點,不再複製資料。

主從複製原理

Redis主從複製分為兩類:

  • 全量複製:一個節點第一次連線到主節點,需要全部的資料
  • 增量複製:之前已經連線到master節點,但是中間網路斷開,或者slave節點宕機了,缺失了一部分資料。

連線階段:

  1. 從節點啟動時,會在自己本地儲存主節點的資訊,包括主節點的host和ip
  2. 從節點內部有個定時任務replicationCron,每隔一秒鐘檢查是否有新的主節點需要連線和複製。如果發現有主節點需要連線,就跟主節點建立連線。如果連線成功,從節點就讓一個專門處理複製工作的檔案事件處理器負責後續的複製工作。為了讓主節點感知到從節點的存活,從節點會定時向主節點發送ping請求。

建立連線以後,就可以同步資料了,這裡也分成兩個階段。

資料同步階段:

如果是新加入的從節點,那就需要全量複製,主節點通過bgsave命令在本地生成一份RDB快照,將RDB快照檔案發給從節點(如果超時會重連,可以調大repl-timeout的值)。

如果從節點自己本來就有資料怎麼辦?

從節點首先需要清除自己的舊資料,然後使用RDB檔案載入資料。

主節點生成RDB期間,接收到寫命令怎麼處理?

開始生成RDB檔案時,主節點會把所有新的寫命令快取在記憶體中。在從節點儲存了RDB之後,再將新的寫命令複製給從節點。(與AOF重寫rewrite期間接受到的命令的處理思路是一樣的)

第一次全量同步完了,主從已經保持一致了,後面就是持續把接受到的命令傳送給從節點。

命令傳播階段:

主節點持續把寫命令,非同步複製給從節點。一般情況下,我們不會使用Redis做讀寫分離,因為Redis的吞吐量已經夠高了,做叢集分片之後併發的問題更少,所以不需要考慮主從延遲的問題。只能通過優化網路來改善。

第二種情況是增量複製:

如果從節點有一段時間斷開了與主節點的連線,是不是要把原來的資料全部清空,重新全量複製一遍?效率太低了。增量複製中從節點通過master_repl_offset記錄了偏移量,以便使用增量複製。

為了降低主節點磁碟開銷,Redis從6.0開始支援無盤複製,主節點生成的RDB檔案不再儲存到磁碟,而是直接通過網路傳送給從節點。無盤複製適用於主節點所在機械磁碟效能較差但是網路寬頻比較富裕的場景。


主從複製的不足

主從複製並沒有解決高可用的問題。在一主一從或者一主多從的情況下,如果伺服器掛了,對外提供的服務就不可用了,單點問題沒有得到解決。如果每次都是手動把之前的從伺服器切換成主伺服器,然後再把剩餘節點設定為它的從節點,這就比較費事費力,還會造成一定時間的服務不可用。


可用性保證之Sentinel

原理

如何實現高可用呢?通過執行監控伺服器來保證服務的可用性。如果主節點超過一定時間沒有給監控伺服器傳送心跳報文,就把主節點標記為下線,然後把某一個從節點變成主節點,應用每一次都是從這個監控伺服器拿到主節點的地址。2.8版本後,提供了一個穩定版本的Sentinel,用來解決高可用的問題。

我們會啟動奇數個數的Sentinel的服務,啟動方式:

  • 通過Sentinel的指令碼啟動:

    ./redis-sentinel ../sentinel.conf
    
  • 使用redis-server的指令碼加Sentinel引數啟動:

    ./redis-server ../sentinel.conf --sentinel
    

Sentinel本質上只是一個執行在特殊模式之下的Redis。Sentinel通過info命令得到被監聽Redis機器的主從節點等資訊。為了保證監控伺服器的可用性,我們會對Sentinel做叢集的部署。Sentinel既監控了所有的服務,Sentinel之間也有互相監控。Sentinel本身沒有主從之分,地位是平等的,只有Redis服務節點有主從之分。

Sentinel是如何直到其他Sentinel節點的存在的?

Sentinel是一個特殊狀態的Redis節點,它也具有訂閱釋出功能。Sentinel上線時,給所有的Redis節點的名字為:_sentinel_:hello的channel傳送訊息。每個哨兵都訂閱了所有Redis節點名字為_sentinel_:hello的channel,所以可以互相感知對方的存在,而進行監控。


功能實現

服務下線

Sentinel是如何知道主節點宕機了的?

Sentinel預設每秒一次向Redis服務節點發送ping命令,如果在指定的時間內沒有收到有效回覆,Sentinel會將該伺服器標記為下線。(主觀下線)。由引數:# sentinel conf控制。預設是30秒。但是,只有你發現主節點下線,並不代表主節點真的下線了,也有可能是自己的網路問題。所以這個時候第一個發現主節點下線的Sentinel節點會繼續詢問其他的Sentinel節點,確認這個節點是否下線。如果quorum數量的Sentinel節點都認為主節點下線,主節點在被真正確認下線(客觀下線)。

quorum:確認客觀下線的最少的哨兵數量,通過配置項進行設定

確定主節點下線之後,就需要重新選舉主節點。Sentinel叢集此時便開始故障轉移,從從節點中選舉一個節點作為主節點。

故障轉移

由Sentinel完成。如果需要從redis叢集中選舉一個節點作為主節點,首先需要從Sentinel叢集中選舉一個Sentinel節點作為Leader。Sentinel使用與原生略有區別的Raft演算法完成。

原生-Raft:

核心思想:先到先得,少數服從多數。Spring cloud 註冊中心解決方案Consul也使用到了Raft協議。

文字描述:

  1. 分散式環境中節點有三個狀態:Follower,Candidate(虛線外框),Leader(實現外框)
  2. 一開始所有的節點都是Follower狀態,如果Follower連線不到Leader(Leader掛了),他就會成為Candidate。Candidate請求其他節點的投票,其他的節點會投給它。如果它得到了大多數節點的投票,他就成為了Leader。這個過程就叫做Leader Election。
  3. 現在所有的寫操作需要在Leader節點上發生。Leader會記錄操作日誌。沒有同步到其他Follower節點的日誌,狀態是uncommitted。等到超過半數的Follower同步了這條記錄,日誌狀態就會變成committed。Leader會通知所有的Follower日誌已經committed,這個時候所有的節點就達成了一致。這個過程叫做Log Replication。
  4. 在Raft協議中,選舉的時候有兩個超時時間。其中一個叫做election timeout(另一個叫做heartbeat timeout)。為了防止同一時間大量節點參與選舉,每個節點在變成Candidate之前需要隨機等待一段時間,時間範圍是150ms~300ms之間。第一個變成Candidate的節點會先發起投票,他會先投給自己,然後請求其他節點的投票。
  5. 如果還沒有收到投票結果,又到了超時時間,需要重置超時時間。只要有大部分節點投給了一個節點,他就會變成Leader。
  6. 成為了Leader之後,它會發送訊息來讓同步資料,傳送訊息的間隔是由heartbeat timeout來控制的。Followers會回覆同步資料的訊息。
  7. 只要Followers收到了同步資料的訊息,代表Leader沒掛,他們就會清除heartbeat timeout的計時。
  8. 但是一旦Follows在heartbeat timeout時間之內沒有收到同步資料的訊息,他就會認為Leader掛了,便開始讓其他節點投票,成為新的Leader。
  9. 必須超過半數以上節點投票,保證只有一個Leader被選出來。
  10. 如果有兩個Follower同時變成了Candidate,就會出現分割投票。但是因為他們的election timeout不同,在發起新的一輪選舉的時候,有一個節點會優先收到更多的投票,所以只有會產生一個Leader。

Sentinel的Raft協議在此基礎之上,略有不同:

  1. master客觀下線觸發選舉,而不是過了election timeout時間開始選舉。
  2. Leader並不會把自己成為了Leader的訊息傳送給其他Sentinel。其他Sentinel等待Leader從從節點選出主節點之後,檢測到新的主節點正常工作之後,就會去掉主節點客觀下線的標識,從而不需要進入故障轉移流程。
  3. 如果一個Sentinel節點獲得的選票數達到Leader最低票數(quorum和Sentinel節點數/2+1的最大值),則該Sentinel節點選舉為Leader,否則重新進行選舉。
  4. 被請求投票的Sentinel節點如果沒有同意過其他Sentinel節點的選舉請求,則同意該請求(選舉票數+1),否則不同意。

Leader確定以後,開始對redis其餘節點進行故障轉移:

對於所有的從節點,按照以下順序來進行選舉主節點:

  1. 如果與哨兵節點斷開的時間超過了某個閾值,就會直接失去選舉權
  2. 比對優先順序,選擇優先順序最高的從節點作為主節點,如果不存在則繼續。優先順序在配置檔案中可以設定:slave_priority 100 數值越小優先順序越高。
  3. 如果優先順序相同,就看誰從主節點中複製的資料最多,也就是選擇複製偏移量最大的從節點作為主節點,
  4. 選擇runid(redis每次啟動的時候生成隨機的runid作為redis的標識)最小的從節點作為主節點。

至此,主節點選舉完畢。

為什麼Sentinel叢集至少是3個?

如果Sentinel叢集中只有2個節點,那麼Leader最低票數至少為2,當該叢集中有一個節點故障後,僅剩的一個節點便永遠無法成為Leader。

確定了主節點之後,對將要成為主節點的從節點發送slaveof no one 讓它成為獨立節點,並對其他從節點發送slaveof [master ip] [master port],讓他們成為這個主節點的從節點,故障轉移完成。

哨兵機制的不足

主從切換的過程中會丟失資料,因為只有一個主節點。只能單點寫,沒有解決水平擴容的問題。如果資料量十分大,就需要對Redis進行資料分片。


Redis 資料分片

實現Redis資料分片,有三種解決方案:

1. 客戶端 Sharding

Jedis客戶端中,支援分片功能。RedisTemplate就是對Jedis的封裝。

ShardedJedis:

Jedis有幾種連線池,其中有一種支援分片。

這裡通過這個連線池分別連線到兩個Redis服務。插入一百條資料後,發現一臺伺服器上有44個key,另外一臺為56個key。

ShardedJedis是如何做到的?

如果希望資料分佈相對均勻,首先可以考慮雜湊後取模,因為key不一定是整數,所以先計算雜湊。例如:hash(key)%N,根據餘數,決定對映到哪個節點。這種方式比較簡單,屬於靜態的分片規則,但是一旦節點數量發生變化(新增或者減少),由於取模的N發生變化,資料就需要重新分佈。為了解決這個問題,又使用了一致性雜湊演算法

ShardedJedis實際上就是使用一致性雜湊演算法。原理是:

把所有的雜湊值空間組織成一個虛擬的圓環(雜湊環),整個空間按照順時針方向組織。因為是圓環,所以0和2^32-1是重疊的。

假如有四臺機器要雜湊環來實現對映(分佈資料),我們就先根據機器的名稱或者IP計算雜湊值,然後分佈到圓環中。對key計算後,得到它在雜湊環中對應的位置。沿著雜湊環順時針找到第一個Node,就是資料儲存的節點。

一致性雜湊解決了動態增減節點時,所有資料都需要重新分佈的問題,它只會影響到下一個相鄰的節點,對其他節點沒有影響。但是也存在一個缺點:

因為節點不一定是均勻分佈的,特別是在節點數量比較少的情況下,所以資料不能得到均勻分佈,為了解決這個問題,我們需要引入虛擬節點

那麼,節點是怎麼實現的?

jedis例項被放到了一顆紅黑樹TreeMap()中,當存取鍵值對時,計算鍵的雜湊值,然後從紅黑樹上摘下比該值大的第一個節點上的JedisShardInfo ,隨後從resources取出Jedis。在jedis.getShard(“k”+i).getClient()獲取到真正的客戶端。

public R getShard(String key) {
    return resources.get(getShardInfo(key));
  }
  
 
public S getShardInfo(byte[] key) {
    // 獲取比當前key的雜湊值要大的紅黑樹的子集
    SortedMap<Long, S> tail = nodes.tailMap(algo.hash(key));
    if (tail.isEmpty()) {
      // 沒有比它大的了,直接從nodes中取出
      return nodes.get(nodes.firstKey());
    }
    // 返回第一個比它大的JedisShardInfo
    return tail.get(tail.firstKey());
  }

使用ShardedJedis之類的客戶端分片程式碼的優勢是配置簡單,不依賴於其他中介軟體,分割槽的邏輯自定義,比較靈活。但是居於客戶端的方案,不能實現動態的服務增減,每個客戶端需要自行維護分片策略,存在重複程式碼。


2. 代理 Proxy

典型的代理分割槽的方案有Twitter開源的Twemproxy和國內的豌豆莢開源的Codis。

Twemproxy

優點:比較穩定,可用性高

缺點:

  1. 出現故障不能自動轉移,架構複雜,需要藉助其他元件(LVS/HAProxy + Keepalived)實現高可用
  2. 擴縮容需要修改配置,不能實現平滑地擴縮容(需要重新分佈資料)。

Codis

是一個代理中介軟體,使用Go進行開發,跟資料庫分庫分表中介軟體的MyCat的工作層次是一樣的,

客戶端連線Codis和連線Redis沒有區別。

**分片原理: **

Codis把所有的key分成了N個槽,每個槽對應一個分組,一個分組對應一個或一組Redis例項。Codis對key進行CRC32運算,得到一個32位的數字,然後模以N,得到餘數,這個就是key對應的槽,槽後面就是Redis的例項,

如果需要解決單點問題,Codis也需要做叢集部署,多個Codis節點需要執行一個Zookeeper或etcd/本地檔案。

新增節點: 可以為節點指定特定的槽位,Codis也提供了自動均衡策略。

獲取資料: 在Redis中的各個例項裡獲取到符合的key,然後再彙總到Codis中。


3. Redis Cluster

在Redis 3.0版本正式推出,用來解決分散式的需求,同時也可以實現高可用。跟Codis不一樣,它是去中心化的,客戶端可以連線到任意一個可用節點。

可以看作是由多個Redis例項組成的資料集合。客戶端不需要關注資料的子集到底存在哪個節點,只需要關注這個集合整體。

資料分佈

使用虛擬槽來實現。Redis建立了16383個槽,每個節點負責一定區間的slot。

物件分佈到Redis節點上時,對key用CRC16演算法計算再%16384,得到一個slot的值,資料落到負責這個slot的Redis節點上。

為什麼slot是16384個?

CRC16演算法產生的hash值有16bit,該演算法可以產生2^16-=65536個值。換句話說,值是分佈在0~65535之間。那作者在做mod運算的時候,為什麼不mod65536,而選擇mod16384?很幸運的是,這個問題,作者是給出了回答的!

The reason is:

  • Normal heartbeat packets carry the full configuration of a node, that can be replaced in an idempotent way with the old in order to update an old config. This means they contain the slots configuration for a node, in raw form, that uses 2k of space with16k slots, but would use a prohibitive 8k of space using 65k slots.
  • At the same time it is unlikely that Redis Cluster would scale to more than 1000 mater nodes because of other design tradeoffs.

So 16k was in the right range to ensure enough slots per master with a max of 1000 maters, but a small enough number to propagate the slot configuration as a raw bitmap easily. Note that in small clusters the bitmap would be hard to compress because when N is small the bitmap would have slots/N bits set that is a large percentage of bits set.

轉換理解以下就是:

  1. 如果槽位為65536,傳送心跳資訊的訊息頭達8k,傳送的心跳包過於龐大。如上所述,在訊息頭中,最佔空間的是myslots[CLUSTER_SLOTS/8]。當槽位為65536時,這塊的大小是:
    65536÷8÷1024=8kb
    因為每秒鐘,redis節點需要傳送一定數量的ping訊息作為心跳包,如果槽位為65536,這個ping訊息的訊息頭太大了,浪費頻寬。
  2. redis的叢集主節點數量基本不可能超過1000個。
    如上所述,叢集節點越多,心跳包的訊息體內攜帶的資料越多。如果節點過1000個,也會導致網路擁堵。因此redis作者,不建議redis cluster節點數量超過1000個。
    那麼,對於節點數在1000以內的redis cluster叢集,16384個槽位夠用了。沒有必要拓展到65536個。
  3. 槽位越小,節點少的情況下,壓縮比高Redis主節點的配置資訊中,它所負責的雜湊槽是通過一張bitmap的形式來儲存的,在傳輸過程中,會對bitmap進行壓縮,但是如果bitmap的填充率slots / N很高的話(N表示節點數),bitmap的壓縮率就很低。
    如果節點數很少,而雜湊槽數量很多的話,bitmap的壓縮率就很低。

可以通過指令檢視key屬於哪個slot:cluster keyslot [key]

如何讓相關的資料落到同一個節點上?

有些操作時不可以跨節點執行的。比如:multi key

解決方案:在key里加入{hash tag}即可,Redis在計算編號的時候會只獲取{}之間的字串進行槽號計算,這樣由於上面兩個不同的鍵,{}裡面的字串是相同的,因此他們可以被計算出相同的槽。

客戶端連線到哪一臺伺服器?訪問的資料不在當前節點上,怎麼辦?

那麼我們就需要讓客戶端重定向。

客戶端重定向

如果我操作指令:set redis 1返回(error) MOVED 13724 127.0.0.1:7293。則表示根據key計算出來的slot不歸現在的埠管理,需要切換到7293埠去操作。這個時候,我們需要更換埠:redis-cli -p 7293操作,才會返回OK。這樣客戶端需要連線兩次。

Jedis等客戶端會在本地維護一份slot-node的對映關係,所以大部分時候都不需要重定向。

資料遷移

如果新增或下線了主節點,資料應該怎麼遷移(重新分配)?

因為key-slot的關係永遠不會變,所以當新增了節點的時候,把原來的slot分配給新的節點負責,並且把相關的資料遷移過來。

redis-cli --cluster add-node 127.0.0.1:7291 127.0.0.1:7297
//新增一個新節點(新增一個7297)

redis-cli --cluster reshard 127.0.0.1:7291
//新增的節點沒有雜湊槽,不能分佈資料,在原來的任意一個節點執行,使得被分配的原節點重新分配slot

//輸入需要分配的雜湊槽的數量,和雜湊槽的來源節點(可以輸入all或者id)

高可用和主從切換原理

只有主節點可以寫,如果一個主節點掛了,從節點怎麼變成主節點?

當從節點發現自己的主節點變為FAIL狀態時,便嘗試進行Failover,以期成為新的主節點。由於掛掉的主節點可能會有多個從節點,從而存在多個從節點競爭成為主節點的過程:

  1. 從節點發現自己的主節點變為FAIL
  2. 將自己記錄的叢集currentEpoch+1,並廣播FAILOVER_AUTH_REQUEST資訊
  3. 其它節點收到該資訊,只有主節點響應,判斷請求者的合法性,併發送FAILOVER_AUTH_ACK,對每一個epoch只發送一次ack(相當於投票只能投一次)
  4. 嘗試failover的從節點收集FAILOVER_AUTH_ACK
  5. 超過半數後變成新的主節點
  6. 廣播ping通其他叢集節點

currentEpoch: 可以當做記錄叢集狀態變更的遞增版本號.叢集節點建立時,不管是 master 還是 slave,都置 currentEpoch 為 0。當前節點接收到來自其他節點的包時,如果傳送者的 currentEpoch(訊息頭部會包含傳送者的 currentEpoch)大於當前節點的currentEpoch,那麼當前節點會更新 currentEpoch 為傳送者的 currentEpoch。因此,叢集中所有節點的 currentEpoch 最終會達成一致,相當於對叢集狀態的認知達成了一致 。

作用是: 當叢集的狀態發生改變,某個節點為了執行一些動作需要尋求其他節點的同意時,就會增加 currentEpoch 的值。當 slave A 發現其所屬的 master 下線時,就會試圖發起故障轉移流程。首先就是增加 currentEpoch 的值,這個增加後的 currentEpoch 是所有叢集節點中最大的。然後slave A 向所有節點發起拉票請求,請求其他 master 投票給自己,使自己能成為新的 master。其他節點收到包後,發現傳送者的 currentEpoch 比自己的 currentEpoch 大,就會更新自己的 currentEpoch,並在尚未投票的情況下,投票給 slave A,表示同意使其成為新的 master。