1. 程式人生 > >Redis Sentinel的使用(基本原理、一主兩從三Sentinel部署、客戶端程式碼使用)

Redis Sentinel的使用(基本原理、一主兩從三Sentinel部署、客戶端程式碼使用)

Redis Sentinel

1. 基本概念

1.1 背景:主從複製的問題

Redis 的主從複製模式可以將主節點的資料改變同步給從節點,這樣從節點就可以起到兩個用:

  • 第一,作為主節點的一個備份,一旦主節點出了故障不可達的情況,從節點可以作為後備“頂” 上來,並且保證資料儘量不丟失(主從複製是最終一致性)。
  • 第二,從節點可以擴充套件主節點的讀能力,一旦主節點不能支撐住大併發量的讀操作,從節點可以在一定程度上幫助主節點分擔讀壓力。

主從複製存在以下問題:

  • (1)一旦主節點出現故障,需要手動將一個從節點晉升為主節點,同時需要修改應用方的主節點地址,還需要命令其他從節點去複製新的主節點,整個過程都需要人工干預。
  • (2)主節點的寫能力受到單機的限制。
  • (3)主節點的儲存能力受到單機的限制。

問題(1)是Redis的高可用問題,可用Redis Sentinel解決;問題(2)、(3)Redis的分散式問題,可用Redis的叢集解決。

1.2 Redis Sentinel 的高可用性

名詞 邏輯結構 物理結構
主節點(master) Redis 主服務/資料庫 一個獨立的Redis程序
從節點(slave) Redis 從服務/資料庫 一個獨立的Redis程序
Redis 資料節點 主節點和從節點 主節點和從節點的程序
Sentinel 節點集合 若干Sentinel 節點的抽象組合 若干Sentine 節點程序
Redis Sentinel Redis高可用實現方案 Sentinel 節點集合和Redis 資料節點程序

Redis Sentinel 是一個分散式架構(這裡的分散式是指:Redis 資料節點、Sentinel 節點集合、客戶端分佈在多個物理節點的架構),其中包含若干個 Sentinel 節點和 Redis 資料點,每個 Sentinel 節點會對資料節點和其餘 Sentinel 節點進行監控,當它發現節點不可達時,會對節點做下線標識如果被標識的是主節點,它還會和其他 Sentinel 節點進行“協商”,當 大多數 Sentinel 節點都認為主節點不可達時,它們會選舉出一個 Sentinel 節點來完成自動 故障轉移的工作,同時會將這個變化實時通知給 Redis 應用方

。整個過程完全是自動的,不需要人工來介入,所以這套方案很有效地解決了 Redis 的高可用問題。

Redis Sentinel 具有以下幾個功能:

  • 監控(Monitoring):Sentinel 節點會定期檢測 Redis 資料節點(包括master和slave)、其餘 Sentinel 節點是否可達。
  • 通知(Notification):Sentinel 節點會將故障轉移的結果通知給應用方。
  • 自動故障遷移(Automatic failover):當master不能正常工作時,Sentinel會實現從節點晉升為主節點並維護後續正確的主從關係。—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.):在 Redis Sentinel 結構中,客戶端 在初始化的時候連線的是 Sentinel 節點集合,從中獲取主節點資訊。—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.

2. Redis Sentinel的部署

下面將以 3 個 Sentinel 節點、1 個主節點、2 個從節點組成一個 Redis Sentinel 進行 說明,並且使用了簡單的加密,拓撲結構如下圖所示。

在這裡插入圖片描述

2.1 部署 Redis 的master和slave節點

啟動master

  • 配置

redis-6379.conf 檔案

port 6379
daemonize yes
logfile "/data/lincoln/redis-stable/mydata/6379.log"
dbfilename "dump-6379.rdb"
dir "/data/lincoln/redis-stable/mydata"

#密碼
requirepass "My-secret-pass"
masterauth  "My-secret-pass"
  • 啟動master
$ ./redis-server ../mydata/redis-6379.conf
  • 驗證是否啟動
$ ./redis-cli -h 127.0.0.1 -p 6379 
127.0.0.1:6379> AUTH My-secret-pass
OK
127.0.0.1:6379> PING
PONG

啟動slave

  • 配置

兩個從節點的配置是完全一樣的,和主節點的配置不一樣的是添加了 slaveof 配置。

port 6380
daemonize yes
logfile "/data/lincoln/redis-stable/mydata/6380.log"
dbfilename "dump-6380.rdb"
dir "/data/lincoln/redis-stable/mydata"
requirepass "My-secret-pass"
masterauth  "My-secret-pass"
#增加了slaveof配置,其餘和主節點一樣
slaveof 127.0.0.1 6379
  • 啟動節點
