多机 Redis 的实现
十二月的第一周,学习多机 Redis 的实现。
主要学习三个方面:复制、哨兵、集群,基本还是以《Redis 设计与实现》为来源学习,图片也来源于该书。
多个 Redis 协同运行,最基础的内容是复制。
复制是指主从复制:一个主服务器,一个从服务器,从服务器的数据是主服务器复制过来的。使用 Redis 的 SLAVEOF 命令,就可以使一个服务器变成另一个服务器的从属服务器,不断从主服务器复制数据,例如:
1 | 127.0.0.1:12345> SLAVEOF 127.0.0.1 6379 |
Redis 的复制有两部分:刚开始复制的全量复制(sync),以及在此之后不断的增量复制(command propagate)。
当从服务器发送 SLAVEOF 命令给主服务器后,首先会复制主服务器的全部数据,然后主服务每修改一条数据,都会将修改命令传播给从服务器,使从服务器不断地跟主服务器保持一致。
刚开始的全量复制,由从服务器发送 SYNC 命令发起,主服务器收到命令之后执行 BGSAVE 命令,在后台生成 RDB 文件,并发送给从服务器。这种方式有个问题,如果把 RDB 文件传送给从服务器中途断了,需要重来一遍,为此又有了 PSYNC 命令。
感觉这部分没什么意思,抄个复制的全流程,过了:
- 从服务器设置主服务器的地址和端口
- 建立 socket 连接
- 从服务器 PING 主服务器
- 身份验证(主从要不然都验证,要不然都不验证)
- 从服务器发送端口信息,让主服务器可以发送信息
- 全量复制
- 增量复制(命令传播)
哨兵(Sentinel)是 Redis 的高可用解决方案之一,在 Redis 复制的基础上实现了自动化的故障恢复,可以参考博文《深入剖析Redis系列(二) - Redis哨兵模式与高可用集群》。
Redis 哨兵的最小配置是一主一从,执行期间会不断检查主服务器和从服务器是否正常运行,如果主服务器挂了,能够自动将一个从服务器升级为新的主服务器,并让其他从服务器指向它。
哨兵实例
Redis 的哨兵系统中可以有一个或多个哨兵实例,每一个哨兵本质上是一个特殊的 Redis 服务器。
启动一个哨兵,最开始会创建一个 Redis 服务器,然后将代码替换成哨兵的专用代码,并根据配置文件初始化哨兵。
哨兵的配置文件,将设置哨兵监控的主服务器(可多个),哨兵初始化完成之后,将与主服务器建立连接,开始监控。
哨兵监控
在哨兵的配置文件中,只设置了主服务器的地址信息,没有设置从服务器的,例如(在 sentinel.conf
文件中配置):
1 | sentinel monitor master1 127.0.0.1 6379 2 |
哨兵通过配置,首先监控主服务器,并获取到每个主服务器的从服务器,然后监控这些从服务器。
哨兵跟每个服务器之间都将建立两个连接:命令连接、订阅连接。
- 命令连接是哨兵专门用来向主服务器发送命令、接收命令回复的连接,例如哨兵向服务器发送
INFO
、PING
命令。 - 订阅连接是哨兵专门用于订阅主服务器的
__sentinel__:hello
频道的连接,例如当一个哨兵向服务器的__sentinel__:hello
频道发送了一条消息,所有订阅了该频道的哨兵,都将收到这条消息。
正常情况下,哨兵与服务器建立两种连接之后,会不停向服务器发送三种命令:
INFO
命令,十秒一次,用于获知服务器的信息PING
命令,一秒一次,用于心跳检测服务器是否存活PUBLISH __sentinel_:hello ...
命令,两秒一次,用于告知服务器和其他哨兵自己的存在(哨兵之间只建立命令连接,不建立订阅连接,哨兵之间通过__sentinel__:hello
频道互相知道存在)
监控服务器故障恢复
哨兵每秒 PING 一次服务器,以此为心跳监控服务器是否存活,如果一段时间 PING 不通服务器,会认为服务器出了故障(服务器下线)。
哨兵眼中有两种服务器下线,一种是主观下线,一种是客观下线:
主观下线
:连续一段时间都 PING 不通服务器(超时时间在配置文件中配置),将服务器标记为主观下线
。客观下线
:如果认定主服务器主观下线
,为了确认它是否真的下线,会跟其他监视该服务器的哨兵通信,看其他哨兵是否也认为这个主服务器主观下线
。如果有一定数量的哨兵(该数量也在配置文件中配置)都认定该主服务器主观下线
,那么就可以认定这个主服务器真的下线了,即客观下线
。
当一个主服务器被判断为客观下线后,会着手选一个新的主服务器:
从所有监视原来主服务器的哨兵中,选举出一个领头哨兵。
选举方式采用 Raft 协议,这是一个分布式协议,有个做得特别好的科普 Raft 的网站分享一下:《Raft Understandable Distributed Consensus》。
由领头哨兵从所有从服务器中,挑选出来一个从服务器(挑选算法有点麻烦,略),并将其转换为主服务器(发送 SLAVEOF no one 命令)。
向其他从服务器发送 SLAVEOF 命令,让它们复制新的主服务器。
Redis 哨兵实现了自动故障恢复,但无法实现读写的负载均衡。如果有多台 Redis 服务器,如何让 key-value 均衡地存储进多台服务器里,兼顾高可用和高性能,是 Redis 集群(Redis Cluster)关注的问题。
一个 Redis 集群由多个 Redis 服务器组成,每个 Redis 服务器被称为一个节点(Cluster Node),也就是多个节点构成一个集群。集群内部采用哈希槽的方式存放数据,例如整个集群预设 16384 个槽位,由 A、B、C 三个节点分占:
- 节点 A 占用第 0 到第 5000 个槽位
- 节点 B 占用第 5001 到第 10000 个槽位
- 节点 C 占用第 10001 到第 16383 个槽位
每个 key 通过 CRC16 校验后对 16384 取模,根据数值决定存放在哪个槽位中。例如 “msg” 经过 CRC16 校验后取模算出来 6257,最终会存储在第 6257 个槽位里,也就是在节点 B 中。
每个节点的数据结构
有三个类:
- clusterNode:节点类
- clusterLink:连接节点类
- clusterState:集群状态类
启动一个 Redis 服务器,如果配置开启集群模式(在 conf 文件中配置 cluster-enabled yes),那么这个 Redis 服务器在启动时就会成为一个集群节点(尽管此时只有它自己一个)。
这个节点在创建时,会实例化一个 clusterNode 对象,也就是它自己:
1 | struct clusterNode { |
它还会实例化一个 clusterLink 对象,用来保存和其他节点连接时的所需信息:
1 | typedef struct clusterLink { |
该节点还会实例化一个 clusterState 对象,用于记录集群的整体信息:
1 | typedef struct clusterState { |
Redis 集群是分布式而非中心式的,并没有一个统一的中心管理器,而是在每个节点中,都存储着集群信息和其他节点信息。
如果节点 A 和节点 B 合并入同一个集群,节点 A 内部会创建一个 clusterNode 对象用来记录节点 B,同样节点 B 也会创建一个 clusterNode 对象用来记录节点 A,两个节点也都会各自更新 clusterState 对象(更新集群信息)。
通过任意一个节点,都可以获知整个集群的工作状况,以及其他联系到节点。
哈希槽
如何把数据分布到集群中的多个节点,做到读写的负载均衡,是一个分布式系统的常见问题。数据分布有很多方案,例如顺序分布、哈希分布、一致性哈希、虚拟槽分区等,Redis 集群采用虚拟槽分区,也就是哈希槽。
哈希槽(槽 slot)中的槽位,是一个虚拟的概念,比如划分了 10000 个槽位,4 个节点各自分得 1000、2000、3000、4000 个槽位,之后通过计算哈希值在哪个槽里,就能知道数据应该存储在哪个节点里,这个过程不是真的需要槽位,槽只是个虚拟概念。
Redis 集群共设置了 16384 个槽位,不管有多少个节点,都是分这 16384 个槽。(16384 即 2^14,是 Redis 作者觉得合适的大小,跟数据传输和压缩比有关,具体原因自行谷歌)
当这 16384 个槽位没有被分完时,集群处于下线状态,只有当所有的槽都被分完,集群才能上线。
通过向节点发送 CLUSTER ADDSLOTS 命令,可以将一个或多个槽位指派给节点负责,例如:
1 | CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000 |
节点处理好后,还会同步给集群内的其他节点,每个节点都会知道,哪个槽位由哪个节点负责。
- 在 clusterNode 结构中,会存储本节点负责处理哪些槽(以位数组 bit array 的数据结构存储,省空间)。
- 在 clusterState 结构中,会存储每个槽位由哪个节点负责(长度为 16384 的节点数组,
clusterNode *slots[16384]
)。
也就是说,每个节点同时存储了【自己负责的槽位】和【所有槽位由谁负责】。当查询 key 时可以直接查到负责的节点,当需要获取本节点负责哪些槽位时(例如槽重新分片)也可以直接返回。
计算 key 存储在哪个槽里,哈希算法是首先计算 key 的 CRC-16 校验和,然后再用该校验和对 16384 取模,计算出一个介于 0 至 16383 之间的数值,也就是槽位。这个哈希算法一行代码就可以实现:
1 | CRC16(key) & 16384 |
当读写一条 Redis 数据时,可以向任何一个节点发出命令,接到命令的节点将首先计算出槽位 i
,再读取 clusterState 中存储的 slots[i]
,得知该槽位由哪个节点负责。如果是自己负责,直接处理,如果是别的节点负责,返回 MOVED 错误,让客户端进行节点转向,转到正确的节点上重新执行命令(MOVED 错误是暗中进行的)。
哈希槽还可以由一个节点转给另一个节点,这被称为重新分片。这个过程是由 Redis 集群管理软件 redis-trib 来负责的,大致逻辑是找到每个槽的负责节点,一个槽一个槽地迁移。没意思,略了。
这周就写到这里吧。