Redis分佈式篇

Redis分佈式篇

1 爲什麼 需要 Redis 集羣

1.1 爲什麼需要集羣?

1.1.1 性能

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

1.1.2 擴展

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

1.1.3 可用性

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

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

2 Redis 主從 複製 ( replication )

2.1 主從 複製配置

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

slaveof 192.168.8.203 6379

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

# Generated by CONFIG REWRITE
replicaof 192.168.8.203 6379

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

./redis-server --slaveof 192.168.8.203 637

或在客戶端直接執行 slaveof xx xx,使該 Redis 實例成爲從節點。
啓動後,查看集羣狀態:

redis> info replication

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

127.0.0.1:6379> set sunda 666
(error) READONLY You can't write against a read only replica.

主節點寫入後,slave 會自動從 master 同步數據。
斷開復制:

redis> slaveof no one

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

2.2 主從複製原理

2.2.1 連接 階段

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

2.2.2 數據 同步階段

​ 3、master node 第一次執行全量複製,通過 bgsave 命令在本地生成一份 RDB 快照,將 RDB 快照文件發給 slave node(如果超時會重連,可以調大 repl-timeout 的值)。slave node 首先清除自己的舊數據,然後用 RDB 文件加載數據。

問題:生成 RDB 期間,master 接收到的命令怎麼處理?

開始生成 RDB 文件時,master 會把所有新的寫命令緩存在內存中。在 slave node保存了 RDB 之後,再將新的寫命令複製給 slave node。

2.2.3 命令傳播階段

​ 4、master node 持續將寫命令,異步複製給 slave node

​ 延遲是不可避免的,只能通過優化網絡。

repl-disable-tcp-nodelay no

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

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

問題:如果從節點有一段時間斷開了與主節點的連接是不是要重新全量複製一遍?如果可以增量複製,怎麼知道上次複製到哪裏?

通過 master_repl_offset 記錄的偏移量

redis> info replication

1571747119946.png

2.3 主從複製的 不足

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

​ 1、RDB 文件過大的情況下,同步非常耗時。
​ 2、在一主一從或者一主多從的情況下,如果主服務器掛了,對外提供的服務就不可用了,單點問題沒有得到解決。如果每次都是手動把之前的從服務器切換成主服務器,這個比較費時費力,還會造成一定時間的服務不可用。

3 可用性保證之 Sentinel

3.1 Sentinel 原理

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

問題是:如果監控服務器本身出問題了怎麼辦?那我們就拿不到 master 的地址了,應用也沒有辦法訪問。
那我們再創建一個監控服務器,來監控監控服務器……似乎陷入死循環了,這個問題怎麼解決?這個問題先放着。
Redis 的 Sentinel 就是這種思路:通過運行監控服務器來保證服務的可用性。

官網:
https://redis.io/topics/sentinel

​ 從 Redis2.8 版本起,提供了一個穩定版本的 Sentinel(哨兵),用來解決高可用的問題。它是一個特殊狀態的 redis 實例。
​ 我們會啓動一個或者多個 Sentinel 的服務(通過 src/redis-sentinel),它本質上只是一個運行在特殊模式之下的 Redis,Sentinel 通過 info 命令得到被監聽 Redis 機器的master,slave 等信息。
1571747203355.png

爲了保證監控服務器的可用性,我們會對 Sentinel 做集羣的部署。Sentinel 既監控所有的 Redis 服務,Sentinel 之間也相互監控。
注意:Sentinel 本身沒有主從之分,只有 Redis 服務節點有主從之分。
概念梳理:master,slave(redis group),sentinel,sentinel 集合

3.1.1 服務 下線

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

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

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

3.1.2 故障 轉移

​ 如果 master 被標記爲下線,就會開始故障轉移流程。
​ 既然有這麼多的 Sentinel 節點,由誰來做故障轉移的事情呢?
​ 故障轉移流程的第一步就是在 Sentinel 集羣選擇一個 Leader,由 Leader 完成故障轉移流程。Sentinle 通過 Raft 算法,實現 Sentinel 選舉。

3.1.2.1 Ratf 算法

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

