前边我们已经完成了redis的主从搭建,这里考虑到容灾的自动切换,如下图:
我们利用哨兵(sentinel)来对各节点进行监控
【主从配置】
首先看一下redis.conf 配置文件中的各个参数,详解如下
# redis进程是否以守护进程的方式运行,yes为是,no为否(不以守护进程的方式运行会占用一个终端)。
daemonize no
# 指定redis进程的PID文件存放位置
pidfile /var/run/redis.pid
# redis进程的端口号
port 6379
#是否开启保护模式,默认开启。要是配置里没有指定bind和密码。开启该参数后,redis只会本地进行访问,拒绝外部访问。要是开启了密码和bind,可以开启。否则最好关闭设置为no。
protected-mode yes
# 绑定的主机地址
bind 127.0.0.1
# 客户端闲置多长时间后关闭连接,默认此参数为0即关闭此功能
timeout 300
# redis日志级别,可用的级别有debug.verbose.notice.warning
loglevel verbose
# log文件输出位置,如果进程以守护进程的方式运行,此处又将输出文件设置为stdout的话,就会将日志信息输出到/dev/null里面去了
logfile stdout
# 设置数据库的数量,默认为0可以使用select <dbid>命令在连接上指定数据库id
databases 16
# 指定在多少时间内刷新次数达到多少的时候会将数据同步到数据文件
save <seconds> <changes>
# 指定存储至本地数据库时是否压缩文件,默认为yes即启用存储
rdbcompression yes
# 指定本地数据库文件名
dbfilename dump.db
# 指定本地数据问就按存放位置
dir ./
# 指定当本机为slave服务时,设置master服务的IP地址及端口,在redis启动的时候他会自动跟master进行数据同步
replicaof <masterip> <masterport>
# 当master设置了密码保护时,slave服务连接master的密码
masterauth <master-password>
# 设置redis连接密码,如果配置了连接密码,客户端在连接redis是需要通过AUTH<password>命令提供密码,默认关闭
requirepass footbared
# 设置同一时间最大客户连接数,默认无限制。redis可以同时连接的客户端数为redis程序可以打开的最大文件描述符,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回 max number of clients reached 错误信息
maxclients 128
# 指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key。当此方法处理后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区
maxmemory<bytes>
# 指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no。
appendonly no
# 指定跟新日志文件名默认为appendonly.aof
appendfilename appendonly.aof
# 指定更新日志的条件,有三个可选参数 - no:表示等操作系统进行数据缓存同步到磁盘(快),always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全), everysec:表示每秒同步一次(折衷,默认值);
appendfsync everysec
1. 主机配置
bind:0.0.0.0
port:6379
protected-mode:no
daemonize:yes
logfile:./redis.log
requirepass:pwdtest@2019
masterauth:pwdtest@2019
2. 从机配置
bind:0.0.0.0
port:6379
protected-mode:no
daemonize:yes
logfile:./redis.log
requirepass:pwdtest@2019
masterauth:pwdtest@2019
replicaof 主机ip 6379
【哨兵模式搭建】
哨兵模式为redis的高可用解决方案,有一个或多个Sentinel实例组成,它可以监视多个机器,当主服务挂掉后通过选举,自动将下线服务器升级为新的主服务器,他的功能主要有:
- 监控(Monitoring): Sentinel会不断检查主服务器和从服务器是否正常运行。
- 通知(Notification): 当被监控的某个redis服务器出现问题时,Sentinel可以通过API向管理员或者其他应用发送通知。
- 故障转移:当主服务器挂掉,Sentinel会自动惊醒故障迁移,也就是主从切换。
- 统一的配置管理:连接者询问sentinel取得主从的地址。
Sentinel使用的算法核心是Raft算法,主要用途就是用于分布式系统,系统容错,以及Leader选举,每个Sentinel都需要定期的执行一下任务:
- 每个Sentinel会自动发现其他Sentinel和从服务器,它以每秒一次的频率向它所知的主服务器、从服务器以及其他Sentinel实例发送一个PING命令
- 如果一个实例(instance)距离最后一次有效回复PING命令的事件超过down-after-millisrconds选项所指定的值,那么这个实例会被Sentinel标记为主观下线。有效回复可以是:+PONG、-LOANDING或者-MASTERDOWN。
- 如果一个主服务器呗标记为主观下线,那么正在监控这个主服务器的所有Sentinel要以每秒一个的频率确认主服务器的确进入了主观下线状态。
- 如果一个主服务器被标记为主观下线,并且有足够数量的Sentinel(至少要达到配置文件指定的数量)在指定的时间范围内同意这一判断,呢么这个主服务器呗标记为客观下线。
- 在一般情况下,每个Sentinel会以每10秒一次的频率向它已知的所有主服务器和从服务器发送INFO命令。当一个主服务器被Sentinel标记为客观下线时,Sentinel向下线主服务器的所有从服务器发送INFO命令的频率会从10秒一次改为1s一次。
- 当没有足够数量的Sentinel同意主服务器已经下线,主服务器的客观下线状态会被移除。当主服务器重新向Sentinel的PING命令返回有效回复时,主服务器的主观下线状态会被移除。
修改 sentinel.conf文件
//端口默认为26379。
port:26379
//关闭保护模式,可以外部访问。
protected-mode:no
//设置为后台启动。
daemonize:yes
//日志文件。
logfile:./sentinel.log
//指定主机IP地址和端口,并且指定当有2台哨兵认为主机挂了,则对主机进行容灾切换。
sentinel monitor mymaster 主机ip 6379 2
//当在Redis实例中开启了requirepass,这里就需要提供密码。
sentinel auth-pass mymaster pwdtest@2019
//这里设置了主机多少秒无响应,则认为挂了。
sentinel down-after-milliseconds mymaster 3000
//主备切换时,最多有多少个slave同时对新的master进行同步,这里设置为默认的1。
snetinel parallel-syncs mymaster 1
//故障转移的超时时间,这里设置为三分钟。
sentinel failover-timeout mymaster 180000
启动哨兵 redis-sentinel sentinel.conf,三个哨兵都启动后,可使用如下命令查看哨兵信息。
可以看到,哨兵已经监听到当前的主机IP端口和运行状态,并且2台从机,3个哨兵。
【注意事项】
redis集群ip进行修改,如果redis集群位置发生变动需要修改ip,记得删除sentinel.conf文件最后位置多出的临时指令。及前边指定的主机ip地址。
【golang程序实现】
package models
import (
"fmt"
"strings"
"time"
"github.com/astaxie/beego/logs"
"github.com/astaxie/beego"
"github.com/FZambia/sentinel"
"github.com/gomodule/redigo/redis"
)
var RedisConnPool *redis.Pool
func init() {
redisAddr := beego.AppConfig.String("redisAddr")
redisType := beego.AppConfig.String("redisType")
MaxIdle, err := beego.AppConfig.Int("maxIdle")
masterName := beego.AppConfig.String("masterName")
password := beego.AppConfig.String("redisPassword")
if err != nil {
logs.Error("获取MaxIdle失败", err)
return
}
if redisType == "sentinel" {
redisAddrs := strings.Split(redisAddr, ",")
sntnl := &sentinel.Sentinel{
Addrs: redisAddrs,
MasterName: masterName,
Dial: func(addr string) (redis.Conn, error) {
timeout := 500 * time.Millisecond
c, err := redis.DialTimeout("tcp", addr, timeout, timeout, timeout)
if err != nil {
return nil, err
}
return c, nil
},
}
RedisConnPool = &redis.Pool{
MaxIdle: MaxIdle,
IdleTimeout: 240 * time.Second,
Dial: func() (redis.Conn, error) {
masterAddr, err := sntnl.MasterAddr()
if err != nil {
return nil, err
}
c, err := redis.Dial("tcp", masterAddr)
if err != nil {
return nil, err
}
//此处1234对应redis密码
if _, err := c.Do("AUTH", password); err != nil {
c.Close()
return nil, err
}
return c, nil
},
TestOnBorrow: CheckRedisRole,
}
} else {
RedisConnPool = &redis.Pool{
MaxIdle: MaxIdle,
IdleTimeout: 240 * time.Second,
// Dial or DialContext must be set. When both are set, DialContext takes precedence over Dial.
Dial: func() (redis.Conn, error) {
c, err := redis.Dial("tcp", redisAddr)
if err != nil {
c.Close()
return nil, err
}
//此处1234对应redis密码
if _, err := c.Do("AUTH", password); err != nil {
c.Close()
return nil, err
}
return c, nil
},
}
}
}
func CheckRedisRole(c redis.Conn, t time.Time) error {
if !sentinel.TestRole(c, "master") {
return fmt.Errorf("Role check failed")
} else {
return nil
}
}
使用方法:
conn := models.RedisConnPool.Get()
defer conn.Close()
host_ip, err := redis.String(conn.Do("GET", maps[j]["host_ip"]))
参考文献: