文章簡介
本文將通過理論+實踐的方式從頭到尾總結Redis中的哨兵機制。文章內容從主從複製的弊端、如何解決弊端、什麼是哨兵、哨兵監控的圖形結構、哨兵監控的原理、如何配置哨兵、哨兵與主從複製的關係等方面來演示。
文中涉及到的工具、書籍、設計圖,可以通過回覆公衆號"Redis哨兵"獲取,注意大小寫。
主從複製弊端
上面的圖形結構,大致的可以理解爲Redis的主從複製拓撲圖。
-
其中1個主節點負責應用系統的寫入數據,另外的4個從節點負責應用系統的讀數據。
-
同時4個從節點向其中的1個一個主節點發起復制請求操作。
在Redis服務運行正常的情況下,該拓撲結結構不會出現什麼問題。試想一下這樣的一個場景。如果主節點服務發生了異常,不能正常處理服務(如寫入數據、主從複製操作)。這時候,Redis服務能正常響應應用系統的讀操作,但是沒法進行寫操作。 出現該情況就會嚴重影響到系統的業務數據。那該如何解決呢?
可以大致想到下面的幾種情況來解決。
-
當主節點發生異常情況時,手動的從部分從節點中選擇一個節點作爲主節點。然後改變其他從節點的主從複製關係。
-
我們也可以寫一套自動處理該情況的服務,避免依賴於人爲的操作。
上面的方案在一定程度上是能幫助我們解決問題。但是過多的人爲干預。例如第1點,我們需要考慮人工處理的實時性和正確性。第2點,自動化處理是能夠很好的解決第1點中的問題,但是自動處理存在如何選擇新主節點的問題,因此這也是一個不好的地方。
通過上面大致的分析,我們不難得出Redis的哨兵機制就是針對種種問題出現的。
什麼是哨兵
可以把Redis的哨兵理解爲一種Redis分佈式架構。該架構中主要存在兩種角色,一種是哨兵,另外一種是數據節點(主從複製節點)。
哨兵主要負責的任務是:
-
每一個哨兵都會監控數據節點以及其他的哨兵節點。
-
當其中的一個哨兵監控到節點不可達是,會給對應的節點做下線標識。如果下線的節點爲主節點。這時候會通知其他的哨兵節點。
-
哨兵節點通過“協商”推舉出從節點中的某一個節點爲主節點。
-
接着將其他的從節點斷開與舊主節點的複製關係,將推舉出來的新主節點作爲從節點的主節點。
-
將切換的結果通知給應用系統。
配置哨兵
在演示環境中,配置了三臺數據節點(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 |
-
(數據節點)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
-
(數據節點)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
-
哨兵節點配置。
# 端口號
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節點。檢測是否能夠正常進行主從複製。
我們在主節點任意寫入一些數據,然後在從節點進行查詢數據。爲了方便,後面將master稱作1號終端,兩個slave分配叫做2號和3號終端。
-
我們在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>
-
接着在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節點的服務。
我們實現查看一下哨兵節點的一個狀態信息。
-
查看哨兵端口爲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
-
查看哨兵端口爲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
-
查看哨兵端口爲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號端口自動斷開了連接。
接着我們通過哨兵機制查看一下數據節點狀態信息。
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端口了,其他的信息沒有發生改變,說明哨兵已經完成切換工作。
接下來我們在新的主節點執行操作命令,查看在從節點是否能夠完成主從複製。
-
在3號端口(新的master)執行一個del命令。
127.0.0.1:8004> del age
(integer) 1
127.0.0.1:8004> keys *
1) "name"
2) "socre"
-
在2號端口執行讀命令。
127.0.0.1:8003> keys *
1) "socre"
2) "name"
此時可以發現我們的主從複製也是正常的。
-
啓動舊的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節點的數據。
配置文件對比
在我們啓動了哨兵模式之後,我們的哨兵配置文件和數據節點配置文件的內容都會自動的生成一個特定的內容。
-
數據節點(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
-
哨兵節點
變化前
# 端口號
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);
}
}
通過訪問該代碼,得到如下結果:
改代碼在實際的生產中,肯定使用時不對的,這裏只是爲了演示代碼如何操作哨兵。
其中的操作邏輯大致如下圖:
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"
}
推薦閱讀
相關推薦
本文分享自微信公衆號 - 卡二條的技術圈(qq1005349393)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。