​ 大體上有兩個步驟:領導選舉,數據複製。
​ Raft 是一個共識算法(consensus algorithm)。比如比特幣之類的加密貨幣,就需要共識算法。Spring Cloud 的註冊中心解決方案 Consul 也用到了 Raft 協議。
​ Raft 的核心思想:先到先得,少數服從多數。
​ Raft 算法演示:

http://thesecretlivesofdata.com/raft/
總結:
Sentinle 的 Raft 算法和 Raft 論文略有不同。
1、master 客觀下線觸發選舉,而不是過了 election timeout 時間開始選舉。
2、Leader 並不會把自己成爲 Leader 的消息發給其他 Sentinel。其他 Sentinel 等待 Leader 從 slave 選出 master 後,檢測到新的 master 正常工作後,就會去掉客觀下線的標識,從而不需要進入故障轉移流程。

3.1.2.2 故障轉移

​ 問題:怎麼讓一個原來的 slave 節點成爲主節點?
​ 1、選出 Sentinel Leader 之後,由 Sentinel Leader 向某個節點發送 slaveof no one命令,讓它成爲獨立節點。
​ 2、然後向其他節點發送 slaveof x.x.x.x xxxx(本機服務),讓它們成爲這個節點的子節點,故障轉移完成。
​ 問題:這麼多從節點,選誰成爲主節點?
​ 關於從節點選舉,一共有四個因素影響選舉的結果,分別是斷開連接時長、優先級排序、複製數量、進程 id。
​ 如果與哨兵連接斷開的比較久,超過了某個閾值,就直接失去了選舉權。如果擁有選舉權,那就看誰的優先級高,這個在配置文件裏可以設置(replica-priority 100),數值越小優先級越高。
​ 如果優先級相同,就看誰從 master 中複製的數據最多(複製偏移量最大),選最多的那個,如果複製數量也相同,就選擇進程 id 最小的那個。

3.2 Sentinel 的功能總結

  • Monitoring. Sentinel constantly checks if your master and slave instances are working as expected.
  • Notification. Sentinel can notify the system administrator, another computer programs, via an API, that something iswrong 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 slave is promoted to master, the other additional slaves are reconfigured to use the new master, and the applications using the Redis server 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 發出通知。
​ 自動故障轉移(failover):如果主服務器發生故障,Sentinel 可以啓動故障轉移過程。把某臺服務器升級爲主服務器,併發出通知。
​ 配置管理:客戶端連接到 Sentinel,獲取當前的 Redis 主服務器的地址。

3.3 Sentinel 實戰

3.3.1 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 安裝路徑/usr/local/soft/redis-5.0.5/爲例。
在 204 和 205 的 src/redis.conf 配置文件中添加

slaveof 192.168.8.203 6379

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

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

三臺服務器內容相同:

daemonize yes
port 26379
protected-mode no
dir "/usr/local/soft/redis-5.0.5/sentinel-tmp"
sentinel monitor redis-master 192.168.2.203 6379 2
sentinel down-after-milliseconds redis-master 30000
sentinel failover-timeout redis-master 180000
sentinel parallel-syncs redis-master 1

​ 上面出現了 4 個'redis-master',這個名稱要統一,並且使用客戶端(比如 Jedis)連接的時候名稱要正確。

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

3.3.2 Sentinel 驗證

啓動 Redis 服務和 Sentinel

cd /usr/local/soft/redis-5.0.5/src
# 啓動 Redis 節點
./redis-server ../redis.conf
# 啓動 Sentinel 節點
./redis-sentinel ../sentinel.conf
# 或者
./redis-server ../sentinel.conf --sentinel

查看集羣狀態:

redis> info replication

203

1571747818355.png

204 和 205

1571747829582.png

模擬 master 宕機,在 203 執行:

redis> shutdown

205 被選爲新的 Master,只有一個 Slave 節點。

1571747852515.png

注意看 sentinel.conf 裏面的 redis-master 被修改了!
模擬原 master 恢復,在 203 啓動 redis-server。它還是 slave,但是 master 又有兩個 slave 了。

slave 宕機和恢復省略.

3.3.3 Sentinel 連接使用

Jedis 連接 Sentinel

package sentinel;

import redis.clients.jedis.JedisSentinelPool;

