Redis哨兵機制全面深入分析與講解[實戰演示篇]

奕鵬發表於2021-04-18

文章簡介

本文將通過理論+實踐的方式從頭到尾總結Redis中的哨兵機制。文章內容從主從複製的弊端如何解決弊端什麼是哨兵哨兵監控的圖形結構哨兵監控的原理如何配置哨兵哨兵與主從複製的關係等方面來演示。

文中相關資料下載地址:連結: pan.baidu.com/s/1cDV9eXuUwuA0QFELD... 密碼: mv86

主從複製弊端

Snipaste_2021-04-16_15-04-45

上面的圖形結構,大致的可以理解為Redis的主從複製拓撲圖。

  1. 其中1個主節點負責應用系統的寫入資料,另外的4個從節點負責應用系統的讀資料。

  2. 同時4個從節點向其中的1個一個主節點發起復制請求操作。

在Redis服務執行正常的情況下,該拓撲結結構不會出現什麼問題。試想一下這樣的一個場景。如果主節點服務發生了異常,不能正常處理服務(如寫入資料、主從複製操作)。這時候,Redis服務能正常響應應用系統的讀操作,但是沒法進行寫操作。 出現該情況就會嚴重影響到系統的業務資料。那該如何解決呢?

可以大致想到下面的幾種情況來解決。

  1. 當主節點發生異常情況時,手動的從部分從節點中選擇一個節點作為主節點。然後改變其他從節點的主從複製關係。

  2. 我們也可以寫一套自動處理該情況的服務,避免依賴於人為的操作。

上面的方案在一定程度上是能幫助我們解決問題。但是過多的人為干預。例如第1點,我們需要考慮人工處理的實時性和正確性。第2點,自動化處理是能夠很好的解決第1點中的問題,但是自動處理存在如何選擇新主節點的問題,因此這也是一個不好的地方。

通過上面大致的分析,我們不難得出Redis的哨兵機制就是針對種種問題出現的。

什麼是哨兵

可以把Redis的哨兵理解為一種Redis分散式架構。 該架構中主要存在兩種角色,一種是哨兵,另外一種是資料節點(主從複製節點)。
Snipaste_2021-04-16_15-38-08

哨兵主要負責的任務是:

  1. 每一個哨兵都會監控資料節點以及其他的哨兵節點。

  2. 當其中的一個哨兵監控到節點不可達是,會給對應的節點做下線標識。如果下線的節點為主節點。這時候會通知其他的哨兵節點。

  3. 哨兵節點通過“協商”推舉出從節點中的某一個節點為主節點。

  4. 接著將其他的從節點斷開與舊主節點的複製關係,將推舉出來的新主節點作為從節點的主節點。

  5. 將切換的結果通知給應用系統。

Snipaste_2021-04-16_15-43-04

配置哨兵

在演示環境中,配置了三臺資料節點(1主2從),三臺哨兵節點。演示中用到的Redis為6.0.8版本。

角色 IP 埠號
(資料節點)master 127.0.0.1 8002
(資料節點)slave 127.0.0.1 8003
(資料節點)slave 127.0.0.1 8004
哨兵節點 127.0.0.1 8005
哨兵節點 127.0.0.1 8006
哨兵節點 127.0.0.1 8007
  1. (資料節點)master配置。
# 服務配置
daemonize yes

# 埠號
port 8002

# 資料目錄
dir "/Users/kert/config/redis/8002"

# 日誌檔名稱
logfile "8002.log"

# 設定密碼
bind 0.0.0.0
# requirepass 8002

# 多執行緒
# 1.開啟執行緒數。
io-threads 2
# 2.開啟讀執行緒。
io-threads-do-reads yes

# 持久化儲存(RDB)
# 1.每多少秒至少有多少個key發生變化,則執行save命令。
save 10 1
save 20 1
save 30 1
# 2.當bgsave命令發生錯誤時,停止寫入操作。
stop-writes-on-bgsave-error yes
# 3.是否開啟rbd檔案壓縮
rdbcompression yes
  1. (資料節點)slave配置。
# 服務配置
daemonize yes

port 8004

dir "/Users/kert/config/redis/8004"

logfile "8004.log"

# 多執行緒
# 1.開啟執行緒數。
io-threads 2
# 2.開啟讀執行緒。
io-threads-do-reads yes

# 持久化儲存(RDB)
# 1.每多少秒至少有多少個key發生變化,則執行save命令。
save 10 1
save 20 1
save 30 1
# 2.當bgsave命令發生錯誤時,停止寫入操作。
stop-writes-on-bgsave-error yes
# 3.是否開啟rbd檔案壓縮
rdbcompression yes

