集群篇

  • 1 哨兵 sentinel
  • 1.1 脑裂
  • 2 非官方集群化解决方案Codis
  • 3 官方集群化解决方案Cluster
  • 3.1 跳转
  • 3.2 迁移


1 哨兵 sentinel

sentinel用来进行节点的自动主从切换。当主节点(master节点)发生故障时,sentinel自动选择最优的从节点(salves节点)切换为主节点,客户端也无需人工修改新的主节点地址再进行重启,而是问询sentinel,sentinel将最新的主节点地址告知客户端。整个切换过程无需人工干预即可完成,能够在故障发生时快速响应,尽快恢复服务。
可以将sentinel集群看成一个ZooKeeper集群,他是集群高可用的心脏,一般由3~5个节点组成,这样个别节点挂了,集群还能正常运转。
sentinel监控Redis集群内的节点状态,当主节点发生故障时,sentinel自动选择最优的从节点切换为主节点。客户端连接集群时,首先sentinel获得主节点的地址,然后再根据地址进行交互,当主节点故障无法连接时,客户端断开对故障主节点的连接,重新向sentinel获得新的主节点的地址建立连接,这样就完成了故障的自动切换。
Redis的主从同步是最终一致,这就导致主节点挂掉时,从节点还没有收到全部的同步消息,这意味着会存在数据丢失,主从同步延迟越大丢失的越多。sentinel本身并不保证数据的一致性让数据不丢失,但是sentinel会限制集群的可用性,以尽可能减少丢失。
有两个参数:

min-slaves-to-write 2
min-slaves-max-lag 10

Redis5.0之后参数改为:

min-replicas-to-write 2
min-replicas-max-lag 10

这两个参数的意思是:min-slaves-to-write 2(min-replicas-to-write 2)表示主节点至少要有2个正常同步的从节点,如果达不到要求,主节点会禁止写服务,仅提供读服务。 min-slaves-max-lag 10(min-replicas-max-lag 10)就是用来界定正常同步的条件,即主从延迟在10s以内,如果超过10s没接到从节点反馈,则认为此从节点处于异常同步状态,除此之外,这两个参数更重要作用是防止集群脑裂。

1.1 脑裂

脑裂简单的来说就是一个集群因为网络问题产生了两个master节点,就像两个脑袋一样,两个master节点为客户端提供服务又无法同步导致数据不一致,就可能产生setnx加锁无效,incr自增重复等问题。
脑裂是如何产生的呢?如果master节点同时与sentinel集群和salves节点产生网络分区,但是master节点与客户端网络正常,正常提供服务,此时sentinel集群无法感知到master节点,就会选择一个salves节点称为新的master节点,但是sentinel集群无法感知到旧master节点,又无法将旧节点降级为salver节点。那么客户端新建立的链接就是和新的master节点建立的,而因为旧的master节点与客户端网络正常,因此部分客户端依然与旧的master节点链接,新旧master节点的网络分区也使得他们无法同步,此时就会出现两个master节点的情况,出现了脑裂。后面即使网络恢复,旧master节点成功被降级为salver节点,此时旧master节点就会追赶新master节点,那么网络故障期间在旧master节点修改的数据就会被覆盖而丢失。
通过配置min-replicas-to-write与min-replicas-max-lag,当脑裂发生时,指向旧master节点的写请求就会被拒绝,那些链接了旧master的客户端就会重新向sentinel获取新的master节点地址,网络恢复后旧master节点被降级为salver节点并进行数据追赶,故障也就恢复了。

当主节点因为故障导致切换,这种属于被动切换,客户端连接不上master节点就会重新向sentinel获取新的master节点地址,但是如果sentinel主动进行主从切换,master节点依然正常服务,此时切换的方式是通过只读限制进行,当客户端对旧master节点执行写指令时,就会收到此连接只读的错误,那么客户端就会重新获取新的master节点进行操作,完成切换。因此一般的集成性工具包里面都会通过在这两个地方捕获异常来完成主节点切换。