import java.util.HashSet;
import java.util.Properties;
import java.util.Set;


public class JedisSentinelTest {
    private static JedisSentinelPool pool;

    private static JedisSentinelPool createJedisPool() {
        // master的名字是sentinel.conf配置文件裏面的名稱
        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;
    }

    public static void main(String[] args) {
        JedisSentinelPool pool = createJedisPool();
        pool.getResource().set("qingshan", "qq"+System.currentTimeMillis());
        System.out.println(pool.getResource().get("qingshan"));
    }
}

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


import com.gupaoedu.util.RedisUtil;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisAppTests {

    @Autowired
    RedisUtil util;

    @Test
    public void contextLoads() {
        util.set("boot", "2673--" +System.currentTimeMillis());
        System.out.println(util.get("boot"));
    }

}
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 節點地址。

3.4 哨兵機制的不足

​ 主從切換的過程中會丟失數據,因爲只有一個 master。
​ 只能單點寫,沒有解決水平擴容的問題。
​ 如果數據量非常大,這個時候我們需要多個 master-slave 的 group,把數據分佈到不同的 group 中。
​ 問題來了,數據怎麼分片?分片之後,怎麼實現路由?

4 Redis 分佈式方案

​ 如果要實現 Redis 數據的分片,我們有三種方案。第一種是在客戶端實現相關的邏輯,例如用取模或者一致性哈希對 key 進行分片,查詢和修改都先判斷 key 的路由。
​ 第二種是把做分片處理的邏輯抽取出來,運行一個獨立的代理服務,客戶端連接到這個代理服務,代理服務做請求的轉發。
​ 第三種就是基於服務端實現。

4.1 客戶端 Sharding

1571748107273.png

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

4.1.1 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 之類的客戶端分片代碼的優勢是配置簡單,不依賴於其他中間件,分區的邏輯可以自定義,比較靈活。但是基於客戶端的方案,不能實現動態的服務增減,每個客戶端需要自行維護分片策略,存在重複代碼。
​ 第二種思路就是把分片的代碼抽取出來,做成一個公共服務,所有的客戶端都連接到這個代理層。由代理層來實現請求和轉發。

4.2 代理 Proxy

1571748259203.png

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

4.2.1 Twemproxy

two-em-proxy
https://github.com/twitter/twemproxy

1571748281722.png

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

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

4.2.2 Codis

https://github.com/CodisLabs/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 協議的客戶端

1571748437326.png

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

1571748485305.png

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

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

不支持的命令
https://github.com/CodisLabs/codis/blob/release3.2/doc/unsupported_cmds.md
獲取數據原理(mget):在 Redis 中的各個實例裏獲取到符合的 key,然後再彙總到 Codis 中。
Codis 是第三方提供的分佈式解決方案,在官方的集羣功能穩定之前,Codis 也得到了大量的應用。

4.3 Redis Cluster

https://redis.io/topics/cluster-tutorial/
Redis Cluster 是在 Redis 3.0 的版本正式推出的,用來解決分佈式的需求,同時也可以實現高可用。跟 Codis 不一樣,它是去中心化的,客戶端可以連接到任意一個可用節點。
數據分片有幾個關鍵的問題需要解決:
1、數據怎麼相對均勻地分片
2、客戶端怎麼訪問到相應的節點和數據
3、重新分片的過程,怎麼保證正常服務

4.3.1 架構

​ Redis Cluster 可以看成是由多個 Redis 實例組成的數據集合。客戶端不需要關注數據的子集到底存儲在哪個節點,只需要關注這個集合整體。
​ 以 3 主 3 從爲例,節點之間兩兩交互,共享數據分片、節點狀態等信息。

1571748577107.png

4.3.2 搭建

首先,本篇要基於單實例的安裝,你的機器上已經有一個Redis
博客 www.sundablog.com

爲了節省機器,我們直接把6個Redis實例安裝在同一臺機器上(3主3從),只是使用不同的端口號。
機器IP 192.168.8.207

cd /usr/local/soft/redis-5.0.5
mkdir redis-cluster
cd redis-cluster
mkdir 7291 7292 7293 7294 7295 7296

