在 redis 2.8 之前, 基本可以认为 redis 不支持高可用部署模式, 因为仅靠 redis 主从同步机制, 当主服务器出现故障时, 并不支持自动将从服务器提升为主服务器, 而是需要运维人员手动介入; 很明显, 单独的 redis 主从模式不具备在生产环境部署的条件;
2013 年 9 发布的 redis 2.8 提供了一种哨兵模式, 终于在实质上解决了 redis 的高可用部署问题;

哨兵系统的构成

哨兵系统内由两部分组成 —— 哨兵节点和数据节点:

  • 哨兵节点: 哨兵节点是特殊的 redis 节点, 不存储数据, 只用于管控数据节点, 一个或多个哨兵节点组成了一个哨兵集群;
  • 数据节点: redis 主节点和从节点都是数据节点, 其存储真正的数据;

哨兵节点的作用:

  • 监控: 哨兵会不断地检查主节点和从节点是否运作正常;
  • 自动故障转移: 当主节点不能正常工作时, 哨兵会开始自动故障转移操作, 它将失效主节点的其中一个从节点提升为新的主节点, 并让其它从节点改为复制新的主节点;
redis 哨兵模式
redis 哨兵模式

节点的发现

启用哨兵模式, 只需要配置其监控的主数据库即可, 哨兵会自动发现所有复制该主节点的从节点:

1
2
3
4
5
6
7
8
# sentinel.conf

# sentinel monitor master-name ip redis-port quorum
# master-name: 要监控的主节点名称
# ip: 主节点 (master) 的地址
# redis-port: 主数据库的端口
# quorum: 触发客观下线的最低通过票数
sentinel monitor mymaster 127.0.0.1 6379 2

1
2
# 启动哨兵模式
> src/redis-sentinel sentinel.conf

哨兵启动后会与要监控的主节点建立两条连接:

哨兵与主节点的连接
哨兵与主节点的连接

其中 _sentinel__:hello 频道是一个特殊的发布/订阅频道, 用于哨兵实例之间的通信;
 
和数据节点建立连接完成后, 哨兵会使用 连接2 发送如下命令:

  • 每 10 秒钟哨兵会向数据节点发送 INFO 命令; 数据节点响应 INFO 命令并返回当前节点的相关信息 (运行 id、从节点信息等) 从而实现新节点的自动发现;
  • 每 2 秒钟哨兵会向主节点的 _sentinel_:hello 频道发布自己的消息, 以与同样监控该节点的其他哨兵分享自己的信息;
    哨兵集群的发现
    哨兵集群的发现
    发布内容为: <哨兵的地址>, <哨兵的端口>, <哨兵的运行ID>, <哨兵的配置版本>,
    <主数据库的名字>, <主数据库的地址>, <主数据库的端口>, <主数据库的配置版本>;
  • 每 1 秒钟哨兵会向主数据、从数据库和其他哨兵节点发送 PING 命令;

故障转移流程

通过如下配置 PING 命令健康检查的超时时间:

1
2
3
4
5
6
# sentinel.conf

# 每隔 1s 发送一次 PING 命令 (因为配置值超过 1s), 当超过 6s 没回复, 认为节点主观下线
sentinel down-after-milliseconds mymaster 6000
# 每隔 600 ms 发送一次 PING 命令, 当超过 600 ms 没回复, 认为节点主观下线
sentinel down-after-milliseconds mymaster 600

具体流程如下:

  1. 每个哨兵节点每隔指定时间会向主节点、从节点及其它哨兵节点发送一次 PING 命令做一次心跳检测; 如果主节点在指定时间内不回复或者是回复一个错误消息, 那么这个哨兵就会认为这个主节点已主观下线 (Subjectively Down, SDOWN);
  2. 如果一个主节点被标记为 SDOWN, 则正在监视这个主节点的所有哨兵节点 要以每秒一次的频率确认该主节点的确进入了 SDOWN 状态;
  3. 当超过指定数量 (sentinel.conf 配置的 quorum) 的哨兵节点认为某主节点主观下线了, 这个节点就被认为已客观下线 (Objectively Down, ODOWN);
  4. 哨兵节点会通过 Raft 算法 共同选举出一个哨兵节点为 leader, 以负责处理主节点的故障转移和通知;
  5. 由 leader 哨兵节点负责真正的故障转移:
    • 过滤掉不健康的 (已下线的), 没有回复哨兵 PING 命令的从节点;
    • 选择配置文件中从节点优先级配置最高的 (redis.conf 中的 replica-priority, 默认值为 100);
    • 若优先级相同, 选择复制偏移量最大的从节点, 也就是复制最完整的从节点;
    • 若复制偏移量依然相同, 则直接选择 runID 最大的从节点;
    • 将选定的从节点提升为新的主节点, 让其它从节点指向新的主节点;
    • 若原主节点恢复也变成从节点, 并指向新的主节点;
    • 通知客户端主节点已经更换;
哨兵自动转移故障
哨兵自动转移故障

基于哨兵自研 redis 集群

redis 哨兵模式虽然解决了 redis 高可用部署的问题, 但是没有解决单点瓶颈横向扩展的问题, 这对于规模扩张十分迅速的互联网行业来说, 也是不可接受的;
redis 3.0 推出原生集群模式是在 2015 年, 而那在之前, 互联网公司想要实现一个 redis 集群就只能自己动手了, 其实我们完全可以在哨兵模式的基础上, 通过旁路数据控制、客户端能力增强, 扩展出可横向数据切分、可动态扩缩容的 redis 集群;
概括来说, 一个 redis 集群的核心能力只有几点:

  1. 多副本、数据分区冗余能力;
  2. 副本故障自动转移能力;
  3. 横向切分, 多数据分片能力;
  4. 可扩缩容、数据迁移能力;

其中, 第 1 点由 redis 原生的主从复制能力解决; 第 2 点由 redis 原生的哨兵模式解决; 要实现一个真正意义上的 redis 集群, 就只需要自主实现第 3、4 两点即可;
我们以去哪儿网 (qunar.com) 的自研 redis 集群为例介绍:

qunar redis 集群架构
qunar redis 集群架构

去哪儿网是国内较早在生产环境大规模部署 redis 实例的互联网公司之一, 由于历史早期 redis 不支持原生集群模式, 去哪儿网只能基于仅具备主从/哨兵能力的 redis 版本自研了一套 redis 集群:

  1. 实现一个旁路的配置中心, 存储 redis 集群内各个实例的信息, 包括: 实例的 ip/port、slot 的 start/end 偏移等;
    redis 集群配置信息
    redis 集群配置信息
  2. 实现 redis 客户端的路由能力: 3.0 之前的 redis 版本不感知什么叫做分片, 没有路由的概念, 无法像 3.0 之后的版本针对不属于当前实例的 key 返回 MOVED 错误, 必须由客户端自己去感知分片实现路由, 需要能够从配置中心读取集群配置, 当查询具体的 key 时, 通过协商一致的 hash 算法计算 key 对应的 slot, 再继续定位到 slot 所在的实例;
  3. 实现配置的及时更新:
    • 增强哨兵集群的配置变更能力, 当主节点故障实施自动切换的同时 (也适用于扩缩容的场景):
      • 更新配置中心, 替换实例的 ip/port;
      • 将最新集群信息写入 zookeeper 对应节点, 方便客户端及时感知集群实例变更, 拉取最新配置;
    • 增强客户端的配置感知能力:
      • 定时从配置中心获取最新配置, 当配置变更, 重新建连;
      • 监听目标集群对应的 zookeeper 节点, 当收到节点变更通知, 及时去配置中心获取最新配置, 重新建连;

扩缩容的实现

动态路由感知不是 qunar 自研 redis 集群最复杂的地方, 扩缩容才是, 因为扩缩容涉及数据的 reshard & migrate; 为了避免影响线上请求, qunar 采用了新建一个扩缩容后的独立目标集群, 然后将路由切过去的解决方案, 该方案的核心就是实现一个主从复制的双向代理中间件:

qunar redis 集群扩缩容
qunar redis 集群扩缩容

  1. 作为源集群的从库, 同步源集群的数据;
  2. 作为目的集群的主库, 实现数据迁移到目的集群;
  3. 根据目的端集群的拓扑信息, 按照客户端分片算法, 将数据分发到目的端集群的各个分片;

当源目两库完成全量同步, 进入增量同步的阶段, 进行数据校验, 若数据一致, 实行配置切换, 即完成了扩缩容操作;

参考资料