$ ./redis-server ../mydata/redis-6380.conf 
$ ./redis-server ../mydata/redis-6381.conf
  • 驗證是否啟動
$ ./redis-cli-h 127.0.0.1 -p 6380 #也是需要AUTH驗證密碼的,以下省略
127.0.0.1:6380> PING
PONG 
$ ./redis-cli-h 127.0.0.1 -p 6381 
127.0.0.1:6381> PING
PONG

確認主從關係

連線主節點檢視:

$ ./redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> INFO  replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=154,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=154,lag=0
...

連線從節點檢視:

$ ./redis-cli -h 127.0.0.1 -p 6380
127.0.0.1:6380> info  replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
...

此時的拓撲圖如下所示:

在這裡插入圖片描述

2.2 部署Sentinel節點

3 個 Sentinel 節點的部署方法是完全一致的(埠不同)。

  • 配置
port 26379
daemonize yes
dir "/data/lincoln/redis-stable/mydata"
logfile "/data/lincoln/redis-stable/mydata/26379.log"

#當前Sentinel節點監控 127.0.0.1:6379 這個主節點  
#2代表判斷主節點失敗至少需要2個Sentinel節點節點同意  
#mymaster是主節點的別名  
sentinel monitor mymaster 127.0.0.1 6379 2

#每個Sentinel節點都要定期PING命令來判斷Redis資料節點和其餘Sentinel節點是否可達,如果超過30000毫秒且沒有回覆,則判定不可達 
sentinel down-after-milliseconds mymaster 30000

#當Sentinel節點集合對主節點故障判定達成一致時,Sentinel領導者節點會做故障轉移操作,選出新的主節點,原來的從節點會向新的主節點發起復制操作,限制每次向新的主節點發起復制操作的從節點個數為1
sentinel parallel-syncs mymaster 1

#故障轉移超時時間為180000毫秒 
sentinel failover-timeout mymaster 180000

#master的密碼
sentinel auth-pass mymaster "My-secret-pass"
  • 啟動sentinel
#法1:
$ ./redis-sentinel ../mydata/sentinel-26379.conf 
#法2:
$ ./redis-server ../mydata/sentinel-26379.conf --sentinel 
#啟動其餘兩個sentinel
$ ./redis-sentinel ../mydata/sentinel-26380.conf 
$ ./redis-sentinel ../mydata/sentinel-26381.conf 
  • 驗證是否啟動:
$ ./redis-cli -h 127.0.0.1 -p 26379
127.0.0.1:26379> info sentinel
# 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:6379,slaves=2,sentinels=3

2.3 Sentinel配置項說明

(1)sentinel monitor

配置如下:sentinel monitor <master-name> <ip> <port> <quorum>

  • Sentinel 節點會定期監控主節點,所以從配置上必然也會有所體現,本配置說明 Sentinel 節點要監控的是一個名字叫做,ip 地址和埠為 的主節點。 代表要判定主節點最終不可達所需要的票數。
  • 還與 Sentinel 節點的領導者選舉有關,至少要有 max(quorum,num(sentinels)/2+1)個 Sentinel 節點參與選舉,才能選出領導者 Sentinel,從而完成故障轉移。
  • 實際上 Sentinel 節點會對所有節點 進行監控,但是在 Sentinel 節點的配置中沒有看到有關從節點和其餘 Sentinel 節點的置, 那是因為 Sentinel 節點會從主節點中獲取有關從節點以及其餘 Sentinel 節點的相關資訊。

(2)sentinel down-after-milliseconds

配置如下:sentinel down-after-milliseconds <master-name> <times>

  • 每個 Sentinel 節點都要通過定期傳送 ping 命令來判斷 Redis 資料節點和其餘 Sentinel 節點是否可達,如果超過了 down-after-milliseconds 配置的時間且沒有有效的回覆,則判定節點不可達.
  • down-after-milliseconds 雖然以 為引數,但實際上對 Sentinel 節點、主節點、從節點的失敗判定同時有效。

(3)sentinel parallel-syncs

配置如下:sentinel parallel-syncs <master-name> <nums>

  • 當 Sentinel 節點集合對主節點故障判定達成一致時,Sentinel 領導者節點會做故障轉移 操作,選出新的主節點,原來的從節點會向新的主節點發起復制操作,parallel-syncs 是 用來限制在一次故障轉移之後,每次向新的主節點發起復制操作的從節點個數。

