哨兵模式介绍
哨兵(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)
}