Redis 技术内幕——Redis Sentinel

Redis Sentinel(哨兵)是 Redis 官方提供的集群管理工具,是 Redis 高可用的解决方案,本身是一个独立运行的进程,它可以监视多个 Master-Slave 集群,发现 Master 宕机之后,能进行自动切换,将该 Master 下的某个 Slave 晋升为 Master,继续处理请求。

Redis Sentinel 主要功能:

  • 监控:检查主从服务器是否运行正常;
  • 提醒:通过 API 向管理员或者其他应用程序发送故障通知;
  • 自动故障迁移:主从切换。

Redis Sentinel 是一个分布式系统,你可以在一个架构中运行多个 Sentinel 进程,这些进程使用流言协议 (Gossip Protocols) 接收 Master 是否下线的信息,并使用投票协议来决定是否执行自动故障迁移,以及选择哪个 Slave 作为新的 Master。这个跟 Zookeeper 是比较类似的。

流言协议:

  • 每个节点都随机地与对方通信,最终所有节点的状态达成一致;
  • 种子节点定期随机向其他节点发送节点列表以及需要传播的消息;
  • 不保证信息一定会传递给所有节点,但是最终会趋于一致。

1.架构说明

Redis Sentinel 架构:

Redis 技术内幕——Redis Sentinel_第1张图片

Redis Sentinel 故障转移:

Redis 技术内幕——Redis Sentinel_第2张图片

1、如果 master 宕机了,连接出现中断,多个 Sentinel 发现并确认 master 有问题;
2、选举出一个 Sentinel 作为领导;
3、选举一个 slave 作为新的 master;
4、通知其余 slave 成为新的 master 的 slave;
5、通知客户端主从变化;
6、Sentinel 等待老的 master 复活成为新 master 的 slave。

Redis 技术内幕——Redis Sentinel_第3张图片

整个过程其实就是由我们手动处理故障变成了 Sentinel 进行故障发现、故障自动转移、通知客户端的过程。

Redis Sentinel 还可以监控多个 master-slave 集群,每个 master-slave 使用 master-name 作为标识,有效节省资源。

#安装与配置

最终要达到的效果如下:

Redis 技术内幕——Redis Sentinel_第4张图片

Sentinel 的默认端口是 26379。生产环境中 Sentinel 节点个数应该为大于等于 3 且最好为奇数。

安装与配置大致过程如下:

1、配置开启主从节点;
2、配置开启 Sentinel 监控主节点(Sentinel 是特殊的 Redis,Sentinel 本身是不存储数据的,而且支持的命令非常有限,主要作用就是监控、完成故障转移、通知);
3、实际应该多机器部署 Sentinel,保证 Sentinel 高可用;
4、详细配置节点

详细配置过程:

Redis 主节点 redis/config/redis-7000.conf 配置(redis.conf 模板文件在 redis/redis.conf):

# 关闭保护模式
protected-mode no
# 配置启动端口
port 7000
# 配置后台启动
daemonize yes
# 修改pidfile指向路径 redis-${port}.pid
pidfile /var/run/redis-7000.pid
# 日志记录方式 redis-${port}.log
logfile "redis-7000.log"
# 配置dump数据存放目录
dir "/opt/soft/redis/data/"
# 配置dump数据文件名 redis-${port}.rdb
dbfilename dump-7000.rdb

启动命令:

redis-server redis-7000.conf

Redis 从节点 redis/config/redis-7001.conf 配置(redis-7002.conf 只是端口号不同):

# 关闭保护模式
protected-mode no
# 配置启动端口
port 7001
# 配置后台启动
daemonize yes
# 修改pidfile指向路径 redis-${port}.pid
pidfile /var/run/redis-7001.pid
# 日志记录方式 redis-${port}.log
logfile "redis-7001.log"
# 配置dump数据存放目录
dir "/opt/soft/redis/data/"
# 配置dump数据文件名 redis-${port}.rdb
dbfilename dump-7001.rdb
# 配置master的ip地址、端口,在Redis启动时会自动从master进行数据同步
slaveof 127.0.0.1 7000

启动命令:

redis-server redis-7001.conf
redis-server redis-7002.conf

Sentinel 节点 redis/config/redis-sentinel-26379.conf 配置(这里只给出其中一个配置,另外两个只是端口号不同,sentinel.conf 模板文件在 redis/sentinel.conf):

# 配置启动端口
port 26379
# 配置后台启动
daemonize yes
# 配置工作目录
dir "/opt/soft/redis/data/"
# 日志记录方式 redis-sentinel-${port}.log
logfile "redis-sentinel-26379.log"
# sentinel监听master的名字、ip、端口、几个sentinel认为master有问题就发生故障转移(最好配置sentinel节点的二分之一加一)
sentinel monitor mymaster 127.0.0.1 7000 2
# 每一个sentinel节点都会向master节点、slave节点和其他sentinel节点1秒钟ping一次。在down-after-milliseconds毫秒内没有进行回复,则判定该节点失败
sentinel down-after-milliseconds mymaster 30000
# slave复制时可以并行的个数,建议1,减轻master的压力
sentinel parallel-syncs mymaster 1
# 故障转移时间
sentinel failover-timeout mymaster 180000

