Redis——分佈式篇

爲什麼需要Redis集羣

爲什麼需要集羣?

性能

Redis本身的QPS已經很高了,但是如果在一些併發量非常高的情況下,性能還是會受到影響。這個時候我們希望有更多的Redis服務來完成工作。

擴展

第二個是處於存儲的考慮。因爲Redis所有的數據都放在內存中,如果數據量大,很容易受到硬件的限制。升級硬件收效和成本比太低,所以我們需要有一種橫向擴展的方法。

可用性

第三點是可用性和安全的問題。如果只有一個Redis服務,一旦服務宕機,那麼所有的客戶端都無法訪問,會對業務造成很大影響。另一個,如果硬件發生故障,而單機的數據無法恢復的話,帶來的影響也是災難性的。

可用性、數據安全、性能都可以通過啓動多個Redis服務實現。其中有一個是主節點(master),可以有多個從節點(slave)。主從之間通過數據同步,存儲完成相同的數據。如果主節點發生故障,則把某個從節點改成主節點,訪問新的主節點。

Redis主從複製(replication)

主從複製配置

假設一主多從,203是主節點,在每個slave節點的redis.conf配置文件增加一行

slaveof 192.168.8.203 6379

在主從切換的時候,這個配置會被重寫成:

replicatof 192.168.8.203 6379

或者在啓動服務時通過參數指定master節點:

./redis-server --slaveof 192.168.8.203 6379

或在客戶端直接執行slaveof ip port,使該Redis示例稱爲xx的從節點。

啓動後,查看集羣狀態:

#進入客戶端
info replication

從節點不能寫入數據(只讀),只能從master節點同步數據,get成功,set失敗。

set king test
#(error) READONLY You can't write against a read only replica.

主節點寫入後,slave會自動從master同步數據。

斷開復制:

slaveof no one

此時從節點會變成自己的主節點,不再複製數據。

主從複製原理

連接階段

  1. slave node啓動時(執行slaveof 命令),會在自己本地保存master node的信息,包括master node的host和port。
  2. slave node內部有個定時任務replicationCron(源碼:replication.c),每隔1秒檢查是否有新的master node要連接和複製,如果發現,就跟master node建立socket網絡連接,如果連接成功,從節點爲該socket建立一個專門處理複製工作的文件事件處理器,負責後續的複製工作,如接收RBD文件、接收命令傳播等。

當從節點變成了主節點的一個客戶端之後,會給主節點發送ping請求。

數據同步階段

  1. master node第一次執行全量複製,通過bgsave命令在本地生成一份RDB快照,將RDB快照文件發給slave node(如果超時會重連,可以調大repl-timeout的值)。slave node首先清除自己的舊數據,然後用RDB文件加載數據。開始生成RDB文件時,master會把所有新的些命令緩存在內存中。在slave node保存了RDB之後,再將新的寫命令複製給slave node。

命令傳播階段

  1. master node持續將寫命令,異步複製給slave node

延遲時不可避免地,只能通過優化網絡。

repl-disable-tcp-nodelay no

當設置爲yes時,TCP會對包進行合併從而減少帶寬,但是發送的頻率會降低,從節點數據延遲增加,一致性變差;具體發送頻率與Linux內核的配置有關,默認配置爲40ms。當設置爲no時,TCP會立馬將主節點的數據發送給從節點,帶寬增加但延遲變小。

一般來說,只有當應用對Redis數據不一致的容忍度較高,且主從節點之間網絡狀態不好時,纔會設置爲yes;多數情況使用默認值no。

如果從節點有一段時間斷開了與主節點的連接是否需要全量複製?增量,如何處理?

通過master_repl_offset記錄的偏移量

#通過命令查看master_repl_offset
info replication

主從複製的不足

主從複製模式解決了數據備份和性能(通過讀寫分離)的問題,但是還是存在一些不足:

  1. RDB文件過大的情況下,同步非常耗時;
  2. 在一主一從或者一主多從的情況下,如果主服務器掛了,對外提供的服務就不可用了,單點問題沒有得到解決。如果每次都是手動把之前的從服務器切換成主服務器,耗時耗力,且非實時處理。

可用性保證Sentinel(哨兵)

Sentinel原理

如何實現主從的自動切換呢?思路:

創建一臺監控服務來監控所有Redis服務節點的狀態,比如:master節點超過一定時間沒有給監控服務器發送心跳報文,就把master標記爲下線,然後把某一個slave變成master。應用每一次都是從這個監控服務器拿到master的地址。

問題:如果監控服務本身出問題了怎麼辦?