(4)sentinel failover-timeout

(5)sentinel auth-pass

配置如下:sentinel auth-pass <master-name> <password>

  • 如果Sentinel監控的主節點配置了密碼,可以通過sentinel auth-pass配置通過新增主節點的密碼,防止Sentinel節點無法對主節點進行監控。
    • 例如:sentinel auth-pass mymaster MySUPER–secret-0123passw0rd

(6)sentinel notification-script

配置如下:sentinel notification-script <master-name> <script-path>

  • 在故障轉移期間,當一些警告級別的Sentinel事件發生(指重要事件,如主觀下線,客觀下線等)時,會觸發對應路徑的指令碼,向指令碼傳送相應的事件引數。
    • 例如:sentinel notification-script mymaster /var/redis/notify.sh

(7)sentinel client-reconfig-script

配置如下:sentinel client-reconfig-script <master-name> <script-path>

  • 在故障轉移結束後,觸發應對路徑的指令碼,並向指令碼傳送故障轉移結果的引數。
    • 例如:sentinel client-reconfig-script mymaster /var/redis/reconfig.sh。

3. Sentinel API

Sentinel命令sentinel支援的合法命令如下:

  • PING :sentinel回覆PONG.
  • SENTINEL masters:顯示被監控的所有master以及它們的狀態.
  • SENTINEL master :顯示指定master的資訊和狀態;
  • SENTINEL slaves :顯示指定master的所有slave以及它們的狀態;
  • SENTINEL get-master-addr-by-name :返回指定master的ip和埠,如果正在進行failover或者failover已經完成,將會顯示被提升為master的slave的ip和埠。
  • SENTINEL reset :重置名字匹配該正則表示式的所有的master的狀態資訊,清楚其之前的狀態資訊,以及slaves資訊。
  • SENTINEL failover :強制sentinel執行failover,並且不需要得到其他sentinel的同意。但是failover後會將最新的配置傳送給其他sentinel。

4. 客戶端使用程式碼

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <vector>

#include "hiredis.h"

using namespace std;

const string passWord = "My-secret-pass";

struct IpPort
{
    string ip;
    int port;
};

//通過Sentinel獲得master的ip和port
int getMasetAddr(const vector<IpPort> &addVec, struct timeval timeOut,string& masterIp, string& masterPort)
{
    //建立和sentinel的連線
    redisContext* context = NULL;
    redisReply *reply = NULL;
    for(int i = 0; i < addVec.size(); ++i)
    {
        printf("i[%d], ip[%s], port[%d]", i, addVec[i].ip.c_str(), addVec[i].port);
        context = redisConnectWithTimeout(addVec[i].ip.c_str(), addVec[i].port, timeOut);
        if (context == NULL || context->err) 
        {
            printf("Connection error: can't allocate redis context,will find next");
            redisFree(context);//斷開連線並釋放redisContext空間
            continue;
        }
        
        //獲取master的ip和port
        reply = static_cast<redisReply*> ( redisCommand(context,"SENTINEL get-master-addr-by-name  mymaster") );
        if(reply->type != REDIS_REPLY_ARRAY || reply -> elements != 2)
        {
            printf("use sentinel to get-master-addr-by-name failure, will find next\n"); 
            freeReplyObject(reply); 
            continue;
        }
        masterIp = reply -> element[0] -> str;
        masterPort = reply -> element[1] -> str;
        break;
    }

    if(masterIp.empty() || masterPort.empty())
        return -1;
    return 0;
}

