[Redis]集群
李代桃僵--Sentinel
目前我们还只是讲 Redis 的主从最终一致性。大家可曾思考过,如果主节点凌晨3 点突发宕机怎么办?只能坐等运维人员从床上爬起来,然后手工进行主从切换,再通知所有的程序把地址统统改一遍重新上线吗?毫无疑问,这样的人工运维效率太低,事故发生后估计至少要花费1个小时才能缓过来。如果一个大型公司发生这样的事故,足以登上新闻了。
所以我们必须要有一个高可用方案来抵抗节点故障,当故障发生时可以自动进行主从切换,程序可以不用重启,运维人员可以继续睡大觉,仿佛什么事也没发生一样。Redis 官方提供了这样一种方案--Redis Sentinel(Sentinel 的含义是哨兵)。如图 3-5 所示,我们可以将 Redis Sentinel 集群看成是一个zookeeper 集群,它是集群高可用的心脏,一般由3~5个节点组成,这样即使个别节点挂了,集群还可以正常运转。
Sentinel 负责持续监控主从节点的健康,当主节点挂掉时,自动选择个最优的从节点切换成为主节点,客户端来连接集群时,会首先连接 Sentinel ,通过 Sentinel来查询主节点的地址
,然后再连接主节点进行数据交互。当主节点发生故障时,客户端会重新向 Sentinel 要地址, Sentinel 会将最新的主节点地址告诉客户端。如此应用程序将无须重启即可自动完成节点切换。
比如图 3-5 所示的主节点挂掉后,集群将可能自动调整为图 3-6 所示结构
从图中我们能看到,如果主节点挂掉了,原先的主从复制也断开了,客户端和损坏的主节点也断开了。一个从节点被提升为新的主节点,其他从节点开始和新的主节点建立复制关系。客户端通过新的主节点继续进行交互,Sentinel 会持续监控己经挂掉了的主节点,待它恢复后,集群会调整为图 3-7 所示的结构。
原先挂掉的主节点现在变成了从节点,从新的主节点那里建立复制关系。
消息丢失#
Redis 主从采用异步复制,意味着当主节点挂掉时,从节点可能没有收到全部的同步消息,这部分未同步的消息就丢失了。如果主从延迟特别大,那么丢失的数据就可能会特别多。Sentinel无法保证消息完全不丢失,但是也能尽量保证消息少丢失。它有两个选项可以限制主从延迟过大。
min-slaves-to-write 1
min-slaves-max-lag 10
第一个参数表示主节点必须至少有一个从节点在进行正常复制,否则就停止对外写服务,丧失可用性。
何为正常复制,何为异常复制?这是由第二个参数控制的,它的单位是秒(s)表示如果在 10s内没有收到从节点的反馈,就意味着从节点同步不正常,要么是网络断开了,要么是一直没有给反馈。
Sentinel 基本用法#
接下来我们看看客户端如何使用 Sentinel。标准的流程应该是客户端可以通过 Sentinel 发现主从节点的地址,然后再通过这些地址建立相应的连接来进行数据存取操作。我们来看看 Python 客户端是如何做的。
>>> from redis.sentinel import Sentinel
>>> sentinel = Sentinel([('localhost',26379)],socket_timeout=0.1)
>>> sentinel.discover master('mymaster')
('127.0.0.1',6379)
>>> sentinel.discover slaves('mymaster')
[('127.0.0.1',6380)]
Sentinel 的默认端口是 26379,不同于 Redis 的默认端口 6379,通过 Sentinel 对象的 discover xxx方法可以发现主从地址。主地址只有一个,从地址可以有多个。
>>> master = sentinel.master_for('mymaster',socket_timeout = 0.1)
>>> slave = sentinel.slave_for('mymaster',socket_timeout = 0.1)
>>> master.set('foo','bar')
>>> slave.get('foo')
'bar'
通过 xxx_for 方法可以从连接池中拿出一个连接来使用,因为从地址有多个Redis 客户端对从地址采用轮询方案,也就是 RoundRobin 轮着来。
有一个问题是,当 Sentinel 进行主从切换时,客户端如何知道地址变更了?通过分析源码,老钱发现 redis-py 在建立连接
的时候进行了主节点地址变更判断。
连接池建立新连接
时,会去查询主节点地址,然后跟内存中的主节点地址进行比对,如果变更了
,就断开所有连接,重新使用新地址建立新连接。如果是旧的主节点挂掉了,那么所有正在使用的连接都会被关闭,然后在重连时就会用上新地址。但是这样还不够,如果是 Sentinel 主动进行主从切换的,但主节点并没有挂掉
,而之前的主节点连接已经建立了且在使用中,没有新连接需要建立,那么这个连接是不是一直切换不了?
继续深入研究源码,老钱发现 redis-py 在另外一个地方也做了控制,那就是在处理命令的时候捕获了一个特殊的异常 ReadOnlyError
,在这个异常里将所有的旧连接全部关闭了,后续指令就会进行重连。主从切换后,之前的主节点被降级为从节点,所有的修改性的指令都会抛出ReadonlyError
。如果没有修改性指令,虽然连接不会得到切换,但是数据不会被破坏所以即使不切换也没关系。
众志成城--Cluster
Redis Cluster是Redis的“亲儿子”,它是Redis作者自己提供的 Redis 集群化方案。与 Codis有所不同,Redis Cluster是去中心化的,如图3-16 所示,该集群由三个Redis 节点组成,每个节点负责整个集群的一部分数据,每个节点负责的数据多少可能不一样。这三个节点相互连接组成一个对等的集群,它们之间通过一种特殊的二进制协议交互集群信息。
Redis Cluster 将所有数据划分为16384
个槽位,它比 Codis 的 1024 个槽位划分得更为精细,每个节点负责其中一部分槽位。槽位的信息存储于每个节点中,它不像 Codis,不需要另外的分布式存储空间来存储节点槽位信息。
当 Redis Cluster 的客户端来连接集群时,也会得到一份集群的槽位配置信息。这样当客户端要查找某个 key 时,可以直接定位到目标节点。这一点不同于 Codis,Codis 需要通过 Proxy 来定位目标节点,Redis Cluster 则直接定位。
客户端为了可以直接定位某个具体的 key 所在的节点,需要缓存槽位相关信息,这样才可以准确快速地定位到相应的节点。同时因为可能会存在客户端与服务器存储槽位的信息不一致的情况,还需要纠正机制来实现槽位信息的校验调整。
另外,Redis Cluster 的每个节点会将集群的配置信息持久化到配置文件中,所以必须确保配置文件是可写的,而且尽量不要依靠人工修改配置文件。
槽位定位算法#
Redis Cluster 默认会对 key 值使用 crc16 算法进行 hash,得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位。
Redis Cluster 还允许用户强制把某个 key 挂在特定槽位上。通过在 key 字符串里面嵌入 tag 标记,这就可以强制 key 所挂的槽位等于 tag 所在的槽位。
def HASH_SLOT(key)
s = key.index "{"
if s
e = key.index "}",s+1
if e && e != s+1
key = key[s+1..e-1]
end
end
crc16(key)%16384
end
跳转#
当客户端向一个错误的节点发出了指令后,该节点会发现指令的key 所在的槽位并不归自己管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去连接这个节点以获取数据。
GET x
-MOVED 3999 127.0.0.1:6381
MOVED 指令的第一个参数 3999 是key 对应的槽位编号,后面是目标节点地址。
MOVED 指令前面有一个减号,表示该指令是一个错误消息。
客户端在收到 MOVED 指令后,要立即纠正本地的槽位映射表。后续所有 key 将使用新的槽位映射表。
迁移#
Redis Cluster 提供了工具 redis-trib 可以让运维人员手动调整槽位的分配情况,它使用 Ruby 语言开发,通过组合各种原生的 Redis Cluster 指令来实现。这一点 Codis做得更加人性化,它不但提供了UI界面可以让我们方便地迁移,还提供了自动化平衡槽位工具,无需人工干预就可以均衡集群负载。不过 Redis 官方向来的策略就是提供最小可用的工具,其他都交由社区完成。接下来我们仔细看看 Redis Cluster 的数据迁移过程。
Redis 迁移的单位是槽,Redis一个槽一个槽地进行迁移,当一个槽正在迁移时这个槽就处于中间过渡状态。如图 3-17 所示,这个槽在源节点的状态为 migrating
,在目标节点的状态为 importing
,表示数据正在从源节点流向目标节点。
迁移工具 redis-trib 首先会在源节点和目标节点设置好中间过渡状态,然后一次性获取源节点槽位的所有 key 列表(keysinslot指令,可以部分获取),再挨个key进行迁移。每个 key 的迁移过程是以源节点作为目标节点的“客户端”,源节点对当前的 key 执行 dump 指令得到序列化内容,然后通过“客户端”向目标节点发送 restore 指令携带序列化的内容作为参数,目标节点再进行反序列化就可以将内容恢复到目标节点的内存中,然后返回“客户端”OK,源节点“客户端”收到后再把当前节点的key 删除掉就完成了单个 key 迁移的全过程。
大致流程如下:从源节点获取内容一存到目标节点一从源节点删除内容。注意这里的迁移过程是同步的,在目标节点执行 restore 指令到源节点删除 key之间,源节点的主线程会处于阻塞状态,直到 key 被成功删除。
如果迁移过程中突然出现网络故障,整个槽的迁移只进行了一半,这时两个节点依旧处于中间过渡状态,待下次迁移工具重新连上时,会提示用户继续进行迁移。
在迁移过程中,如果每个 key 的内容都很小,migrate 指令会执行得很快,它就不会影响客户端的正常访问。如果 key 的内容很大,因为 migrate 指令是阻塞指令,会同时导致源节点和目标节点卡顿,影响集群的稳定型。所以在集群环境下,业务逻辑要尽可能避免产生很大的 key。
在迁移过程中,客户端访问的流程会有很大的变化。
首先新旧两个节点对应的槽位都存在部分 key 数据。客户端先尝试访问旧节点,如果对应的数据还在旧节点里面,那么旧节点正常处理。如果对应的数据不在旧节点里面,那么有两种可能,要么该数据在新节点里,要么根本就不存在。旧节点不知道是哪种情况,所以它会向客户端返回一个-ASK targetNodeAddr
的重定向指令,客户端收到这个重定向指令后,先去目标节点执行一个不带任何参数的 ASKING 指令,然后在目标节点再重新执行原先的操作指令。
为什么需要执行一个不带参数的 ASKING 指令呢?
因为在迁移没有完成之前,按理说这个槽位还是不归新节点管理的,如果这个时候向目标节点发送该槽位的指令,节点是不认的,它会向客户端返回一个-MOVED重定向指令告诉它去源节点去执行。如此就会形成重定向循环。ASKING 指令的目标就是打开目标节点的选项,告诉它下一条指令不能不理,而要当成自己的槽位来处理。从以上过程可以看出,迁移是会影响服务效率的,同样的指令在正常情况下一个 tt 就能完成,而在迁移情况下需要3个ttl才能搞定,
容错#
Redis Cluster 可以为每个主节点设置若干个从节点,当主节点发生故障时,集群会自动将其中某个从节点提升为主节点。如果某个主节点没有从节点,那么当它发生故障时,集群将完全处于不可用状态。不过 Redis 也提供了一个参数 cluster-require-full-coverage
可以允许部分节点发生故障,其他节点还可以继续提供对外访问。
网络抖动#
真实世界的机房网络往往不是风平浪静的,它们经常会发生各种各样的小问题。比如网络抖动就是非常常见的一种现象,突然之间部分连接变得不可访问,然后很快又恢复正常。为解决这种问题,Redis Cluster 提供了一种选项 cluster-node-timeout
,表示当某个节点持续 timeout 的时间失联时,才可以认定该节点出现故障,需要进行主从切换。如果没有这个选项,网络抖动会导致主从频繁切换(数据的重新复制)。还有另外一个选项 cluster-slave-validity-factor
作为倍乘系数放大这个超时时间来宽松容错的紧急程度。如果这个系数为零,那么主从切换是不会抗拒网络抖动的。如果这个系数大于1,它就成了主从切换的松弛系数。
可能下线(PFail)与确定下线(Fail )#
因为 Redis Cluster 是去中心化的,一个节点认为某个节点失联了并不代表所有的节点都认为它失联了,所以集群还得经过一次协商的过程,只有当大多数节点都认定某个节点失联了,集群才认为该节点需要进行主从切换来容错。
Redis 集群节点采用 Gossip 协议来广播自己的状态以及改变对整个集群的认知。比如一个节点发现某个节点失联了(PFail,即Possibly Fail),它会将这条信息向整个集群广播,其他节点就可以收到这点的失联信息。如果收到了某个节点失联的节点数量(PFail Count)已经达到了集群的大多数,就可以标记该失联节点为确定下线状态(Fail),然后向整个集群广播,强迫其他节点也接受该节点已经下线的事实,并立即对该失联节点进行主从切换。
Cluster 基本用法#
redis-py 客户端不支持 Cluster 模式,要使用 Cluster,必须安装另外一个包,这个包是依赖 redis-py 包的。
pip install redis-py-cluster
下面我们看看 redis-py-cluster 如何使用。
>>> from rediscluster import StrictRedisCluster>>>#Requires at least one node for cluster discovery. Multiplenodes is recommended.
>>> startup nodes[{"host":"127.0.0.1","port":"7000"}]>>>rcStrictRedisCluster(startup nodes=startup nodes, decoderesponses=True)
>>> re.set("foo","bar")
True
>>> print(rc.get("foo”))
'bar'
Cluster 是去中心化的,它由多个节点组成,构造 StrictRedisCluster 实例时,我们可以只用一个节点地址,其他地址可以自动通过这个节点来发现。不过如果提供多个节点地址,安全性会更好。如果只提供一个节点地址,那么如果这个节点挂了,客户端就必须更换地址才可以继续访问Cluster。第二个参数 decode responses 表示是否要将返回结果中的 byte 数组转换成 unicode。
Cluster 使用起来非常方便,和普通的redis-py 差别不大,仅仅是构造方式不同。但是它们也有迥异之处,比如:Cluster不支持事务:Cluster的mget方法比 Redis要慢很多,被拆分成了多个 get 指令;Cluster 的 rename 方法不再是原子的,它需要将数据从源节点转移到目标节点。
槽位迁移感知#
如果 Cluster 中某个槽位正在迁移或者已经迁移完毕,那么客户端如何能感知到槽位的变化呢?客户端保存了槽位和节点的映射关系表,它需要及时得到更新,才可以正常地将某条指令发到正确的节点中。
我们前面提到Cluster有两个特殊的error指令,一个是MOVED
,一个是ASKING
。
MOVED 指令是用来纠正槽位的。如果我们将指令发送到了错误的节点,该节点发现对应的指令槽位不归自己管理,就会将目标节点的地址随同 MOVED 指令回复给客户端通知客户端去目标节点去访问。这个时候客户端就会刷新自己的槽位关系表,然后重试指令,后续所有打在该槽位的指令都会转到目标节点。
ASKING 指令和 MOVED 不一样,它是用来临时纠正槽位的。如果当前槽位正处于迁移中,指令会先被发送到槽位所在的旧节点。如果旧节点存在数据,那就直接返回结果了,如果不存在数据,那么数据可能真的不存在,也可能在迁移目标节点上,所以旧节点会通知客户端去新节点尝试拿数据,看看新节点有没有。这时就会给客户端返回一个 asking error 携带上目标节点的地址。客户端收到这个 askingcrror 后,就会去目标节点尝试。客户端不会刷新槽位映射关系表,因为它只是临时纠正该指令的槽位信息,不影响后续指令。
重试2次
MOVED 和 ASKING 指令都是重试指令,客户端会因为这两个指令多重试一次。大家有没有想过:会不会存在一种情况,客户端有可能重试2次呢?这种情况是存在的,比如一条指令被发送到错误的节点,这个节点会先给你一个 MOVED 错误告知你去另外一个节点重试,所以客户端就去另外一个节点重试了,结果刚好这个时候运维人员要对这个槽位进行迁移操作,于是给客户端回复了一个ASKING 指令告知客户端去目标节点去重试指令。所以这种情形下,客户端重试了2次。
重试多次
在某些特殊情况下,客户端甚至会重试多次,大家可以打开自己的脑洞,想一想什么情况下客户端会重试多次。
正是因为存在多次重试的情况,所以客户端的源码里在执行指令时都会有一个循环,然后会设置一个最大重试次数,Java 和 Pyihon 都有这个参数,只是设置的值不一样。当重试次数超过这个值时,客户端会直接向业务层抛出异常。
集群变更感知#
当服务器节点变更时,客户端应该立即得到通知以实时刷新自己的节点关系表。那么客户端是如何得到通知的呢?这要分为两种情况。
-
目标节点挂掉了,客户端会抛出一个 ConnectionError,紧接着会随机挑一个节点来重试,这时被重试的节点会通过 MOVED指令告知目标槽位被分配到的新的节点地址。
-
运维手动修改了集群信息,将主节点切换到其他节点,并将旧的主节点移除出集群。这时打在旧的主节点上的指令会收到一个 ClusterDown 的错误,告知当前节点所在集群不可用(当前节点已经被孤立了,它不再属于之前的集群)。这时客户端就会关闭所有的连接,清空槽位映射关系表,然后向上层抛错。待下一条指令过来时,就会重新尝试初始化节点信息。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器