複製redis配置文件到7291目錄

cp /usr/local/soft/redis-5.0.5/redis.conf /usr/local/soft/redis-5.0.5/redis-cluster/7291

修改7291的配置文件

port 7291
dir /usr/local/soft/redis-5.0.5/redis-cluster/7291/
cluster-enabled yes
cluster-config-file nodes-7291.conf
cluster-node-timeout 5000
appendonly yes
pidfile /var/run/redis_7291.pid

把7291下的redis.conf複製到其他5個目錄。

cd /usr/local/soft/redis-5.0.5/redis-cluster/7291
cp redis.conf ../7292
cp redis.conf ../7293
cp redis.conf ../7294
cp redis.conf ../7295
cp redis.conf ../7296

批量替換內容

cd /usr/local/soft/redis-5.0.5/redis-cluster
sed -i 's/7291/7292/g' 7292/redis.conf
sed -i 's/7291/7293/g' 7293/redis.conf
sed -i 's/7291/7294/g' 7294/redis.conf
sed -i 's/7291/7295/g' 7295/redis.conf
sed -i 's/7291/7296/g' 7296/redis.conf

安裝ruby依賴、rubygems依賴、gem-redis依賴

yum install ruby -y
yum install rubygems -y
gem install redis -v 3.0.7

啓動6個Redis節點

cd /usr/local/soft/redis-5.0.5/
./src/redis-server redis-cluster/7291/redis.conf
./src/redis-server redis-cluster/7292/redis.conf
./src/redis-server redis-cluster/7293/redis.conf
./src/redis-server redis-cluster/7294/redis.conf
./src/redis-server redis-cluster/7295/redis.conf
./src/redis-server redis-cluster/7296/redis.conf

是否啓動了6個進程

ps -ef|grep redis

創建集羣
舊版本中的redis-trib.rb已經廢棄了,直接用–cluster命令
注意用絕對IP,不要用127.0.0.1

cd /usr/local/soft/redis-5.0.5/src/
redis-cli --cluster create 192.168.8.207:7291 192.168.8.207:7292 192.168.8.207:7293 192.168.8.207:7294 192.168.8.207:7295 192.168.8.207:7296 --cluster-replicas 1

Redis會給出一個預計的方案,對6個節點分配3主3從,如果認爲沒有問題,輸入yes確認

>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 127.0.0.1:7295 to 127.0.0.1:7291
Adding replica 127.0.0.1:7296 to 127.0.0.1:7292
Adding replica 127.0.0.1:7294 to 127.0.0.1:7293
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
M: dfdc9c0589219f727e4fd0ad8dafaf7e0cfb4f1c 127.0.0.1:7291
   slots:[0-5460] (5461 slots) master
M: 8c878b45905bba3d7366c89ec51bd0cd7ce959f8 127.0.0.1:7292
   slots:[5461-10922] (5462 slots) master
M: aeeb7d7076d9b25a7805ac6f508497b43887e599 127.0.0.1:7293
   slots:[10923-16383] (5461 slots) master
S: ebc479e609ff8f6ca9283947530919c559a08f80 127.0.0.1:7294
   replicates aeeb7d7076d9b25a7805ac6f508497b43887e599
S: 49385ed6e58469ef900ec48e5912e5f7b7505f6e 127.0.0.1:7295
   replicates dfdc9c0589219f727e4fd0ad8dafaf7e0cfb4f1c
S: 8d6227aefc4830065624ff6c1dd795d2d5ad094a 127.0.0.1:7296
   replicates 8c878b45905bba3d7366c89ec51bd0cd7ce959f8
Can I set the above configuration? (type 'yes' to accept): 

注意看slot的分佈

7291  [0-5460] (5461個槽) 
7292  [5461-10922] (5462個槽) 
7293  [10923-16383] (5461個槽)

集羣創建完成

>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
....
>>> Performing Cluster Check (using node 127.0.0.1:7291)
M: dfdc9c0589219f727e4fd0ad8dafaf7e0cfb4f1c 127.0.0.1:7291
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
M: 8c878b45905bba3d7366c89ec51bd0cd7ce959f8 127.0.0.1:7292
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
M: aeeb7d7076d9b25a7805ac6f508497b43887e599 127.0.0.1:7293
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
S: 8d6227aefc4830065624ff6c1dd795d2d5ad094a 127.0.0.1:7296
   slots: (0 slots) slave
   replicates aeeb7d7076d9b25a7805ac6f508497b43887e599