# 配置主節點資訊
replicaof 127.0.0.1 8002
  1. 哨兵節點配置。
# 埠號
port 8006
# 執行模式
daemonize yes
# 資料目錄
dir "/Users/kert/config/redis/sentinel/8006"
# 日誌檔案
logfile "8006.log"
# 監聽資料節點
sentinel monitor mymaster 127.0.0.1 8002 2(判定主節點下線狀態的票數)
# 設定主節點連線許可權資訊
sentinel auth-pass mymaster 8002
# 判斷資料節點和sentinel節點多少毫秒數內沒有響應ping,則處理為下線狀態
sentinel down-after-milliseconds mymaster 30000
# 主節點下線後,從節點向新的主節點發起復制的個數限制(指的一次同時允許幾個從節點)。
sentinel parallel-syncs mymaster 1
# 故障轉移超時時間
sentinel failover-timeout mymaster 180000

所有的哨兵節點直接將port、dir和logfile修改為對應的具體哨兵資訊即可。

接著啟動對應的服務Redis服務。

// 啟動master節點
kert@kertdeMacBook-Pro-2  ~/config/redis/8002  redis-server ./redis.conf

// 啟動slave節點
kert@kertdeMacBook-Pro-2  ~/config/redis/8003  redis-server ./redis.conf
kert@kertdeMacBook-Pro-2  ~/config/redis/8004  redis-server ./redis.conf

// 啟動哨兵節點
kert@kertdeMacBook-Pro-2  ~/config/redis/sentinel  redis-sentinel 8007.conf
kert@kertdeMacBook-Pro-2  ~/config/redis/sentinel  redis-sentinel 8006.conf
kert@kertdeMacBook-Pro-2  ~/config/redis/sentinel  redis-sentinel 8005.conf

哨兵啟動,需要用到Redis安裝完之後自帶的 redis-sentinel命令。

檢視Redis服務執行狀態。

 kert@kertdeMacBook-Pro-2  ~/config/redis/sentinel  ps -ef | grep redis
  501 99742     1   0  3:53PM ??         0:00.47 redis-server 0.0.0.0:8002
  501 99776     1   0  3:53PM ??         0:00.36 redis-server 0.0.0.0:8003
  501 99799     1   0  3:53PM ??         0:00.10 redis-server *:8004
  501 99849     1   0  3:53PM ??         0:00.06 redis-sentinel *:8007 [sentinel]
  501 99858     1   0  3:53PM ??         0:00.04 redis-sentinel *:8006 [sentinel]
  501 99867     1   0  3:53PM ??         0:00.03 redis-sentinel *:8005 [sentinel]

看到上面的結果,則表示我們的Redis服務已經正常啟動。

演示故障切換

我們先開啟三個終端,分配時master節點和兩個slave節點。檢測是否能夠正常進行主從複製。

Snipaste_2021-04-16_16-01-40
我們在主節點任意寫入一些資料,然後在從節點進行查詢資料。為了方便,後面將master稱作1號終端,兩個slave分配叫做2號和3號終端。

  1. 我們在1號終端寫入資料。
127.0.0.1:8002> set name tony
OK
127.0.0.1:8002> set age 1
OK
127.0.0.1:8002> set socre 1
OK
127.0.0.1:8002>
  1. 接著在2號和3號終端下面執行如下的查詢操作。
127.0.0.1:8003> get name
"tony"
127.0.0.1:8003> get age
"1"
127.0.0.1:8003> get socre
"1"

事實證明我們的主從複製是成功的,接下來我們就停掉master節點的服務。

我們實現檢視一下哨兵節點的一個狀態資訊。

  1. 檢視哨兵埠為8005的節點。
kert@kertdeMacBook-Pro-2  ~  redis-cli -p 8005 info
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=127.0.0.1:8002,slaves=2,sentinels=3
  1. 檢視哨兵埠為8006的節點。
kert@kertdeMacBook-Pro-2  ~  redis-cli -p 8006 info
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=127.0.0.1:8002,slaves=2,sentinels=3
  1. 檢視哨兵埠為8007的節點。
kert@kertdeMacBook-Pro-2  ~  redis-cli -p 8007 info
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=127.0.0.1:8002,slaves=2,sentinels=3

通過上面的幾個狀態資訊,我們可以看到哨兵檢測的主節點資訊,主節點下面有幾個從節點,同時哨兵節點有幾個。

我們殺掉master的程式。可以看到1號埠自動斷開了連線。

Snipaste_2021-04-16_16-15-12

接著我們通過哨兵機制檢視一下資料節點狀態資訊。