2 非官方集群化解决方案Codis

Redis单例首先会导致内存过大,这也意味着rdb文件过大,主从同步的的全量同步时间也要消耗更多的时间,重启的数据恢复消耗的时间也更多。其次是CPU,单个实例也使得处理性能受单个实例的CPU限制。集群方案可以将多个小内存实例综合起来减少单实例内存大小,并将多台实例的CPU计算能力聚集到一起。
Codis是一款非官方的Redis集群解决方案。Codis将挂载的Redis实例构建成一个集群,可以动态的扩缩容。客户端操作Codis和操作Redis没什么区别,Codis本身只是作为一个转发代理中间件,将特定的Key转发给特定的Redis实例,因此可以通过多个Codis节点来提升QPS,并提升容灾能力。
默认情况下Codis将key划分为1024个槽位(slot)槽位数目是可配置的,如果集群Redis节点较多,可以扩大。对于指令中的key,Codis先进行crc32运算得出hash值,然后对槽位数取模得到一个余数,这个余数就是这个key对应的槽位。Codis要做的就是维护槽位和Redis实例的对应关系。前面说了Codis支持多节点,因此Codis采用zookeeper或者etcd(可选)来同步槽位与实例的对应关系,当槽位关系变化时能及时同步各个节点。
槽位关系改变意味着需要进行数据迁移,Codis增加了SLOTSSCAN指令来扫描槽内的所有key,迁移时Codis通过SLOTSSCAN扫描需要迁移的槽位,获得迁移的key,然后挨个迁移key到新实例,如果有新的请求到正在迁移的槽位上,Codis会先强制对这个key进行迁移,然后将请求转发到新实例上,以此避免数据错乱。SLOTSSCAN和SCAN命令一样无法避免重复,但是这并不影响迁移,因此迁移完成的key会被清除,再次扫描时也就是扫描不出来了。
如果使用mget获取多个key的值,Codis的方案是将key解析后分到不同的组,然后每个组调用mget获得结果,在Codie整合后返回客户端。
Codis提供了自动均衡功能,当Redis实例对应的槽位不均衡时,Codis会自动进行调整。
Codis提供集群化的解决方案的同时也带了了一定的缺点:

  • key分布在不同的实例,因此Codis无法支持事务
  • rename指令同样危险,因为新旧name可能在不同的实例,导致rename无法正确完成
  • key对应内容不宜过大,因为Codis对于一个key是一次性迁移,如果key过大会导致迁移耗时过长,服务卡顿,官方建议单个结构字节量不超过1M,因此大结构需要在业务上拆分。
  • 新增了一层Codis的网络代价,性能有所下降,不过影响有限。
  • 多了zk运维的代价,不过有的企业有内部的zk集群,可以直接使用
  • 非官方方案,这就导致Codis无法及时支持Redis的新功能,但是Codis也提供官方Cluster没有的简便功能,如federation(联邦),可以对多个集群进行管理,减少运维成本。

3 官方集群化解决方案Cluster

ReidsCluster是Redis的官方集群解决方案。
Cluster是去中心化的,他将数据分为16384个槽位(slots),而且不将槽位信息存在另外一个分布式系统上,而是每个节点都维护一份完整的槽位信息。当Cluster的客户端链接集群时,就会获得一份完整的槽位信息,这样客户端要查找某个Key时,就可以直接定位到目标节点。这点不同于Codis通过Proxy来确定目标节点,但是这也表示Cluster客户端需要自己缓存一套槽位信息已进行准备定位,而且集群槽位可能会出现与客户端缓存信息不一致的情况,因此需要一个纠正机制。
Cluster同样是用crc32算法计算出hash值,然后再对16384取模计算出槽位,除此之外Cluster还支持通过在key字符串中嵌入tag标记强制key挂在特定的槽位上。

3.1 跳转

如果客户端向一个错误的节点发送指令,节点发现这个key不归自己管,就会回复客户端一个跳转指令,让他去正确的节点:

get name
-MOVE 3999 111.0.0.1:6381

MOVE前面的’-'标识这是一条错误信息, 3999标识这个key所对应的槽位,111.0.0.1:6381标识这个key所在的节点,客户端收到消息后需要纠正自己的槽位表,然后去正确节点获取数据。

3.2 迁移

Cluster提供了redis-trib来让运维人员进行手动槽位调整,这一点Codis就更好,不仅有UI界面方便迁移,还有自动平衡的功能。
redis-trib的迁移单位是槽,当一个槽在迁移时,原节点处于migrating状态,目标节点处于importing状态。redis-trib设置好原节点和目标节点状态后,就一次性获得槽位内的所有key,然后依次进行迁移。迁移过程中,原节点作为客户端,目标节点作为服务端:原节点对key执行dump指令序列化内容,然后向目标节点发送带有序列化内容的restore指令,目标节点存储成功后返回OK,原节点删除Key。
在目标节点执行restore到原节点删除key这个过程中,原节点的线程是阻塞的,这样避免了新旧节点同时存在信息的并发问题,但是因此如果一个key很大,那么就会导致原节点和目标节点都产生卡顿,影响服务。
如果迁移过程中发生故障,那么恢复后因为原节点和目标节点还处于迁移状态,所以redis-trib会在恢复后提示继续迁移。
如果在迁移过程中发生了对原节点的数据访问,客户端会先访问原节点,如果有则直接返回,如果没有则要么是key已被迁移,要么是这个key不存在,此时原节点就会返回一个 -Ask targetNodeAddr的错误消息,客户端在接到这个指令时,要先向目标节点发送一个asking指令,然后再对目标节点执行数据访问。
asking指令的目的就是告诉目标节点,下一条指令不要进行槽位校验,这是因为迁移完成之前槽位的映射关系还在原节点上,直接访问目标节点,目标节点发现这个槽位不归自己管就会返回-MOVE信息,让去访问原节点,陷入死循环。可以看到迁移有可能影响到服务效率,让一个ttl能完成的指令变成需要3个ttl才能完成。
重试纠错机制还可能导致多次重试,比如客户端访问一个错误节点,获得-MOVE消息后进行重试,重试新节点,新节点又恰好在迁移返回-ASK消息,此时就需要再次重试。为了避免这种情况,一些语言的客户端会提示重试次数限制设置,超过次数直接向上层抛异常。

如果一个主节点故障,会自动选择一个从节点晋级为主节点,但是如果主节点没有挂从节点,那么这个节点就无法服务了,此时整个集群是继续服务还是暂停服务可以通过cluster-require-full-coverage 设置,允许部分节点故障后继续对外提供服务。节点的故障判断通过cluster-node-timeout设置,只有失联时间超过这个值才进行主从切换,这样可以有效避免网络抖动导致的频繁主从切换,并且可以通过cluster-slave-validity-factor设置容忍值,就是实际校验时间是两个参数的乘积,然后我感觉没有必要呀,直接改成超时时间不就行了,为什么还分两个参数。
因此Cluster是去中心化的,所以没有一个中心节点来决断节点是否断链接,集群中一个节点任务另一个节点失联并不代表所有节点都链接不上目标节点了,因此如果一个节点发现另一个节点失联后会通过Gossip协议进行广播,如果一个节点收到的目标节点失联消息数据占据节点的大多数,就会标记该节点下线,并且广播其他节点,强迫其他节点接收此节点失联,并对失联节点进行主从切换。
如果是因为目标节点挂掉导致的主从切换,客户端访问时会收到一个ConnectionError,此时客户端任意取一个节点访问,通过-MOVE就能获得新节点位置。

PS:
【JAVA核心知识】系列导航 [持续更新中…] 关联导航:Redis应用篇 关联导航:Redis基础篇 关联导航:Redis原理篇 关联导航:Redis的过期删除策略与淘汰策略 欢迎关注…

参考资料:
《Redis深度历险》