S: ebc479e609ff8f6ca9283947530919c559a08f80 127.0.0.1:7294
   slots: (0 slots) slave
   replicates dfdc9c0589219f727e4fd0ad8dafaf7e0cfb4f1c
S: 49385ed6e58469ef900ec48e5912e5f7b7505f6e 127.0.0.1:7295
   slots: (0 slots) slave
   replicates 8c878b45905bba3d7366c89ec51bd0cd7ce959f8
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered

重置集羣的方式是在每個節點上個執行cluster reset,然後重新創建集羣

連接到客戶端

redis-cli -p 7291
redis-cli -p 7292
redis-cli -p 7293

批量寫入值

cd /usr/local/soft/redis-5.0.5/redis-cluster/
vim setkey.sh

腳本內容

#!/bin/bash
for ((i=0;i<20000;i++))
do
echo -en "helloworld" | redis-cli -h 192.168.8.207 -p 7291 -c -x set name$i >>redis.log
done
chmod +x setkey.sh
./setkey.sh

每個節點分佈的數據

127.0.0.1:7292> dbsize
(integer) 6683
127.0.0.1:7293> dbsize
(integer) 6665
127.0.0.1:7291> dbsize
(integer) 6652

其他命令,比如添加節點、刪除節點,重新分佈數據:

redis-cli --cluster help
Cluster Manager Commands:
  create         host1:port1 ... hostN:portN
                 --cluster-replicas <arg>
  check          host:port
                 --cluster-search-multiple-owners
  info           host:port
  fix            host:port
                 --cluster-search-multiple-owners
  reshard        host:port
                 --cluster-from <arg>
                 --cluster-to <arg>
                 --cluster-slots <arg>
                 --cluster-yes
                 --cluster-timeout <arg>
                 --cluster-pipeline <arg>
                 --cluster-replace
  rebalance      host:port
                 --cluster-weight <node1=w1...nodeN=wN>
                 --cluster-use-empty-masters
                 --cluster-timeout <arg>
                 --cluster-simulate
                 --cluster-pipeline <arg>
                 --cluster-threshold <arg>
                 --cluster-replace
  add-node       new_host:new_port existing_host:existing_port
                 --cluster-slave
                 --cluster-master-id <arg>
  del-node       host:port node_id
  call           host:port command arg arg .. arg
  set-timeout    host:port milliseconds
  import         host:port
                 --cluster-from <arg>
                 --cluster-copy
                 --cluster-replace
  help           

For check, fix, reshard, del-node, set-timeout you can specify the host and port of any working node in the cluster.

附錄:

集羣命令

cluster info :打印集羣的信息
cluster nodes :列出集羣當前已知的所有節點(node),以及這些節點的相關信息。
cluster meet :將 ip 和 port 所指定的節點添加到集羣當中,讓它成爲集羣的一份子。
cluster forget :從集羣中移除 node_id 指定的節點(保證空槽道)。
cluster replicate :將當前節點設置爲 node_id 指定的節點的從節點。
cluster saveconfig :將節點的配置文件保存到硬盤裏面。

槽slot命令

cluster addslots [slot …] :將一個或多個槽(slot)指派(assign)給當前節點。
cluster delslots [slot …] :移除一個或多個槽對當前節點的指派。
cluster flushslots :移除指派給當前節點的所有槽,讓當前節點變成一個沒有指派任何槽的節點。
cluster setslot node :將槽 slot 指派給 node_id 指定的節點,如果槽已經指派給另一個節點,那麼先讓另一個節點刪除該槽>,然後再進行指派。
cluster setslot migrating :將本節點的槽 slot 遷移到 node_id 指定的節點中。
cluster setslot importing :從 node_id 指定的節點中導入槽 slot 到本節點。
cluster setslot stable :取消對槽 slot 的導入(import)或者遷移(migrate)。

鍵命令