於是Redis引入了Sentinel的設計思路:通過運行監控服務器來保證服務的可用性。

從Redis2.8版本起,Redis提供了一個穩定版本的Sentinel(哨兵),用來解決高可用的問題。它是一個特殊狀態的redis實例。

我們會啓動一個或者多個Sentinel服務

src/redis-sentinel

它本質上只是一個運行在特殊模式之下的Redis服務,Sentinel通過info命令得到被監聽Redis機器的master,slave等信息。

爲了保證監控服務器的可用性,我們會對Sentinel做集羣的部署。Sentinel既監控所有的Redis服務,也相互監控。

Sentinel本身沒有主從之分,只有Redis服務節點有主從之分。

服務下線

Sentinel默認以每秒鐘1次的頻率向Redis服務發送ping命令。如果在down-after-milliseconds內都沒有收到有效回覆,Sentinel會將該服務標記爲下線(主觀下線)。

#sentinel.conf
sentinel down-after-milliseconds <master-name> <milliseconds>

這個時候Sentinel節點會繼續詢問其他的Sentinel系欸但,確認主觀下線的節點是否下線,如果多數Sentinel節點都認爲其下線,此時該節點被確認下線(客觀下線),這個時候就需要重新選舉master。

故障轉移

如果master被標記爲下線,就會開始故障轉移流程。

既然有這麼多的Sentinel節點,由誰來主導故障轉移呢?

故障轉移流程的第一步就是在Sentinel集羣選出一個Leader,由Leader完成故障轉移流程。Sentinel通過Raft算法,實現Sentinel集羣的Leader選舉。

Raft算法

在分佈式存儲系統中,通常通過維護多個副本來提高系統的可用性,那麼多個節點之間必須要面對數據一致性的問題。Raft的目的就是通過複製的方式,使所有節點達成一致,但是這麼多節點,以哪個節點的數據爲準?所以必須選出一個Leader。

大體上有兩個步驟:Leader選舉,數據複製。

Raft是一個共識算法(consensus algorithm)。Spring Cloud的註冊中心解決方案Consul也用到了Raft協議。

Raft的核心思想:先到先得,少數服從多數。

Raft算法演示:http://thesecretlivesofdata.com/raft/

總結:

Sentinel的Raft算法和Raft論文略有不同。

  1. master客觀下線觸發選舉,而不是過了election timeout時間開始選舉。
  2. Leader並不會把自己成爲Leader的消息發給其他Sentinel。其他Sentinel等待Leader從slave選出master後,檢測到新的master正常工作後,就會去掉客觀下線的標識,從而不需要進入故障轉移流程。

故障轉移

  • 如何讓一個原來的slave節點稱爲主節點?
  1. 選出Sentinel Leader之後,由Sentinel Leader向某個節點發送slaveof no one命令,使其稱爲獨立節點。
  2. 然後向其他節點發送slaveof ip port(本機服務),讓它們成爲上面節點的子節點,故障轉移完成。
  • 這麼多從節點,選誰成爲主節點?

關於從節點選舉,一共有四個因素影響選舉的結果,分別是斷開連接時長、優先級排序、複製數量、進程id。

如果與Sentinel連接斷開比較久,超過了一個閾值,就直接失去了選舉權。如果擁有選舉權,那就看誰的優先級高,這個在配置文件裏可以設置(replica-priority 100),數值越小優先級越高。

如果優先級相同,就看誰從master中複製的數據最多(複製偏移量最大),選最多的那個,如果複製數量也相同,就選擇進程id最小的那個。

Sentinel的功能總結

  • Monitoring. Sentinel constantly checks if your master and replica instances are working as expected.
  • Notification. Sentinel can notify the system administrator, or other computer programs, via an API, that something is wrong with one of the monitored Redis instances.
  • Automatic failover. If a master is not working as expected, Sentinel can start a failover process where a replica is promoted to master, the other additional replicas are reconfigured to use the new master, and the applications using the Redis server are informed about the new address to use when connecting.
  • Configuration provider. Sentinel acts as a source of authority for clients service discovery: clients connect to Sentinels in order to ask for the address of the current Redis master responsible for a given service. If a failover occurs, Sentinels will report the new address.

不是我懶,是這樣更準確,更權威。咳咳......

四大功能:

  • 監控:Sentinel會不斷檢查您的主實例和副本實例是否按預期工作
  • 通知:Sentinel可以通過API通知系統管理員或其他計算機程序,其中一個受監視的Redis實例出了問題
  • 自動故障轉移:如果主服務器未按預期工作,則Sentinel可以啓動故障轉移過程,在該過程中,將副本升級爲主服務器,將其他附加副本重新配置爲使用新的主服務器,並通知使用Redis服務器的應用程序要使用的新地址
  • 配置提供服務:Sentinel充當客戶端服務發現的授權來源:客戶端連接到Sentinels,以詢問負責給定服務的當前Redis主服務器的地址。如果發生故障轉移,Sentinels將報告新地址

