关注公众号 【离心计划】呀,一起逃离地球表面
Redis专栏合集
【专栏】01| Redis夜的第一章
【专栏】基础篇02| Redis 旁路缓存的价值
【专栏】基础篇03| Redis 花样的数据结构
【专栏】基础篇04| Redis 该怎么保证数据不丢失(上)
【专栏】基础篇05| Redis 该怎么保证数据不丢失(下)
【专栏】核心篇06| Redis 存储高可用背后的模式
前言
前一节我们从存储高可用的角度讲了一下其背后的经典架构模式,数据库发展这么多年,发展到分布式、云计算、大数据的时代,单机一体化自然有些水土不服,随着Google的GFS、MapReduce和BigTable的发表,分布式计算与分布式存储方案遍地开花,分片式存储大环境下对于流量来说是负载均衡的体现,对于数据存储来说是Auto-Sharding的刚需,这一节让我们看看Redis在其发展过程中“jio”美的集群模式。
从主从到哨兵
Redis的主从模式与我们上一节讲的相同,属于整库存储,主库负责写从库负责读,分流读写请求。
但是为了如果出现了故障,就需要运维人员手动进行主从切换,这是很繁琐的,那么工程师们首先想到的就是让这个工作自动化,还记得上一小节我们提到的两种方式:双机互联切换、中间代理切换,而哨兵就是这个中间代理。
监控
哨兵是一种特殊的Redis实例,只是在配置Redis上有所不同。哨兵作为Redis的故障切换工作者,主要有监控与切换两部分工作。监控主要是健康检测,哨兵会定时去ping主从,如果主库没有响应pong,那哨兵就判定这个主库挂了,判定为下线状态,然后完成主从切换。而哨兵本身也是以集群的方式提供检测与切换服务,所以有多个哨兵时就需要达成一个共识,共识有两方面:大家都觉得它挂了和自己觉得它挂了,这在哨兵集群中称为客观下线与主观下线。
我们按照事前事中和事后的方法分析哨兵集群完成主从切换需要做的事情,首先事前每个哨兵实例需要不间断去检查主库和从库,由于哨兵配置时会指定主库,所以主库的ip与端口肯定知道,那么哨兵是怎么知道有哪些从库的呢?这其实是哨兵会向主库发送INFO命令,主库会返回从库列表给哨兵。
事中则是当哨兵发现主库有问题时,会判定为主观下线,并且需要让别的实例知道,那么哨兵之间是怎么互相通知的呢?这就需要Redis的消息订阅机制了,消息队列的工作模式中就有订阅/发布模式,Redis中亦然。哨兵们会在主库上订阅一个频道,比如叫做__sentinel_info,那么哨兵可以在这个频道上发送自己上线的事件比如新来了一个哨兵,发送了自己的ip与端口信息,别的哨兵订阅了这个频道就会收到这个消息,就知道多加入了一个兄弟,这样哨兵之间就都知道了彼此的ip与端口,这也是为什么在配置哨兵集群时只需要在配置文件中写好主库的地址,就能知道互相通信的原因。
哨兵判断主库主观下线后会发送is-master-down-by-addr询问其他实例是否同意下线这个主库,别的哨兵收到后会根据自身判断情况给出yes或者no的响应。那这里又有一个问题,最终判断下线的标准是什么呢?哨兵很公平:少数服从多数,因此当一个哨兵收到超过一半的yes票后,就可以判定这个主库为客观下线了。
选主
事后则是判断完是否下线后,就需要进行主从切换,那么第一个问题就是,谁来切换,这么多哨兵兄弟,总不能每个人去切换一遍。这又是哨兵中的leader选举过程了,当哨兵实例判断为客观下线后,就毛遂自荐,向别的哨兵表明我可以去进行切换,别的哨兵实例需要给这个自荐的实例投票,当自荐的哨兵满足超过一般的赞成票并且超过了配置文件的票数阙值才能进行主从切换,这其中还有一个规定,就是一个哨兵只能给一个自荐者投票,这样才能保证最后的唯一性。当唯一的一个自荐哨兵满足条件后,它就成为了leader去进行主从切换了,当然在leader选举过程中会由于各种问题导致选举失败,那么就会进行下一轮选举。
找到了谁来切换后,就有第二个问题,切换谁?这么多从库,我又得选举一个出来,真的麻烦(由此也知道故障切换是一个极其复杂且耗时的操作)。这次选举,就不是毛遂自荐的形式了,由leader哨兵独裁,那独裁总得有标准吧,到底哪个从库更优秀呢?哨兵机制是有一个评分标准的,专门用于leader选举,而评分的条件有:
-
根据已有配置优先级决定,属于人为因素,因为有些从库可能本身配置就比较好,运维人员就可以指定它的高优先级
-
与原主库同步进度。同步的越多说明数据丢失的越少
-
ID小的从库优先级更高,这个数据兜底策略
有了这些标准,就可以找出一个哨兵去完成切换的工作了。
关注公众号 【离心计划】呀,一起逃离地球表面
通知
知道了选哪一个从库成为新主库后,leader就可以完成切换,并告诉其他从库新主库的ip与端口,从库会执行replicaof命令与主库建立连接,从库会进行数据复制,关于主从库之间是如何保持数据一致性的,我们会在其他小结讲解。那么对于服务端来说,主从已经完成了切换,那么如何让客户端知道新主库的信息呢?
这又用到了Redis的消息订阅通知机制,这次的频道在每个哨兵实例上,哨兵在切换主从库后,会发送一个switch master事件到频道上,Redis客户端主动订阅哨兵该频道后,接收到这个事件就可以从中拿到新的主库ip与端口信息进行切换,当然这个过程可能就会有存在gap导致写入失败。
以上就是哨兵集群的基本工作模式,哨兵通过监控、选主、通知完成了主从切换的任务,也叫故障转移。同时哨兵集群也存在一定问题,比如需要保证哨兵集群本身的高可用,需要维护额外的哨兵实例,当然还有就是哨兵集群中每个Redis实例存放全量数据,没有解决数据容量问题,所以哨兵是站在高可用的角度,解决数据容量问题还有切片集群的方案。
Redis Cluster
上一讲我们说过切片集群的模式就是把一整份数据按照一定规则切分成多份数据,由多个实例去存储。Redis的集群方案在发展过程中主要形成了以Codis为主的第三方Proxy与官方的RedisCluster,分别对应了中心化与去中心化两种模式。
数据分布
在Redis3.0之前并没有切片官方集群方案,切片集群是数据分片存储的一种方案,而Redis Cluster是具体的一种实现。Redis作为键值数据库,在完成数据分片存储时需要知道这个key放到哪个分片,这个key从哪个分片去拿,这就涉及一个数据分布问题,Cluster的做法是将所有key分成16384个槽,每一个key根据其CRC16计算出16位的值后对16384取模后对应唯一一个槽。而槽与实例之间的映射关系,是在Cluster部署时就做好的,可以平均分配槽,也可以根据配置不同手动cluster addslots 添加槽,无论什么方式都必须完成16384个槽的全部分布。
那么key与槽之间的映射关系完成了,客户端作为集群外的角色如何知道槽与Redis实例的对应关系呢?首先,当我们分配完所有槽后,Redis实例之间可以互相通信交换彼此的槽信息并在自己这里维护一个全量路由表,当客户端以CLuster方式与集群连接后,可以知道所有实例,客户端可以通过cluster slots命令获取这个路由信息,这样客户端就知道了槽与实例的对应关系。
而每一个分片所在的实例,为了保证存储高可用,使用了双机架构中的主从模式,每一个分片下也包括一主多从,但是Cluster模式的从库是不承担流量的,只做数据备份,因此其实也可以叫做主备模式,但是这只是Cluster对它的定义,由于Cluster是去中心化的,读写分流其实做在客户端实现完全是可以的。
故障转移
Cluster下当节点发生故障后如何进行下线或者主从切换?Cluster不像哨兵集群有人帮它,它只能通过内部实例之间不断彼此通信达到互相检测的目的。具体实现我们简单来说就是每个实例不断Ping别的实例,别的实例返回对应的Pong消息,这个过程遵循Gossip协议,当Pong的延迟超过了cluster-node-timeout阙值时候,当前实例就判断目标实例主观下线,然后会广播这个主观下线的消息,只有其余的持有槽的主节点才会进行投票,从节点没资格,超过一半实例判断主观下线后,就判断为客观下线了,然后Cluster就会进行下线或者切换处理,这个过程和哨兵集群处理方式大同小异。
然后就是和哨兵一样的问题,选哪个从节点作为主节点。这里和哨兵处理流程一样,先是资格评定,由于没有哨兵,所以从节点要自评,如果与主库断开连接时间超过cluster-node-time*cluster-slave-validity-factor,则当前从节点不具备故障转移资格,cluster-slave-validity-factor设置为0代表任何slave都可以被转换为master,默认为10。
满足资格后,根据数据复制的offset大小,设置一个定时器,offset越大表示复制程度越高,定时阙值越小,达到定时时间后触发选举,这样保证数据复制多的实例更早进行选举,而选举的过程和上面一样,让其余切片主库参与投票。
最后得到唯一资格从库后,进行主从切换,然后广播给其余主库更新路由表,自己也更新一下,结束。
看到这了还不关注嘛,关注公众号 【离心计划】,一起逃离地球表面
数据迁移
当我们需要重新分布槽比如新加入了几台实例,Cluster会怎么做呢?根据官方文档(https://redis.io/commands/cluster-setslot/#redis-cluster-live-resharding-explained),主要有这么几个步骤
-
将目标节点设置为importing状态,将源实例设置为migrating状态
-
获取源实例该槽的key列表,用migrate命令分批迁移key,migrate包括dump、restore和delete三个命令,dump负责序列化对应的key,restore负责反序列化key重放命令,delete负责在源实例删除迁移的key。所以这三个命令组成的migrate是一个原子命令。
这个过程中,由于migrate是原子的,且是一个同步阻塞操作,因此如果迁移过程中存在bigkey,那么就会阻塞用户请求。
目前为止,客户端可以完成下面的几个步骤,但是当我们的槽信息发送变动时,比如将原来的槽110从实例A分配到实例B上去,这个动作如何让客户端感知呢?
-
对key进行CRC16取模获得槽索引
-
根据路由表获取槽对应的实例信息
-
向对应实例发送具体命令
这个问题本质上是一个注册发现问题,只不过我们之前的注册发现讲的都是服务,而现在是槽而已,由于上面我们说到Redis每个实例维护了全量的路由信息,因此实例对槽的动态分配是实时感知的,Redis将槽的动态发现放到了集群内部,同样我们以事前事中和事后的模板方法分析数据迁移这个问题,我们一样以槽110从A到B举例。
事前,一切正常执行,因为没有发生槽的变动。
事中,也就是正在迁移一个槽中的所有key,那么这个槽在实例A上处理进行中状态,此时如果有对槽110的key读写操作来了,实例A会返回客户端一个ASK,这个ASK包含了正在迁移的状态、实例B的地址信息;然后客户端拿到这个ASK信息后,先向实例B发送ASKING命令,这个命令的唯一作用就是打开客户端REDIS_ASKING标识,然后再发送具体指令,这个指令就会携带REDIS_ASKING标识到达B,B就知道了这个指令是经过重定向后来的,就返回具体信息,然后REDIS_ASKING标识就会被剔除,它是一次性的。
事后,已经完成了A到B的迁移,此时客户端的路由信息依旧没有更新,此时发送key到A后,A会返回MOVED,也是携带了B的地址信息,客户端拿到这个信息后会重写路由表,然后向B发送指令,这样下一次命中槽110的key就会直接发往B了。
这样,Cluster就靠自己内部的沟通完成了数据迁移并且保证了迁移过程中的稳定性,这也是去中心化存储的表现之一,通过内部元信息实现数据稳定。
喂喂喂过分了,到这还不关注,关注公众号 【离心计划】,一起逃离地球表面
Codis
Codis作为中心化集群模式,主要包括了几个组件
-
codis f&codisdashboard集群管理工具,可以以Web的方式进行集群的管理,像server的变动、数据迁移等
-
codis proxy 转发请求到server,兼容了RESP协议,可以理解为特殊Redis,也可以完成读写分离
-
codis server 二次开发的Redis,增加了支持数据迁移的数据结构
-
zookeeper 集群元数据管理,包含proxy、server信息,以及数据分布信息
数据分布
Codis的数据分布和Cluster是差不多的,也是以槽为单位分配key,只不过这个分配过程可以通过Codis dashboard完成。另外在Cluster中槽与实例的分配关系维护在每个Redis实例中,而Codis由于其中心化模式,这些信息属于元信息,统一由zookeeper管理。然后proxy可以总zk获取路由表信息,当请求来的时候由proxy处理CRC取模操作,完成key到槽、槽到实例的映射关系,而这部分操作之前是由客户端自己完成的。
另外,Codis也是使用了主从架构,但是和Cluster不一样的是Codis的从实例天然支持读写分离,在上层Proxy就可以做掉。
故障转移
Codis的故障转移是借助了哨兵机制来保证,从上面Codis的架构图中也可以看到右上部分有Sentinel部分,这边不再阐述。
数据迁移
Codis的数据迁移有两种,分别是同步迁移和异步迁移,同步迁移的工作和Cluster差不多,都有bigkey的阻塞问题。而异步迁移重点就在于异步,我在RPC专栏中解释过同步异步、阻塞非阻塞等含义,异步面向的是服务端,接收请求的一端不马上处理请求而是直接返回,等完成后再返回真正结果,所以异步迁移的过程大家应该就很清晰了。异步迁移过程中源实例的这一批正在迁移的key是只读的,那我们自然想问如果写命令来了怎么办,还记得在将Redis数据持久化时,AOF的重写过程如果遇到增量命令怎么办,那时候Redis的做法是设立缓冲区对吧,在这里codis比较粗暴直接报错,交给proxy进行重试就可以了。
小结
这一节我们大致了解了一下Redis的几种经典集群模式,由于篇幅所限(其实是我写不动了),就到这了。但是还有很多可以拿出来讲讲的东西,比如往大了看从槽到实例映射其实是负载均衡的体现,去中心化和中心化的理念以及什么什么变更了客户端怎么知道其实是分布式中状态变更与通知的问题,所以后面有时间在番外篇中我们可以从更宽阔的角度看看Redis,毕竟专栏的目标就是,不止步于get/set。
参考文献
-
https://redis.io/docs/management/scaling/
-
https://codechina.gitcode.host/programmer/distributed-DB/5-Redis-Cluster.html#redis-cluster-%E6%8E%A2%E7%B4%A2%E4%B8%8E%E6%80%9D%E8%80%83