cluster keyslot :計算鍵 key 應該被放置在哪個槽上。
cluster countkeysinslot :返回槽 slot 目前包含的鍵值對數量。
cluster getkeysinslot :返回 count 個 slot 槽中的鍵

4.3.3 數據分佈

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

4.3.3.1 哈希後 取模

​ 例如,hash(key)%N,根據餘數,決定映射到那一個節點。這種方式比較簡單,屬於靜態的分片規則。但是一旦節點數量變化,新增或者減少,由於取模的 N 發生變化,數據需要重新分佈。
​ 爲了解決這個問題,我們又有了一致性哈希算法。

4.3.3.2 一致性

​ 一致性哈希的原理:
​ 把所有的哈希值空間組織成一個虛擬的圓環(哈希環),整個空間按順時針方向組織。因爲是環形空間,0 和 2^32-1 是重疊的。
​ 假設我們有四臺機器要哈希環來實現映射(分佈數據),我們先根據機器的名稱或者 IP 計算哈希值,然後分佈到哈希環中(紅色圓圈)。

1571749016225.png

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

1571749032547.png

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

1571749046917.png

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

1571749059910.png

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

​ 一致性哈希解決了動態增減節點時,所有數據都需要重新分佈的問題,它只會影響到下一個相鄰的節點,對其他節點沒有影響。
​ 但是這樣的一致性哈希算法有一個缺點,因爲節點不一定是均勻地分佈的,特別是在節點數比較少的情況下,所以數據不能得到均勻分佈。解決這個問題的辦法是引入虛擬節點(Virtual Node)。
​ 比如:2 個節點,5 條數據,只有 1 條分佈到 Node2,4 條分佈到 Node1,不均勻。

1571749095499.png

Node1 設置了兩個虛擬節點,Node2 也設置了兩個虛擬節點(虛線圓圈)。
這時候有 3 條數據分佈到 Node1,1 條數據分佈到 Node2。

1571749105882.png

​ Redis 虛擬槽分區

​ Redis 既沒有用哈希取模,也沒有用一致性哈希,而是用虛擬槽來實現的。
​ Redis 創建了 16384 個槽(slot),每個節點負責一定區間的 slot。比如 Node1 負責 0-5460,Node2 負責 5461-10922,Node3 負責 10923-16383。

1571749129064.png

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

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

​ 查看 key 屬於哪個 slot:

redis> cluster keyslot sunda

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

問題:怎麼讓相關的數據落到同一個節點上?

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

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

user{2673}base=…
user{2673}fin=…
127.0.0.1:7293> set a{qs}a 1
OK
127.0.0.1:7293> set a{qs}b 1
OK
127.0.0.1:7293> set a{qs}c 1
OK
127.0.0.1:7293> set a{qs}d 1
OK
127.0.0.1:7293> set a{qs}e 1
OK

問題:客戶端連接到哪一臺服務器?訪問的數據不在當前節點上,怎麼辦?

4.3.4 客戶端 重定向

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

127.0.0.1:7291> set qs 1
(error) MOVED 13724 127.0.0.1:7293

​ 服務端返回 MOVED,也就是根據 key 計算出來的 slot 不歸 7191 端口管理,而是歸 7293 端口管理,服務端返回 MOVED 告訴客戶端去 7293 端口操作。
​ 這個時候更換端口,用 redis-cli –p 7293 操作,纔會返回 OK。或者用./redis-cli -c -p port 的命令(c 代表 cluster)。這樣客戶端需要連接兩次。
​ Jedis 等客戶端會在本地維護一份 slot——node 的映射關係,大部分時候不需要重定向,所以叫做 smart jedis(需要客戶端支持)。
問題:新增或下線了 Master 節點,數據怎麼遷移(重新分配)?

4.3.5 數據遷移

因爲 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)。

問題:只有主節點可以寫,一個主節點掛了,從節點怎麼變成主節點?

4.3.6 高可用 和主 從 切換原理

​ 當 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.廣播 Pong 通知其他集羣節點。

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

4.3.7 總結

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

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

公衆號

如果大家想要實時關注我更新的文章以及分享的乾貨的話,可以關注我的公衆號。
QQ圖片20191012084332.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章