kert@kertdeMacBook-Pro-2  ~  redis-cli -p 8005 info
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=127.0.0.1:8004,slaves=2,sentinels=3

通過上面的查詢結果,我們可以看到address的值程式設計了8004埠了,其他的資訊沒有發生改變,說明哨兵已經完成切換工作。

接下來我們在新的主節點執行操作命令,檢視在從節點是否能夠完成主從複製。

  1. 在3號埠(新的master)執行一個del命令。
127.0.0.1:8004> del age
(integer) 1
127.0.0.1:8004> keys *
1) "name"
2) "socre"
  1. 在2號埠執行讀命令。
127.0.0.1:8003> keys *
1) "socre"
2) "name"

此時可以發現我們的主從複製也是正常的。

  1. 啟動舊的master,並執行讀命令。
 kert@kertdeMacBook-Pro-2  ~/config/redis/8002  redis-server ./redis.conf
 kert@kertdeMacBook-Pro-2  ~/config/redis/8002  redis-cli -p 8002
127.0.0.1:8002> keys *
1) "name"
2) "socre"

此時你也會發現,原來的master節點變成了slave節點,並且能夠正常複製新master節點的資料。

配置檔案對比

在我們啟動了哨兵模式之後,我們的哨兵配置檔案和資料節點配置檔案的內容都會自動的生成一個特定的內容。

  1. 資料節點(master距離)。

變化前

# 服務配置
daemonize yes

# 埠號
port 8002

# 資料目錄
dir "/Users/kert/config/redis/8002"

# 日誌檔名稱
logfile "8002.log"

# 設定密碼
bind 0.0.0.0
# requirepass 8002

# 多執行緒
# 1.開啟執行緒數。
io-threads 2
# 2.開啟讀執行緒。
io-threads-do-reads yes
# 持久化儲存(RDB)
# 1.每多少秒至少有多少個key發生變化,則執行save命令。
save 10 1
save 20 1
save 30 1
# 2.當bgsave命令發生錯誤時,停止寫入操作。
stop-writes-on-bgsave-error yes
# 3.是否開啟rbd檔案壓縮
rdbcompression yes

變化後

# 服務配置
daemonize yes

# 埠號
port 8002

# 資料目錄
dir "/Users/kert/config/redis/8002"

# 日誌檔名稱
logfile "8002.log"

# 設定密碼
bind 0.0.0.0
# requirepass 8002

# 多執行緒
# 1.開啟執行緒數。
io-threads 2
# 2.開啟讀執行緒。
io-threads-do-reads yes

# 持久化儲存(RDB)
# 1.每多少秒至少有多少個key發生變化,則執行save命令。
save 10 1
save 20 1
save 30 1
# 2.當bgsave命令發生錯誤時,停止寫入操作。
stop-writes-on-bgsave-error yes
# 3.是否開啟rbd檔案壓縮
rdbcompression yes
# Generated by CONFIG REWRITE
pidfile "/var/run/redis.pid"
user default on nopass ~* +@all

replicaof 127.0.0.1 8004
  1. 哨兵節點

變化前

# 埠號
port 8006
# 執行模式
daemonize yes
# 資料目錄
dir "/Users/kert/config/redis/sentinel/8006"
# 日誌檔案
logfile "8006.log"
# 監聽資料節點
sentinel monitor mymaster 127.0.0.1 8002 2(判定主節點下線狀態的票數)
# 設定主節點連線許可權資訊
sentinel auth-pass mymaster 8002
# 判斷資料節點和sentinel節點多少毫秒數內沒有響應ping,則處理為下線狀態
sentinel down-after-milliseconds mymaster 30000
# 主節點下線後,從節點向新的主節點發起復制的個數限制(指的一次同時允許幾個從節點)。
sentinel parallel-syncs mymaster 1
# 故障轉移超時時間
sentinel failover-timeout mymaster 180000

變化後

# 埠號
port 8005
# 執行模式
daemonize yes
# 資料目錄
dir "/Users/kert/config/redis/sentinel/8005"
# 日誌檔案
logfile "8005.log"
# 監聽資料節點
sentinel myid 5724fd60af87e728e6f8f03ded693960c983e156
# 判斷資料節點和sentinel節點多少毫秒數內沒有響應ping,則處理為下線狀態
sentinel deny-scripts-reconfig yes
# 主節點下線後,從節點向新的主節點發起復制的個數限制(指的一次同時允許幾個從節點)。
sentinel monitor mymaster 127.0.0.1 8004 2
# 故障轉移超時時間
sentinel config-epoch mymaster 3
# Generated by CONFIG REWRITE
protected-mode no
user default on nopass ~* +@all
sentinel leader-epoch mymaster 3
sentinel known-replica mymaster 127.0.0.1 8002
sentinel known-replica mymaster 127.0.0.1 8003
sentinel known-sentinel mymaster 127.0.0.1 8006 8fbd2cce642c881f752775afee9b3591e0d90dc6
sentinel known-sentinel mymaster 127.0.0.1 8007 69530c74791e5f32db1c2a006c826a6463bc6496
sentinel current-epoch 3
pidfile "/var/run/redis.pid"