int main(int argc, char **argv) {
    unsigned int j;
    redisContext *context;
    redisReply *reply;

    vector<IpPort> addrVec = { {"127.0.0.1",26379}, {"127.0.0.1",26380}, {"127.0.0.1",26381} };
    string master_ip(""); 
    string master_port("");
    struct timeval timeout = { 1, 500000 }; // 1.5 seconds

    if(getMasetAddr(addrVec, timeout, master_ip, master_port) != 0)
    {
        printf("get master name and port failed!");
        exit(1);
    }
    printf("master ip [%s], port[%s]",master_ip.c_str(), master_port.c_str());

    //建立master連線
    context = redisConnectWithTimeout(master_ip.c_str(), strtoul(master_port.c_str(), NULL, 0), timeout);
    if (context == NULL || context->err) 
    {
        if (context) 
        {
            printf("Connection error: %s\n", context->errstr);
            redisFree(context);
        } 
        else 
        {
            printf("Connection error: can't allocate redis context\n");
        }
        exit(1);
    }

    //密碼驗證
    reply = static_cast<redisReply*> ( redisCommand(context,"AUTH %s", passWord.c_str()) );
    printf("redis AUTH result: %s\n",reply -> str);
    if(NULL == reply || strcmp(reply->str, "OK") != 0  )  
    {
		printf("redis AUTH failure"); 
        freeReplyObject(reply); 
        return -1;
    }

    /* Set a key using binary safe API */
    string binary_key = "bar";
    string binary_str ("hell \0 world!!!",15);
    reply = static_cast<redisReply*> ( redisCommand(context,"SET %s %b", binary_key.c_str(), binary_str.c_str(), binary_str.size() ) );
    if(NULL == reply) 
    {
        printf("command execute set key failure\n");
        redisFree(context);
        exit(1); 
    }
        //返回執行結果為狀態的命令。比如set命令的返回值的型別是REDIS_REPLY_STATUS,然後只有當返回資訊是"OK"時,才表示該命令執行成功。
    if( !(reply->type == REDIS_REPLY_STATUS && strcmp(reply->str, "OK") == 0)) 
    {
        printf("command execute set key failure\n"); 
        freeReplyObject(reply); 
        redisFree(context);
        exit(1);
    }
    printf("SET (binary API): %s , vaule size[%d]\n", reply->str, binary_str.size() );
    freeReplyObject(reply);

    //get binary key
    reply = static_cast<redisReply*> ( redisCommand(context,"GET %s", binary_key.c_str()) );
    if(reply->type != REDIS_REPLY_STRING)
    {
        printf("command execute get key [%s] failure\n", binary_key.c_str()); 
        freeReplyObject(reply); 
        redisFree(context);
        exit(1);
    }
    string binary_result(reply -> str, reply -> len);
    cout<<"GET "<<binary_key << ":"<< binary_result <<"value size :"<< binary_result.size()<<endl;
    freeReplyObject(reply);

    /* Set a key */
        //不推薦使用%s,此時binary_str在儲存的時候遇到'\0'就已經發生截斷了
    reply = static_cast<redisReply*> ( redisCommand(context,"SET %s %s", "foo", binary_str.c_str()) );
    if(NULL == reply) 
    {
        printf("command execute set key failure\n");
        redisFree(context);
        exit(1); 
    }
    if(!(reply->type == REDIS_REPLY_STATUS && strcmp(reply->str, "OK") == 0)) 
    {
        printf("command execute set key failure\n"); 
        freeReplyObject(reply); 
        redisFree(context);
        exit(1);
    }
    printf("SET: %s\n", reply->str);
    freeReplyObject(reply);

    //Get a key
    reply = static_cast<redisReply*> ( redisCommand(context,"GET foo") );
    if(reply->type != REDIS_REPLY_STRING)
    {
        printf("command execute get key foo failure\n"); 
        freeReplyObject(reply); 
        redisFree(context);
        exit(1);
    }
    string foo_result(reply -> str, reply -> len);
    cout<<"GET foo :"<< foo_result <<"value size :"<< foo_result.size()<<endl;
    freeReplyObject(reply);

    /* Disconnects and frees the context */
    redisFree(context);

    return 0;
}

5. Redis Sentinel的基本實現原理

本節將介紹 Redis Sentinel 的基本實現原理,具體包含以下幾個方面:Redis Sentinel 的三個定時任務、主觀下線和客觀下線、Sentinel 領導者選舉、故障轉移。

5.1 三個定時監控任務

Redis Sentinel 通過三個定時監控任務完成對各個節點發現和監控:

1)每隔 10 秒,每個 Sentinel 節點會向主節點和從節點發送 info 命令獲取最新的拓撲結構。

這個定時任務的作用具體可以表現在三個方面:

  • 通過向主節點執行 info 命令,獲取從節點的資訊,這也是為什麼 Sentinel 節點不需要 顯式配置監控從節點。
  • 當有新的從節點加入時都可以立刻感知出來。
  • 節點不可達或者故障轉移後,可以通過 info 命令實時更新節點拓撲資訊。

