哨兵模式介紹
哨兵(Sentinel)模式是Redis高可用的其中一種實現模式,其包含一個主節點(master)、多個從節點(replication,也稱slave),以及多個哨兵節點(sentinel)。每個sentinel節點會對數據節點和其餘sentinel節點進行監控,當它發現節點不可達時,會對節點做下線標識。如果被標識的是“主節點”,它還會和其他的sentinel節點進行“協商”,當大多數sentinel節點都認爲主節點不可達時,它們會選舉一個sentinel節點來完成自動故障轉移的工作,同時會將這個變化實時通知給Redis應用方。整個過程是自動的,不需要人工干預,解決了Redis的高可用問題。
哨兵模式服務端搭建(單機版)
在此採用單機版進行介紹,分佈式版需要將IP地址改爲對應的服務器IP即可。
節點說明 | IP地址 | redis端口號 | sentinel端口號 |
---|---|---|---|
master | 127.0.0.1 | 6379 | 26379 |
replica | 127.0.0.1 | 6380 | 26380 |
replica | 127.0.0.1 | 6381 | 26381 |
1. redis配置及啓動
首先配置三個redis數據節點爲主從模式,複製redis解壓文件中的redis.conf三份,並重命名爲redis_6379.conf、redis_6380.conf、redis_6381.conf。
修改主節點配置文件 redis_6379.conf:
port 6379
daemonize yes
pidfile "/var/run/redis_6379.pid"
logfile "6379.log"
dir "/usr/bin/sentinel/redis6379/"
修改從節點1的配置文件redis_6380.conf:
port 6380
daemonize yes
pidfile "/var/run/redis_6380.pid"
logfile "6380.log"
dir "/usr/bin/sentinel/redis6380/"
replicaof 127.0.0.1 6379
類似的,修改從節點2的配置文件redis_6381.conf:
port 6381
daemonize yes
pidfile "/var/run/redis_6381.pid"
logfile "6381.log"
dir "/usr/bin/sentinel/redis6381/"
replicaof 127.0.0.1 6379
接下來在命令行啓動主節點和兩個從節點:
redis-server redis_6379.conf
redis-server redis_6380.conf
redis-server redis_6381.conf
連接端口號爲6379的redis服務器,執行info replication可以查看主從節點信息,端口號6379的爲master,端口號爲6380、6381的爲slave。
2. sentinel配置及啓動
接下來配置sentinel,複製redis解壓文件中的sentinel.conf三份,並重命名爲redis_sentinel_26379.conf、redis_sentinel_26380.conf、redis_sentinel_26381.conf。
修改配置文件redis_sentinel_26379.conf:
port 26379
daemonize yes
pidfile "/var/run/redis_sentinel_26379.pid"
logfile "26379.log"
dir "/usr/bin/sentinel/redis6379/"
sentinel monitor mymaster 127.0.0.1 6379 2
# mymaster是主節點的別名,監控的主節點是127.0.0.1:6379,
# 2代表判斷主節點失敗至少需要2個sentinel節點同意
類似的,修改配置文件redis_sentinel_26380.conf:
port 26380
daemonize yes
pidfile "/var/run/redis_sentinel_26380.pid"
logfile "26380.log"
dir "/usr/bin/sentinel/redis6380/"
sentinel monitor mymaster 127.0.0.1 6379 2
類似的,修改配置文件redis_sentinel_26381.conf:
port 26381
daemonize yes
pidfile "/var/run/redis_sentinel_26381.pid"
logfile "26381.log"
dir "/usr/bin/sentinel/redis6381/"
sentinel monitor mymaster 127.0.0.1 6379 2
在命令行啓動哨兵節點:
redis-sentinel redis_sentinel_26379.conf
redis-sentinel redis_sentinel_26380.conf
redis-sentinel redis_sentinel_26381.conf
連接端口號爲26379的哨兵節點,執行info sentinel可以查看哨兵信息。
3. 故障轉移示例
執行 ps aux | grep redis 可以看到有單機版本有3個redis數據節點以及3個sentinel節點。
將此時的端口號爲6379的進程殺死可以查看故障轉移,執行 info sentinel 看到主節點已切換至端口號爲6380的節點。此時會將主從節點信息的改動自動寫入redis的配置文件以及sentinel配置文件。
至此介紹完了服務端的配置、啓動、故障轉移演示,實現原理後續繼續發掘。
go客戶端自動切換主節點
哨兵模式的客戶端如果直接連接固定redis的IP地址和端口號,就不會自動切換主節點,因此,需要連接sentinel節點,由sentinel節點獲取主節點的信息,實現服務端故障轉移後,客戶端能夠自動連接到新的主節點上。
以go語言的redis client使用的redigo包爲例,連接哨兵模式服務需要引入sentinel包,
sentinel包地址:https://github.com/FZambia/sentinel
其接口文檔地址:https://godoc.org/github.com/FZambia/sentinel#Sentinel.Discover
客戶端自動切換主節點的Demo如下,newSentinelPool函數實現了自動連接主節點,需要注意的是當主節點宕機時有主從節點切換的過程,此時會有幾秒的連接失敗,但服務端切換完成後,客戶端可以自動連接。
package main
import (
"errors"
"fmt"
"time"
"github.com/gomodule/redigo/redis"
"github.com/sentinel-master"
)
// Sentinel provides a way to add high availability (HA) to Redis Pool using
// preconfigured addresses of Sentinel servers and name of master which Sentinels
// monitor. It works with Redis >= 2.8.12 (mostly because of ROLE command that
// was introduced in that version, it's possible though to support old versions
// using INFO command).
//
// Example of the simplest usage to contact master "mymaster":
func newSentinelPool() *redis.Pool {
sntnl := &sentinel.Sentinel{
Addrs: []string{":26379", ":26380", ":26381"},
MasterName: "mymaster",
Dial: func(addr string) (redis.Conn, error) {
timeout := 500 * time.Millisecond
c, err := redis.DialTimeout("tcp", addr, timeout, timeout, timeout)
if err != nil {
fmt.Println("newSentinelPool sntnl.Dial() error [", err, "]")
return nil, err
}
return c, nil
},
}
return &redis.Pool{
MaxIdle: 3,
MaxActive: 64,
Wait: true,
IdleTimeout: 240 * time.Second,
Dial: func() (redis.Conn, error) {
masterAddr, err := sntnl.MasterAddr()
if err != nil {
fmt.Println("newSentinelPool Dial() masterAddr error [", err, "]")
return nil, err
}
fmt.Println("MasterAddr [", masterAddr, "]")
c, err := redis.Dial("tcp", masterAddr)
if err != nil {
fmt.Println("connect master addr error [", err, "]")
return nil, err
}
return c, nil
},
TestOnBorrow: func(c redis.Conn, t time.Time) error {
if !sentinel.TestRole(c, "master") {
return errors.New("Role check failed")
} else {
return nil
}
},
}
}
func main() {
pool := newSentinelPool()
conn := pool.Get()
defer conn.Close()
err := pool.TestOnBorrow(conn, time.Now())
if err != nil {
return
}
_,err = conn.Do("SET", "k", "value")
if err != nil {
fmt.Println("set error")
return
}
v, err := conn.Do("GET", "k")
if err != nil {
fmt.Println("get error")
return
}
fmt.Println("v:",v)
}