Sentinel實戰

Sentinel配置

爲了保證Sentinel的高可用,Sentinel也需要做集羣部署,集羣中至少需要三個Sentinel實例。

hostname IP 節點角色&端口
master 192.168.8.203 Master:6379/Sentinel:26379
slave1 192.168.8.204 Slave:6379/Sentinel:26379
slave2 192.168.8.205 Slave:6379/Sentinel:26379

以Redis安裝路徑/home/soft/redis-5.0.5/爲例

在204和205的src/redis.conf配置文件中添加

slaveof 192.168.8.203 6379

在203、204、205創建Sentinel配置文件(安裝後根目錄下默認有sentinel.conf):

cd /home/soft/redis-5.0.5
mkdir logs
mkdir rdbs
mkdir sentinel-tmp
vim sentinel.conf

三臺服務器內容相同,如下:

daemonize yes
port 26379
protected-mode no
dir "/home/soft/redis-5.0.5/sentinel-tmp"
sentinel monitor redis-master 192.168.8.203 6379 2
sentinel down-after-milliseconds redis-master 30000
sentinel failover-timeout redis-master 180000
sentinel parallel-syncs redis-master 1
#上面四個redis-master名稱需要統一

參數解釋:

  • protected-mode
    • 是否允許外部網絡訪問
  • dir
    • sentinel的工作目錄
  • sentinel monitor
    • sentinel監控的redis主節點
  • sentinel down-after-milliseconds
    • master宕機多久,纔會被Sentinel確認主觀下線
  • sentinel failover-timeout
    • 同一個sentinel對同一個master兩次failover之間的間隔時間
    • 當一個slave從一個錯誤的master那裏同步數據開始計算時間。直到slave被糾正爲向正確的master那裏同步數據時
    • 當想要取消一個正在進行的failover所需要的時間。
    • 當進行failover時,配置所有slaves指向新的master所需的最大時間。
  • sentinel parallel-syncs
    • 這個配置項指定了在發生failover主備切換時最多可以有多少個slave同時對新的master進行同步,這個數字越小,完成failover所需的時間就越長,但是如果這個數字越大,就意味着越多的slave因爲replication而不可用。可以通過將這個值設爲1來保證每次只有一個slave處於不能處理命令請求的狀態

Sentinel驗證

啓動Redis服務和Sentinel服務

cd /home/soft/redis-5.0.5/src

#啓動Redis服務
./redis-server ../redis.conf

#啓動Sentinel服務
./redis-sentinel ../sentinel.conf

#或者通過如下命令
./redis-server ../sentinel.conf --sentinel

查看集羣狀態:

info replication

模擬master宕機,在master服務端執行命令:

shutdown

觀察其他節點集羣狀態查看role:是否從slave變成master

Sentinel連接使用

Jedis連接Sentinel

master name來自於sentinel.conf的配置

private static JedisSentinelPool createJedisPool() {
    String masterName = "redis-master";
    Set<String> sentinels = new HashSet<String>();
    sentinels.add("192.168.8.203:26379");
    sentinels.add("192.168.8.204:26379");
    sentinels.add("192.168.8.205:26379");
    pool = new JedisSentinelPool(masterName, sentinels);
    return pool;
}

Spring Boot連接Sentinel

spring.redis.sentinel.master=redis-master
spring.redis.sentinel.nodes=192.168.8.203:26379,192.168.8.204:26379,192.168.8.205:26379

無論是Jedis還是Spring Boot(2.x版本默認是Lettuce),都只需要配置全部哨兵的地址,由哨兵返回當前的master節點地址。

哨兵機制的不足

主從切換的過程中會丟失數據,因爲只有一個master。

只能單點寫,沒有解決水平擴容的問題。

如果數據量非常大,這個時候我們需要多個master-slave的group,把數據分佈到不同的group中。

問題來了,數據怎麼分片?分片之後,怎麼實現路由?

Redis分佈式方案

如果要實現Redis數據的分片,我們有三種方案。第一種是在客戶端實現相關的邏輯,例如用取模或者一致性哈希對key進行分片,查詢和修改都先判斷key的路由。

第二種是把分片處理的邏輯抽取出來,運行一個獨立的代理服務,客戶端連接到這個代理服務,代理服務做請求的轉發。