2) 每隔 2 秒,每個 Sentinel 節點會向 Redis 資料節點的__sentinel__:hello 頻道上傳送該 Sentinel 節點對於主節點的判斷以及當前 Sentinel 節點 的資訊,同時每個Sentinel 節點也會訂閱該頻道,來了解其他 Sentinel 節點以及它們對主節點的判斷,所以 這個定時任務可以完成以下兩個工作:

  • 發現新的 Sentinel 節點:通過訂閱主節點的__sentinel__:hello 瞭解其他的 Sentinel 節點資訊,如果是新加入的 Sentinel 節點,將該 Sentinel 節點資訊儲存 起來,並與該 Sentinel 節點建立連線。
  • Sentinel 節點之間交換主節點的狀態,作為客觀下線以及領導者選舉的依據。

3)每隔 1 秒,每個 Sentinel 節點會向主節點、從節點、其餘 Sentinel 節點發送一條 ping 命令做一次心跳檢測,來確認這些節點當前是否可達。

5.2 主觀下線和客觀下線

第三個定時任務,每個 Sentinel 節點會每隔 1 秒對主節點、從節點、其他 Sentinel 節點 傳送 ping 命令做心跳檢測,當這些節點超過 down-after-milliseconds 沒有進行有效回覆,Sentinel 節點就會對該節點做失敗判定,這個行為叫做主觀下線(Subjectively Down,簡稱 SDOWN)

當 Sentinel 主觀下線的節點是主節點時,該 Sentinel 節點會通過 sentinel is-master-down-by-addr 命令向其他 Sentinel 節點詢問對主節點的判斷,當超過 個數,Sentinel 節點認為主節點確實有問題,這時該 Sentinel 節點會做出**客觀下線(Objectively Down,簡稱 ODOWN)**的決定。

  • sentinel is-master-down-by-addr 命令:
sentinel is-master-down-by-addr <ip> <port> <current_epoch> <runid>
  • 引數
    • ip:主節點 IP。
    • port:主節點埠。
    • current_epoch:當前配置紀元。
    • runid:此引數有兩種型別,不同型別決定了此 API 作用的不同。
      • 當 runid 等於“*” 時,作用是 Sentinel 節點直接交換對主節點下線的判定。
      • 當 runid 等於當前 Sentinel 節點的 runid 時,作用是當前 Sentinel 節點希望目標 Sentinel 節點同意自己成為領導者的請求。
  • 返回結果引數:
    • down_state:目標 Sentinel 節點對於主節點的下線判斷,1 是下線,0 是線上。
    • leader_runid:當 leader_runid 等於“*” 時,代表返回結果是用來做主節點是否 不可達,當 leader_runid 等於具體的 runid,代表目標節點同意 runid 成為領導者。
    • leader_epoch:領導者紀元。

5.3 Sentinel 領導者選舉

當 Sentinel 節點對於主節點已經做了客觀下線,Sentinel 節點之間會做一個領導者選舉的 工作,選出一個 Sentinel 節點作為 領導者進行故障轉移的工作。Redis 使用了 Raft演算法 實現領導者選舉。

  • 1)每個線上的 Sentinel 節點都有資格成為領導者,當它確認主節點主觀下線時候,會向 其他 Sentinel 節點發送 sentinel is-master-down-by-addr 命令,要求將自己設定 為領導者。
  • 2)收到命令的 Sentinel 節點,如果沒有同意過其他 Sentinel 節點的 sentinel is- master-down-by-addr 命令,將同意該請求,否則拒絕。
  • 3)如果該 Sentinel 節點發現自己的票數已經大於等於 max(quorum, num(sentinels/2+ 1),那麼它將成為領導者。
  • 4)如果此過程沒有選舉出領導者,將進入下一次選舉。

5.4 故障轉移

領導者選舉出的 Sentinel 節點負責故障轉移,具體步驟如下:

  • 1)在從節點列表中選出一個節點作為新的主節點,選擇方法如下:
    • a)過濾:“不健康”(主觀下線、斷線)、5 秒內沒有回覆過 Sentinel 節點 ping 響應、與主節點失聯超過 down-after-milliseconds*10 秒。
    • b)選擇 slave-priority(從節點優先順序)最高的從節點列表,如果存在則返回,不存在則繼續。
    • c)選擇複製偏移量最大的從節點(複製的最完整),如果存在則返回,不存在則繼續。
    • d)選擇 runid 最小的從節點。
  • 2)Sentinel 領導者節點會對第一步選出來的從節點執行 slaveof no one 命令讓其成為 主節點。
  • 3)Sentinel 領導者節點會向剩餘的從節點發送命令,讓它們成為新主節點的從節點,複製 規則和 parallel-syncs 引數有關。
  • 4)Sentinel 節點集合會將原來的主節點更新為從節點,並保持著對其關注,當其恢復後命令它去複製新的主節點。

參考