启动命令:

redis-sentinel redis-sentinel-26379.conf
redis-sentinel redis-sentinel-26380.conf
redis-sentinel redis-sentinel-26381.conf

Sentinel 会自动发现 Redis slave,并写入到 redis-sentinel-${port}.conf,Sentinel 彼此之间也能自动感知到。

2.客户端连接

客户端初始化时连接的是 Sentinel 节点集合,不再是具体的 Redis 节点,但 Sentinel 只是配置中心不是代理。Sentinel 中的数据节点与普通数据节点没有区别。

1.请求响应流程

  1. 客户端遍历 Sentinel 节点集合,获取一个可用的 Sentinel 节点;
  2. 客户端执行一个 Sentinel 的 API:get-master-addr-by-name < masterName> 获取 master 节点真正的地址和端口;
  3. 客户端执行 role 或者 role replication 进行一次验证,验证是否是真的 master 节点;
  4. 连接 master 进行操作;
  5. 客户端会订阅 Sentinel 的某一个频道,这个频道里会有谁是 master 的一个变化,假如有变化,则发送消息给客户端,客户端再去新的 master 进行连接。

2.Java客户端连接

Jedis 方式:

JedisSentinelPool jedisSentinelPool = new JedisSentinelPool(masterName, sentinelSet, poolConfig, timeout);
Jedis jedis = null;
try {
	jedis = jedisSentinelPool.getResource();
	// jedis command
} catch (Exception e) {
	logger.error("error", e);
} finally {
	if(jedis != null) {
		jedis.close();
	}
}

JedisSentinelPool 的实现原理:

先看下 JedisSentinelPool 的构造函数:

public JedisSentinelPool(String masterName, Set<String> sentinels,
        final GenericObjectPoolConfig poolConfig, final int timeout) {
	this(masterName, sentinels, poolConfig, timeout, null, Protocol.DEFAULT_DATABASE);
}

public JedisSentinelPool(String masterName, Set<String> sentinels,
        final GenericObjectPoolConfig poolConfig, final int connectionTimeout, final int soTimeout,
        final String password, final int database, final String clientName) {
    this.poolConfig = poolConfig; //连接池配置
    this.connectionTimeout = connectionTimeout; // 连接超时时间
    this.soTimeout = soTimeout;   // 读写超时时间
    this.password = password;     // 密码
    this.database = database;     // 数据库, Redis一共有16个数据库, 默认使用0
    this.clientName = clientName; // 客户端名
    HostAndPort master = initSentinels(sentinels, masterName); // 初始化sentinel
    initPool(master); // 初始化连接池, 连接master
}

看下 initSentinels 方法:

private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {
    HostAndPort master = null;
    boolean sentinelAvailable = false;

    // 遍历所有sentinel节点, 获取master节点
    for (String sentinel : sentinels) {
        final HostAndPort hap = HostAndPort.parseString(sentinel);
        Jedis jedis = null;
        try {
            jedis = new Jedis(hap);

            // 执行sentinel的API:get-master-addr-by-name 命令获取master节点真正的地址和端口
            List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
            sentinelAvailable = true;

            if (masterAddr == null || masterAddr.size() != 2) {
                // 如果不可用则continue
                continue;
            }
            // 如果可用则break
            master = toHostAndPort(masterAddr);
            break;
        } catch (JedisException e) {
            log.warn("Cannot get master address from sentinel running @ {}. Reason: {}. Trying next one.", hap, e.toString());
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
    }
    if (master == null) {
        if (sentinelAvailable) {
            throw new JedisException("Can connect to sentinel, but " + masterName + " seems to be not monitored...");
        } else {
            throw new JedisConnectionException("All sentinels down, cannot determine where is " + masterName + " master is running...");
        }
    }

    // 遍历所有sentinel节点, 订阅消息(主观下线、客观下线、领导者选举、主从切换)
    for (String sentinel : sentinels) {
        final HostAndPort hap = HostAndPort.parseString(sentinel);
        JedisSentinelPool.MasterListener masterListener = new JedisSentinelPool.MasterListener(masterName, hap.getHost(), hap.getPort());
        // whether MasterListener threads are alive or not, process can be stopped
        masterListener.setDaemon(true);
        masterListeners.add(masterListener);
        masterListener.start();
    }
    return master;
}

MasterListener 是一个线程:

protected class MasterListener extends Thread {
    ...
    @Override
    public void run() {
        while (running.get()) {
            j = new Jedis(host, port);
            try {
                ...
                j.subscribe(new JedisPubSub() {
                    @Override
                    public void onMessage(String channel, String message) { // 订阅+switch-master(主从节点切换)频道
                        String[] switchMasterMsg = message.split(" ");
                        if (switchMasterMsg.length > 3) {

                            if (masterName.equals(switchMasterMsg[0])) {
                                // 重新初始化连接池
                                initPool(toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4])));
                            } else {
                                log.debug("Ignoring message on +switch-master for master name {}, our master name is {}", switchMasterMsg[0], masterName);
                            }
                        } else {
                            log.error("Invalid message received on Sentinel {}:{} on channel +switch-master: {}", host, port, message);
                        }
                    }
                }, "+switch-master");
            } catch (JedisException e) {
                ...
            } finally {
                j.close();
            }
        }
    }
}