第三種就是基於服務端實現。

客戶端Sharding

Jedis客戶端提供了Redis Sharding的方案,並且支持連接池。

ShardedJedis

public class ShardingTest {
    public static void main(String[] args) {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
​
        // Redis 服務器
        JedisShardInfo shardInfo1 = new JedisShardInfo("127.0.0.1", 6379);
        JedisShardInfo shardInfo2 = new JedisShardInfo("192.168.8.205", 6379);
​
        // 連接池
        List<JedisShardInfo> infoList = Arrays.asList(shardInfo1, shardInfo2);
        ShardedJedisPool jedisPool = new ShardedJedisPool(poolConfig, infoList);
        ShardedJedis jedis = null;
        try{
            jedis = jedisPool.getResource();
            for(int i=0; i<100; i++){
                jedis.set("k"+i, ""+i);
            }
            for(int i=0; i<100; i++){
                System.out.println(jedis.get("k"+i));
            }
​
        }finally{
            if(jedis!=null) {
                jedis.close();
            }
        }
    }
}

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

第二種思路就是把分片的代碼抽取出來,做成一個公共服務,所有的客戶端都連接到這個代理層。由代理層來實現請求和轉發。

代理Proxy

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

Twemproxy

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

不足:

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

Codis

Codis是一個代理中間件,用Go語言開發的。

功能:客戶端連接Codis跟連接Redis沒有區別。

  Codis Tewmproxy Redis Cluster 
重新分片不需要重啓 YES NO YES
pipeline YES YES  
多key操作的hash tags{} YES YES YES
重新分片時的多key操作 YES - NO
客戶端支持 所有 所有 支持cluster協議的客戶端

分片原理:Codis把所有的key分成N個槽(例如1024),每個槽對應一個分組,一個分組對應於一個或者一組Redis實例。Codis對key進行CRC32運算,得到一個32位的數字,然後模擬N(槽的個數),得到餘數,這個就是key對應的槽,槽後面就是Redis的實例。比如4個槽:

Codis的槽位映射關係是保存在Proxy中的,如果要解決單點的問題,Codis也要做集羣部署,多個Codis節點怎麼同步槽和實例的關係呢?需要運行一個Zookeeper(或者etcd/本地文件)。

在新增節點的時候,可以爲節點指定特定的槽位。Codis也提供了自動均衡策略。Codis不支持事務,其他的一些命令也不支持。

獲取數據原理(mget):在Redis中的各個實例裏獲取到符合的key,然後再彙總到Codis中。

Codis是第三方提供的分佈式解決方案,在官方的集羣功能穩定之前,Codis也得到了大量的應用。

Codis是第三方提供的分佈式解決方案,在官方的集羣功能穩定之前,Codis也得到了大量的應用。

Redis Cluster

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

數據分片有幾個關鍵的問題需要解決:

  1. 數據怎麼相對均勻的分片
  2. 客戶端怎麼訪問到響應的節點和數據
  3. 重新分片的過程,怎麼保證正常服務

架構

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

以3主3從爲例,節點之間的兩兩交互,共享數據分片、節點狀態等信息。

數據分佈

如果是希望數據分佈相對均勻的話,我們首先可以考慮哈希後取模。

哈希後取模

例如,hash(key)%N,根據餘數,決定映射到哪一個節點。這種方式比較簡單,屬於靜態的分片規則。但是一旦節點數量變化,新增或者減少,由於取模的N發生變化,數據需要重新分佈。

爲了解決這個問題,我們又有了一致性哈希算法。

一致性哈希

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

假設我們有四臺機器要哈希環來實現映射(分佈數據),我們先根據機器的名稱或者IP計算哈希值,然後紛紛不到哈希環中(紅色圓圈)。

現在有4條數據或者4個訪問請求,對key計算後,得到哈希環中的位置(綠色圓圈)。沿哈希環順時針找到的第一個Node,就是數據存儲的節點。

在這種情況下,新增了一個Node5節點,不影響數據的分佈。

刪除了一個節點Node4,隻影響相鄰的一個節點。

谷歌的MurmurHash就是一致性哈希算法。在分佈式系統中,負載均衡、分庫分表等場景中都有應用。

一致性哈希解決了動態增減節點時,所有數據都需要重新分佈的問題,它只會影響到下一個相鄰的節點,對其他節點沒有影響。

但是這樣的一致性哈希算法有一個缺點,因爲節點不一定是均勻地分佈的,特別是在節點數比較少的情況下,所以數據不能得到均勻分佈。解決這個問題的辦法是引入虛擬節點(Virtual Node)。