實戰程式碼

這裡我們使用PHP原生類操作Redis哨兵,首先我們建立一個Redis操作類,類中程式碼如下:

class OperationRedis
{
 private $redis;

    private $requestParams;

    private $redisHandler;

    /**
     * 本機IP地址
     * @var string
     */
    private $redisHost = '192.168.2.102';

    public function __construct()
    {
        $this->requestParams = Request::instance()->all();
        $this->redisHandler  = new \Redis();
        $this->redis         = $this->redisHandler->connect($this->redisHost, 8005);
    }

    /**
     * 獲取Redis哨兵資訊
     * @author kert
     */
    public function getRedisNode()
    {
        /** @var array $masterLists 通過sentinel節點獲取所有主節點 */
        $masterLists = $this->redisHandler->rawCommand('SENTINEL', 'masters');
        dump('master列表配置資訊', $masterLists);
        foreach ($masterLists as $value) {
            /** @var array $masterInfo 主節點資訊 */
            $masterInfo = $this->redisHandler->rawCommand('SENTINEL', 'master', $value[1]);
            dump('master節點資訊', $masterInfo);

            // 向主節點插入資料
            $insertNumber = $this->insertInfoByMaster((string)$this->redisHost, (int)$value[5]);
            dump('Redis佇列數量', $insertNumber);

            /** @var array $slaveLists 主節點下面的從節點資訊 */
            $slaveLists = $this->redisHandler->rawCommand('SENTINEL', 'slaves', $value[1]);
            dump('master下的slave節點資訊', $slaveLists);

            foreach ($slaveLists as $v) {
                $this->getInfoBySlave((string)$this->redisHost, (int)$v[5]);
            }
        }
    }

    /**
     * 向主節點插入資料
     * @param string $masterIp
     * @param int $port
     * @return int
     * @author kert
     */
    private function insertInfoByMaster(string $masterIp, int $port): int
    {
        $masterRedis = new \Redis();
        $masterRedis->connect($masterIp, $port);

        return $masterRedis->lPush('sentinel', time());
    }

    /**
     * 向所有的從節點獲取資料
     * @param string $slaveIp
     * @param int $port
     * @author kert
     */
    private function getInfoBySlave(string $slaveIp, int $port)
    {
        $slaveRedis = new \Redis();
        $slaveRedis->connect($slaveIp, $port);
        $array = $slaveRedis->lRange('sentinel', 0, 1000);
        echo "從節點{$slaveIp},埠號{$port}獲取到的對應資料為:" . PHP_EOL;
        dump($array);
    }
}

通過訪問該程式碼,得到如下結果:

Snipaste_2021-04-17_16-28-59

改程式碼在實際的生產中,肯定使用時不對的,這裡只是為了演示程式碼如何操作哨兵。

其中的操作邏輯大致如下圖:
Snipaste_2021-04-17_17-11-46

Laravel框架配置哨兵

Laravel框架自帶Redis操作類。我們只需要簡單配置即可。找到config/database.php檔案。設定如下配置資訊即可:

'redis' => [
'client' => env('REDIS_CLIENT', 'predis'),
'options' => [
    'cluster' => env('REDIS_CLUSTER', 'predis'),
    'prefix'  => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_database_'),
],

'default' => [
    'tcp://192.168.2.102:8005',
    'tcp://192.168.2.102:8006',
    'tcp://192.168.2.102:8007',    //這3個都是sentinel節點的地址
    'options' => [
        'replication' => 'sentinel',
        'service'     => env('REDIS_SENTINEL_SERVICE', 'mymaster'),    //sentinel
        'parameters'  => [
            'password' => env('REDIS_PASSWORD', null),    //redis的密碼,沒有時寫null
            'database' => 0,
        ],
    ],           
    'database' => env('REDIS_DB', 0),
  ],
],

接下來就可以直接操作Redis資料了。

public function laravelRedis()
{
    var_dump(Redis::connection()->set(time(), time()));
}
// output
object(Predis\Response\Status)#237 (1) {
  ["payload":"Predis\Response\Status":private]=>
  string(2) "OK"
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章