最后看下 initPool 方法:

private void initPool(HostAndPort master) {
    synchronized(initPoolLock){
        if (!master.equals(currentHostMaster)) {
            currentHostMaster = master;
            if (factory == null) {
                factory = new JedisFactory(master.getHost(), master.getPort(), connectionTimeout,
                        soTimeout, password, database, clientName);
                initPool(poolConfig, factory);
            } else {
                factory.setHostAndPort(currentHostMaster);
                internalPool.clear();
            }
            log.info("Created JedisPool to master at " + master);
        }
    }
}

这样就完成了整个 JedisSentinelPool 的实现。

2.故障转移实现原理

Redis Sentinel 通过三个定时任务实现了 Sentinel 节点对于主节点、从节点、其余 Sentinel 节点的监控。Sentinel 在对节点做失败判定时分为主观下线和客观下线。Sentinel 实现读写分离高可用依赖 Sentinel 节点的消息通知,获取 Redis 数据节点的状态变化。

1、三个定时任务

为了保证 Redis Sentinel 可以对 Redis 节点做失败判定以及做故障转移,在 Redis Sentinel 内部是有三个定时任务作为基础的:

  • 每 10 秒每个 sentinel 对 master 和 slave 执行 info 操作;
    • 发现 slave 节点;
    • 确认主从关系;
  • 每 2 秒每个 sentinel 通过 master 节点的 channel(频道)交换信息(pub/sub);
    • 通过__sentinel__:hello频道交互;
    • 交互对节点的 “看法” 和自身信息;
  • 每 1 秒每个 sentinel 对其他 sentinel 和 redis 执行 ping;

2、三个消息

  • +switch-master:切换主节点(从节点晋升为主节点);
  • +convert-to-slave:切换从节点(原主节点降为从节点);
  • +sdown:主观下线。

3、主观下线和客观下线

主观下线:单个 sentinel 节点对 Redis 节点做出的下线判断;

客观下线:超过 quorum(sentinel monitor < masterName> < ip> < port> < quorum> 配置)个 sentinel 节点对 Redis 节点做出的下线判断;

只有当 master 被认定为客观下线时,才会发生故障迁移。

4、领导者选举

完成故障转移的过程只需要一个 sentinel 来完成。

选举:通过 sentinel is-master-down-by-addr 命令都希望成为领导者。

sentinel 领导者选举使用的是 raft 算法,大致过程:

  1. 每一个做主观下线的 sentinel 节点向其他 sentinel 节点发送命令,要求将它设置为领导者;
  2. 收到命令的 sentinel 节点,如果在发生故障周期内没有同意其他 sentinel 节点发送的命令,那么将同意该请求,否则拒绝;
  3. 如果该 sentinel 节点发现自己的票数已经超过 sentinel 集合半数且超过 quorum(sentinel monitor 配置),那么它将称为领导者;
  4. 如果此过程有多个 sentinel 节点成为了领导者,那么将等待一段时间重新进行选举。

5、故障转移(sentinel 领导者节点完成)

  1. 从 slave 节点中选出一个 “合适的” 节点作为新的 master 节点;
  2. 对步骤 1 的 slave 节点执行 slaveof no one 命令让其成为 master 节点;
  3. 向剩余的 slave 节点发送命令,让它们成为新 master 节点的 slave 节点,复制规则和 parallel-syncs 参数有关;
  4. 对原来的 master 节点配置为 slave,并保持对其 “关注”,当其恢复后命令它去复制新的 master 节点。

选择 “合适的” slave 节点:

  1. 选择 slave-priority(slave 节点优先级)最高的 slave 节点,如果存在则返回,不存在则继续;
  2. 选择复制偏移量最大的 slave 节点(复制的最完整),如果存在则返回,不存在则继续;
  3. 选择 runid 最小的 slave 节点(启动最早的节点)。

3.常见开发运维问题

1.节点运维

节点运维场景问题:

  • 机器下线,例如过保等情况。
  • 机器性能不足。例如 CPU、内存、硬盘、网络等。
  • 节点自身故障,例如服务不稳定。

主节点下线:主节点下线需要手动故障转移,通过 sentinel failover < masterName> 命令去给任意一个 sentinel 去执行,完成故障转移的过程。这个故障转移过程中,忽略了主观下线、客观下线、领导者选举,因为这个 sentinel 已经是领导者了,而且确定了要下线的 master,所以没有这些过程。

从节点/sentinel 节点下线:临时下线还是永久下线,例如是否做一些清理工作。但是要考虑读写分离的情况。

主节点上线:sentinel failover 进行替换。

从节点上线:slaveof 即可,sentinel 节点可以感知。

sentinel 节点上线:参考其他 sentinel 节点启动即可。

2.高可用读写分离

从节点的作用:

  • 副本,高可用的基础;
  • 扩展读能力;

你可能感兴趣的:(Redis)