比如:2個節點,5條數據,只有1條分佈到Node2,4條分佈到Node1,不均勻。

Node1設置了兩個虛擬節點,Node2也設置了兩個虛擬節點(虛線圓圈)。

這時候有3條數據分佈到Node1,1條數據分佈到Node2.

Redis虛擬槽分區

Redis既沒有用哈希取模,也沒有用一致性哈希,而是用虛擬槽來實現的。

Redis創建了16384個槽(slot),每個節點負責一定區間的slot。比如Node1負責0-5460,Node2負責5461-10922,Node3負責10923-16383 。

Redis的每個master節點維護一個16384位(2048bytes=2KB)的位序列,比如:序列的第0位是1,就代表第一個slot是它負責;序列的第一位是0,代表第二個slot不歸它負責。

對象分佈到Redis節點上時,對key用CRC16算法計算再%16384,得到一個slot的值,數據落到負責這個slot的Redis節點上。

查看key屬於哪個slot:

cluster keyslot king

注意:key與slot的關係是永遠不會變的,會變得只有slot和Redis節點得關係。

怎麼讓相關得數據落到同一個節點上

比如有些multi key操作是不能跨節點得,如果要讓某些數據分佈到一個節點上,例如用戶2673得基本信息和金融信息,怎麼辦?

在key裏面加入{hash tag}即可。Redis在計算槽編號得時候只會獲取{}之間得字符串進行槽編號計算,這樣由於上面兩個不同得鍵,{}裏面得字符串是相同得,因此它們可以被計算出相同得槽。

客戶端重定向

比如在7291端口得Redis得redis-cli客戶端操作:

set king 1
#(error) MOVED 13724 127.0.0.1:7293

服務端返回MOVED,也就是根據key計算出來的slot不歸7191端口管理,而是歸9293端口管理,服務端返回MOVED告訴客戶端去7293端口操作。

這個時候更換端口,用redis-cli -p 7293操作,纔會返回OK。或者用

./redis-cli -c -p port

-c代表cluster。這樣客戶端需要連續兩次。

Jedis等客戶端會在本地維護一份slot——node的映射關係,大部分時候不需要重定向,所以叫做smart jedis(需要客戶端支持)。

新增或下線了Master節點,數據怎麼遷移(重新分配)?

數據遷移

因爲key和slot的關係是永遠不會變的,當新增了節點的時候,需要把原有的slot分配給新的節點負責,並且把相關的數據遷移過來。

添加新節點(新增一個7297):

redis-cli --cluster add-node 127.0.0.1:7291 127.0.0.1:7297

新增的節點沒有哈希槽,不能分佈數據,在原來的任意一個節點上執行:

redis-cli --cluster reshard 127.0.0.1:7291

輸入需要分配的哈希槽的數量(比如500),和哈希槽的來源節點(可以輸入all或者id)。

高可用和主從切換原理

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

  1. slave發現自己的master變爲FAIL
  2. 將自己記錄的集羣currentEpoch加1,並廣播FAILOVER_AUTH_REQUEST信息
  3. 其他節點收到該信息,只有master響應,判斷請求者的合法性,併發送FAILOVER_AUTH_ACK,對每一個epoch只發送一次ack
  4. 嘗試failover的slave收集FAILOVER_AUTH_ACK
  5. 超過半數後變成新Master
  6. 廣播通知其他集羣節點。

Redis Cluster既能夠實現主從的角色分配,又能夠實現主從切換,相當於集成了Replication和Sentinel的功能。

總結

優勢:

  1. 無中心架構
  2. 數據按照slot存儲分佈在多個節點,節點間數據共享,可動態調整數據分佈
  3. 可擴展性,可線性擴展到1000個節點(官方推薦不超過1000個),節點可動態添加或刪除
  4. 高可用性,部分節點不可用時,集羣仍可用。通過增加Slave做standby數據副本,能夠實現故障自動failover,系欸但之間通過gossip協議交換狀態信息,用投票機制完成slave到master的角色提升。
  5. 降低運維成本,提高系統的擴展性和可用性。

不足:

  1. Client實現複雜,驅動要求實現Smart Client ,緩存slots mapping信息並且及時更新,提高了開發難度,客戶端的不成熟影響業務的穩定性。
  2. 節點會因爲某些原因發生阻塞(阻塞時間大於cluster-node-timeout),被判斷下線,這種failover是沒有必要的。
  3. 數據通過異步複製,不保證數據的強一致性。
  4. 多個業務使用同一套集羣時,無法根據統計區分冷熱數據,資源隔離性較差,容易出現相互影響的情況。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章