作者:斜阳
高可用架构演进背景
在分布式系统中不可避免的会遇到网络故障,机器宕机,磁盘损坏等问题,为了向用户不中断且正确的提供服务,要求零碎有肯定的冗余与容错能力。RocketMQ 在日志,统计分析,在线交易,金融交易等丰盛的生产场景中施展着至关重要的作用,而不同环境对基础设施的老本与可靠性提出了不同的诉求。在 RocketMQ v4 版本中有两种支流高可用设计,别离是主备模式的无切换架构和基于 Raft 的多正本架构(图中左侧和右侧所示)。生产实践中咱们发现,两正本的冷备模式下备节点资源利用率低,主宕机时非凡类型音讯存在可用性问题;而 Raft 高度串行化,基于多数派的确认机制在扩大只读正本时不够灵便,无奈很好的反对两机房对等部署,异地多核心等简单场景。RocketMQ v5 版本交融了上述计划的劣势,提出 DLedger Controller 作为管控节点(两头局部所示),将选举逻辑插件化并优化了数据复制的实现。
如何实现高可用零碎
正本组与数据分片
在 Primary-Backup 架构的分布式系统中,一份数据将被复制成多个副原本防止数据失落。解决雷同数据的一组节点被称为正本组(ReplicaSet),正本组的粒度能够是单个文件级别的(例如 HDFS),也能够是分区级 / 队列级的(例如 Kafka),每个实在存储节点上能够包容若干个不同正本组的正本,也能够像 RocketMQ 一样粗粒度的独占节点。独占可能显著简化数据写入时确保长久化胜利的复杂度,因为每个正本组上只有主正本会响应读写申请,备机个别配置只读来提供平衡读负载,选举这件事儿等价于让正本组内一个正本持有独占的写锁。
RocketMQ 为每个存储数据的 Broker 节点配置 ClusterName,BrokerName 标识来更好的进行资源管理。多个 BrokerName 雷同的节点形成一个正本组。每个正本还领有一个从 0 开始编号,不反复也不肯定间断的 BrokerId 用来示意身份,编号为 0 的节点是这个正本组的 Leader / Primary / Master,故障时通过选举来从新对 Broker 编号标识新的身份。例如 BrokerId = {0, 1, 3},则 0 为主,其余两个为备。
一个正本组内,节点间共享数据的形式有多种,资源的共享水平由低到高来说个别有 Shared Nothing,Shared Disk,Shared Memory,Shared EveryThing。典型的 Shared Nothing 架构是 TiDB 这类纯分布式的数据库,TiDB 在每个存储节点上应用基于 RocksDB 封装的 TiKV 进行数据存储,下层通过协定交互实现事务或者 MVCC。相比于传统的分库分表策略来说,TiKV 易用性和灵便水平很高,更容易解决数据热点与伸缩时数据打散的一系列问题,但实现跨多节点的事务就须要波及到屡次网络的通信。另一端 Shared EveryThing 的案例是 AWS 的 Aurora,Aliyun 的 PolarStore,旁路 Kernal 的形式使利用齐全运行于用户态,以最大水平的存储复用来缩小资源耗费,一主多备齐全共用一份底层牢靠的存储,实现一写多读,疾速切换。
大多数 KV 操作都是通过关键字的一致性哈希来计算所调配的节点,当这个节点所在的主正本组产生存储抖动,主备切换,网络分区等状况下,这个分片所对应的所有键都无奈更新,部分会有一些操作失败。音讯零碎的模型有所不同,流量大但跨正本组的数据交互极少,无序音讯发送到预期分区失败时还能够向其余正本组(分片)写入,一个正本组的故障不影响全局,这在整体服务的层面上额定提供了跨正本组的可用性。此外,思考到 MQ 作为 Paas 层产品,被宽泛部署于 Windows,Linux on Arm 等各种环境,只有缩小和 Iaas 层产品的深度绑定,能力提供更好的灵活性。这种部分故障隔离和轻依赖的个性是 RocketMQ 选则 Shared Nothing 模型重要起因。
正本组中,各个节点解决的速度不同,也就有了日志水位的概念。Master 和与其差距不大的 Slave 独特组成了同步正本集(SyncStateSet)。如何定义差距不大呢?掂量的指标能够是日志水位(文件大小)差距较小,也能够是备落后的工夫在肯定范畴内。在主宕机时,同步正本集中的其余节点有机会被晋升为主,有时须要对系统进行容灾演练,或者对某些机器进行保护或灰度降级时心愿定向的切换某一个正本成为新主,这又产生了优先正本(PriorityReplica)的概念。抉择优先正本的准则和策略很多,能够动静抉择水位最高,退出工夫最久或 CommitLog 最长的正本,也能够反对机架,可用区优先这类动态策略。
从模型的角度来看,RocketMQ 单节点上 Topic 数量较多,如果像 kafka 以 topic / partition 粒度保护状态机,节点宕机会导致上万个状态机切换,这种惊群效应会带来很多潜在危险,因而 v4 版本时 RocketMQ 抉择以单个 Broker 作为切换的最小粒度来治理,相比于其余更细粒度的实现,正本身份切换时只须要重调配 Broker 编号,对元数据节点压力最小。因为通信的数据量少,能够放慢主备切换的速度,单个正本下线的影响被限度在正本组内,缩小治理和运维老本。这种实现也一些毛病,例如存储节点的负载无奈以最佳状态在集群上进行负载平衡,Topic 与存储节点自身的耦合度较高,程度扩大个别会扭转分区总数,这就须要在下层附加额定的解决逻辑。
为了更标准更精确的掂量正本组的可用性指标,学术上就引入了几个名词:
- RTO(Recovery Time Objective)复原工夫指标,个别示意业务中断到复原的工夫。
- RPO(Recovery Point Object)复原点指标,用于掂量业务连续性。例如某个硬盘每天备份,故障时失落最近备份后的所有更新。
- SLA(Service-Level Agreement)服务等级协定,厂商以合约的模式对用户进行服务质量承诺,SLA 越高通常老本也越高。
节点数量与可靠性关系密切,依据不同生产场景,RocketMQ 的一个正本组可能会有 1,2,3,5 个正本。
- 单正本老本最低,保护最简略,宕机时其余正本组接管新音讯的写入,但已写入的数据无奈读取,造成局部音讯生产提早。底层硬件故障还可能导致数据永恒失落,个别用于非关键日志,数据采集等低可靠性老本诉求较强的场景。
- 两正本较好的衡量了数据冗余的老本与性能,RocketMQ 跨正本组容灾的个性使得两正本模式实用于绝大部分 IOPS 比拟高的场景。此时备机能够摊派肯定的读压力(尤其是主正本因为内存缓和或者产生冷读时)。两正本因为不满足多数派(quorum)准则,没有内部零碎的参加时,故障时无奈进行选举切换。
- 三正本和五正本是业界应用最为宽泛的,精心设计的算法使得少数状况下零碎能够自愈。基于 Paxos / Raft 属于就义高可用性来保障一致性的 CP 型设计,存储老本很高,容易受到 IO 散布不平均和水桶效应的影响。每条数据都须要半数以上正本响应的设计在须要写透(write through)多正本的音讯场景下不够灵便。
日志复制还是音讯复制
如何保障正本组中数据的最终一致性?那必定是通过数据复制的形式实现,咱们该抉择逻辑复制还是物理复制呢?
逻辑复制: 应用音讯来进行同步的场景也很多,各种 connector 实现实质上就是把音讯从一个零碎挪到另外一个零碎上,例如将数据导入导出到 ES,Flink 这样的零碎上进行剖析,依据业务须要抉择特定 Topic / Tag 进行同步,灵便水平和可扩展性十分高。这种计划随着 Topic 增多,零碎还会有服务发现,位点和心跳治理等下层实现造成的性能损失。因而对于音讯同步的场景,RocketMQ 也反对以音讯路由的模式进行数据转移,将音讯复制作为业务生产的特例来对待。
物理复制: 赫赫有名的 MySQL 对于操作会记录逻辑日志(bin log)和重做日志(redo log)两种日志。其中 bin log 记录了语句的原始逻辑,比方批改某一行某个字段,redo log 属于物理日志,记录了哪个表空间哪个数据页改了什么。在 RocketMQ 的场景下,存储层的 CommitLog 通过链表和内核的 MappedFile 机制形象出一条 append only 的数据流。主正本将未提交的音讯按序传输给其余正本(相当于 redo log),并依据肯定规定计算确认位点(confirm offset)判断日志流是否被提交。这种计划仅应用一份日志和位点就能够保障主备之间预写日志的一致性,简化复制实现的同时也进步了性能。
为了可用性而设计的多正本构造,很显著是须要对所有须要长久化的数据进行复制的,抉择物理复制更加节俭资源。RocketMQ 在物理复制时又是如何保证数据的最终一致性呢?这就波及到数据的水位对齐。对于音讯和流这样近似 FIFO 的零碎来说,越近期的音讯价值越高,音讯零碎的正本组的单个节点不会像数据库系统一样,保留这个正本的全量数据,Broker 一方面一直的将冷数据规整并转入低频介质来节约老本,同时对热数据盘上的数据也会由远及近滚动删除。如果正本组中有正本宕机较久,或者在备份重建等场景下就会呈现日志流的不对齐和分叉的简单状况。在下图中咱们将主节点的 CommitLog 的首尾位点作为参考点,这样就能够划分出三个区间。在下图中以蓝色箭头示意。排列组合一下就能够证实备机此时的 CommitLog 肯定满足下列 6 种状况之一。
上面对每种状况进行探讨与剖析:
- 1-1 状况下满足备 Max <= 主 Min,个别是备新上线或下线较久,备跳过存量日志,从主的 Min 开始复制。
- 1-2,2-2 两种状况下满足 主 Min < 备 Max <= 主 Max,个别是因为备网络闪断导致日志水位落后,通过 HA 连贯追寻主即可。
- 1-3,2-3 两种状况下备 Max > 主 Max,可能因为主异步写磁盘宕机后又成为主,或者网络分区时双主写入造成 CommitLog 分叉。因为新主落后于备,大量未确认的音讯失落,非正常模式的选举(RocketMQ 将这种状况称为 unclean 选举)是应该尽量避免的。
- 3-3 实践上不会呈现,备的数据长于主,起因可能是主节点数据失落又叠加了非正常选举,因而这种状况须要人工染指解决。
租约与节点身份变更
前文提到 RocketMQ 每个正本组的主正本才承受内部写申请,节点的身份又是如何决定的呢?
分布式系统个别分为中心化架构和去中心化架构。对于 MultiRaft,每个正本组蕴含三个或者五个正本,正本组内能够通过 Paxos / Raft 这样的共识协定来进行选主。典型的中心化架构,为了节俭数据面资源老本会部署两正本,此时依赖于内部 ZK,ETCD,或者 DLedger Controller 这样的组件作为核心节点进行选举。由外置组件裁决成员身份波及到分布式中两个重要的问题:1. 如何判断节点的状态是否失常。2. 如何防止双主问题。
对于第一个问题,kubernetes 的解决方案绝对优雅,k8s 对与 Pod 的健康检查包含存活检测(Liveness probes)和就绪检测(Readiness probes),Liveness probes 次要是探测利用是否还活着,失败时重启 Pod。Readiness probes 来判断探测利用是否承受流量。简略的心跳机制个别只能实现存活检测,来看一个例子:假如有正本组中有 A、B、C 三个正本,另有一个节点 Q(哨兵)负责观测节点状态,同时承当了全局选举与状态保护的职责。节点 A、B、C 周期性的向 Q 发送心跳,如果 Q 超过一段时间(个别是两个心跳距离)收不到某个节点的心跳则认为这个节点异样。如果异样的是主正本,Q 将正本组的其余正本晋升为主并播送告知其余正本。
在工程实际中,节点下线的可能性个别要小于网络抖动的可能性。咱们假如节点 A 是正本组的主,节点 Q 与节点 A 之间的网络中断。节点 Q 认为 A 异样。从新抉择节点 B 作为新的 Master,并告诉节点 A、B、C 新的 Master 是节点 B。节点 A 自身工作失常,与节点 B、C 之间的网络也失常。因为节点 Q 的告诉事件达到节点 A、B、C 的程序是未知的,如果先达到 B,在这一时刻,零碎中同时存在两个工作的主,一个是 A,另一个是 B。如果此时 A、B 都接管内部申请并与 C 同步数据,会产生重大的数据谬误。上述 “ 双主 ” 问题呈现的起因在于尽管节点 Q 认为节点 A 异样,但节点 A 本人不认为本人异样,在旧主新主都承受写入的时候就产生了日志流的分叉,其问题的实质是因为网络分区造成的零碎对于节点状态没有达成统一。
租约是一种防止双主的无效伎俩,租约的典型含意是当初核心节点抵赖哪个节点为主,并容许节点在租约有效期内失常工作。如果节点 Q 心愿切换新的主,只需期待前一个主的租约过期,则就能够平安的颁发新租约给新 Master 节点,而不会呈现双主问题。这种状况下系统对 Q 自身的可用性诉求十分高,可能会成为集群的性能瓶颈。生产中应用租约还有很多实现细节,例如依赖时钟同步须要颁发者的有效期设置的比接收者的略大,颁发者自身的切换也较为简单。
在 RocketMQ 的设计中,心愿以一种去中心化的设计升高核心节点宕机带来的全局危险,(这里认为中心化和是否存在核心节点是两件事)所以没有引入租约机制。在 Controller(对应于 Q)解体复原期间,因为 Broker 对本人身份会进行永恒缓存,每个主正本会治理这个正本组的状态机,RocketMQ Dledger Controller 这种模式可能尽量保障在大部分正本组在哨兵组件不可用时依然不影响收发音讯的外围流程。而旧主因为永恒缓存身份,无奈降级导致了网络分区时零碎必须容忍双主。产生了多种解决方案,用户能够通过预配置抉择 AP 型可用性优先,即容许零碎通过短时分叉来保障服务连续性(下文还会持续谈谈为什么音讯零碎中分叉很难防止),还是 CP 型一致性优先,通过配置最小正本 ack 数超过集群半数以上节点。此时发送到旧主的音讯将因为无奈通过 ha 链路将数据发送给备,向客户端返回超时,由客户端将发动重试到其余分片。客户端经验一个服务发现的周期之后,客户端就能够正确发现新主。
特地的,在网络分区的状况下,例如旧主和备,Controller 之间产生网络分区,此时因为没有引入租约机制,旧主不会主动降级,旧主能够配置为异步双写,每一条音讯须要通过主备的双重确认能力向客户端返回胜利。而备在切换为主时,会设置本人只须要单个正本确认的同步写盘模式。此时,客户端短时间内依然能够向旧主发送音讯,旧主须要两正本确认能力返回胜利,因而发送到旧主的音讯会返回 SLAVE_NOT_AVAILABLE 的超时响应,通过客户端重试将音讯发往新的节点。几秒后,客户端从 NameServer / Controller 获取新的路由时,旧主从客户端缓存中移除,此时实现了备节点的晋升。
外置的组件能够对节点身份进行调配,上图展现了一个两正本的正本组上线流程:
- 多个 Controller 通过选举和对 Broker 的申请进行重定向,最终由一个 Controller 做为主节点进行身份调配。
- 如果 RocketMQ 正本组存在多个正本且须要选主,节点默认以备的身份启动,备节点会将本人注册到 Controller。
- 节点从 Controller 获取 BrokerMemberGroup,蕴含了这个正本组的形容和连贯信息。
<!—->
-
- 若调配的身份为备,解析出主节点的对外服务的地址并连贯,实现日志截断后进行 HA 同步。
- 若调配的身份为主,期待备机连贯到本身的 HA 端口,并向 NameServer 再次宣告本人是主节点。
<!—->
- 主节点保护整个正本组的信息,向备发动数据复制,周期性的向 Controller 汇报主备之间水位差距,复制速度等。
RocketMQ 弱依赖 Controller 的实现并不会突破 Raft 中每个 term 最多只有一个 leader 的假如,工程中个别会应用 Leader Lease 解决脏读的问题,配合 Leader Stickiness 解决频繁切换的问题,保障主的唯一性。
- Leader Lease: 租约,上一任 Leader 的 Lease 过期后,期待一段时间再发动 Leader 选举。
- Leader Stickiness:Leader Lease 未过期的 Follower 回绝新的 Leader 选举申请。
注:Raft 认为具备最新已提交的日志的节点才有资格成为 Leader,而 Multi-Paxos 无此限度。
对于日志的连续性问题,Raft 在确认一条日志之前会通过位点查看日志连续性,若查看到日志不间断会回绝此日志,保障日志连续性,Multi-Paxos 容许日志中有空洞。Raft 在 AppendEntries 中会携带 Leader 的 commit index,一旦日志造成多数派,Leader 更新本地的 commit index(对应于 RocketMQ 的 confirm offset)即实现提交,下一条 AppendEntries 会携带新的 commit index 告诉其它节点,Multi-Paxos 没有日志连接性假如,须要额定的 commit 音讯告诉其它节点。
计算日志分叉位点
除了网络分区,很多状况导致日志数据流分叉。有如下案例:三正本采纳异步复制,异步长久化,A 为旧主 B C 为备,切换霎时 B 日志水位大于 C,此时 C 成为新主,B C 正本上的数据会产生分叉,因为 B 还多出了一段未确认的数据。那么 B 是如何以一个简略牢靠的办法去判断本人和 C 数据分叉的位点?
一个直观的想法就是,间接将主备的 CommitLog 从前向后逐步字节比拟,个别生产环境下,主备都有数百 GB 的日志文件流,读取和传输大量数据的计划费时费力。很快咱们发现,确定两个大文件是否雷同的一个好方法就是比拟数据的哈希值,须要比照的数据量一下子就从数百 GB 升高为了几百个哈希值,对于第一个不雷同的 CommitLog 文件,还能够采取部分哈希的形式对齐,这里依然存在一些计算的代价。还有没有优化的空间呢,那就是利用任期 Epoch 和偏移量 StartOffset 实现一个新的截断算法。这种 Epoch-StartOffset 满足如下准则:
- 通过共识协定保障给定的一个任期 Epoch 只有一个 Leader。
- 只有 Leader 能够写入新的数据流,满足肯定条件才会被提交。
- Follower 只能从 Leader 获取最新的数据流,Follower 上线时依照选举算法进行截断。
上面是一个选举截断的具体案例,选举截断算法思维和流程如下:
主 CommitLog Min = 300,Max = 2500,EpochMap = {<6, 200>, <7, 1200>, <8,2500>} 备 CommitLog Min = 300,Max = 2500,EpochMap = {<6, 200>, <7, 1200>, <8,2250>}
- 备节点连贯到主节点进行 HA 协商,获取主节点的 Epoch-StartOffset 信息并比拟
- 备从后向前找到任期 - 起始点雷同的那个点作为分叉任期,在上述案例里是 <8,2250>
- 抉择这个任期里主备完结位点的最小值(如果主正本没有切换且为最大任期,则主正本的完结位点是无穷大)
实现的代码如下:
public long findLastConsistentPoint(final EpochStore compareEpoch) {
long consistentOffset = -1L;
final Map<Long, EpochEntry> descendingMap = new TreeMap<>(this.epochMap).descendingMap();
for (Map.Entry<Long, EpochEntry> curLocalEntry : descendingMap.entrySet()) {final EpochEntry compareEntry = compareEpoch.findEpochEntryByEpoch(curLocalEntry.getKey());
if (compareEntry != null &&
compareEntry.getStartOffset() == curLocalEntry.getValue().getStartOffset()) {consistentOffset = Math.min(curLocalEntry.getValue().getEndOffset(), compareEntry.getEndOffset());
break;
}
}
return consistentOffset;
}
数据回发与日志截断
故障产生后,零碎将会对分叉数据进行修复,有很多小小细节值得深究与探讨。
在实现数据截断的过程中,有一个很非凡的动作,当备切主的时候要把 ConsumeQueue 的 Confirm Offset 晋升到 CommitLog 的 MaxPhyOffset,即便这一部分数据在主上是否被提交是未知的。回想起几年前看 Raft 的时候,当一条日志被传输到 Follower,Follower 确认收到这条音讯,主再把这条日志利用到本人的状态机时,告诉客户端和告诉所有的 follower 去 commit 这条日志这两件事是并行的,如果 leader 先回复 client 解决胜利,此时 leader 挂了,因为其余 follower 的确认位点 confirm offset 个别会略低于 leader,两头这段未决日志还没利用到 follower 的状态机上,这时就呈现了状态机不统一的状况,即曾经写入 leader 的数据失落了。让咱们来举一个具体的案例,假如两正本一主一备:
- 主的 max offset = 100,主向备发送以后 confirm offset = 40 和 message buffer = [40-100] 的数据
- 备向主回复 confirm offset = 100 后主须要同时做几件事
<!—->
-
- 本地提交(apply)[40-100] 区间的数据,用后盾的 dispatch 线程异步构建这段数据的索引
- 向 producer 响应 [40-100] 这段数据是发送胜利的。
- 向多个备机异步的提交,实际上是发送了 confirm offset = 100
<!—->
- 此时主忽然宕机,备机的 confirm offset 可能是 [40-100] 中的值
所以当备切换为主的时候,如果间接以 40 进行截断,意味着客户端曾经发送到服务端的音讯失落了,正确的水位应该被晋升至 100。然而备还没有收到 2.3 的 confirm = 100 的信息,这个行为相当于要提交了未决音讯。事实上新 leader 会恪守 “Leader Completeness” 的约定,切换时任何正本都不会删除也不会更改旧 leader 未决的 entry。新 leader 在新的 term 下,会间接利用一个较大的版本将未决的 entry 一起提交,这里正本组主备节点的行为独特保障了复制状态机的安全性。
那么备切换胜利的标记是什么,什么时候能力接管 producer 新的流量呢?对于 Raft 来说一旦切换就能够,对于 RocketMQ 来说这个阶段会被稍稍推延,即索引曾经齐全构建完结的时候。RocketMQ 为了保障构建 consume queue 的一致性,会在 CommitLog 中记录 consume queue offset 的偏移量,此时 confirm offset 到 max offset 间的数据是正本作为备来接管的,这部分音讯在 consume queue 中的偏移量曾经固定下来了,而 producer 新的流量时因为 RocketMQ 预计算位点的优化,等到音讯理论放入 CommitLog 的再实在的数据散发(dispatch)的时候就会发现对应地位的 consume queue 曾经被占用了,此时就造成了主备索引数据不统一。实质起因是 RocketMQ 存储层预构建索引的优化对日志有一些侵入性,但切换时短暂期待的代价远远小于失常运行时提速的收益。
消息中间件场景
a. 元数据变更是否依赖于日志
目前 RocketMQ 对于元数据是在内存中独自治理的,备机距离 5 秒向以后的主节点同步数据。例如以后主节点上创立了一个长期 Topic 并承受了一条音讯,在一个同步周期内这个 Topic 又被删除了,此时主备节点元数据可能不统一。又比方位点更新的时候,对于单个队列而言,多正本架构中存在多条生产位点更新链路,Consumer 拉取音讯时更新,Consumer 被动向 broker 更新,管控重置位点,HA 链路更新,当正本组产生主备切换时,consumer group 同时产生 consumer 高低线,因为路由发现的时间差,还可能造成同一个生产组两个不同 consumer 别离生产同一正本组主备上同一个队列的状况。
起因在于备机重做元数据更新和音讯流这两件事是异步的,这有肯定概率会造成脏数据。因为 RocketMQ 单个节点上 Topic / Group 数量较多,通过日志的实现会导致长久化的数据量很大,在简单场景下基于日志做回滚依赖 snapshot 机制也会减少计算开销和复原工夫。这个问题和数据库很像,MySQL 在执行 DDL 批改元数据时通过会创立 MDL 锁,阻塞用户其余操作拜访表空间的拜访。备库同步主库也会加锁,元数据批改开始点和完结点所代表的两个日志并不是一个原子操作,这意味着主库上在批改元数据的过程中如果宕机了,备库上持有的 MDL 锁就无奈开释。MySQL 的解决方案是在主库每次解体复原后,都写一条非凡的日志,告诉所有连贯的备库开释其持有的所有 MDL 排他锁。对所有操作都走日志流进行状态机复制要求存储层有多种日志类型,实现也更加简单。RocketMQ 抉择以另一种同步的模式操作,即相似 ZAB 这样二阶段协定,例如位点更新时的能够抉择配置 LockInStrictMode 让备都同步这条批改。事实上 RocketMQ 为了优化上述位点跳跃的景象,客户端在未重启时,遇到服务端主备切换还会用优先驳回本位置点的形式获取音讯,进一步缩小反复生产。
b. 同步复制与异步复制
同步复制的含意是用户的一个操作在多个正本上都曾经提交。失常状况下,假如一个正本组中的 3 个正本都要对雷同一个申请进行确认,相当于数据写透 3 个正本(简称 3-3 写),3-3 写提供了十分高的数据可靠性,然而把所有从节点都配置为同步复制时任何一个同步节点的中断都会导致整个正本组解决申请失败。当第三个正本是跨可用区时,长尾也会带来肯定的提早。
异步复制模式下,尚未复制到从节点的写申请都会失落。向客户端确认的写操作也无奈保障被长久化。异步复制是一种故障时 RPO 不为 0 的配置模式,因为不必思考从节点上的状态,总是能够持续响应写申请,零碎的提早更低,吞吐性能更好。为了衡量两者,通常只有其中一个从节点是同步的,而其余节点是异步的模式。只有同步的从节点变得不可用或性能降落,则将另一个异步的从节点晋升为同步模式。这样能够保障至多有两个节点(即主节点和一个同步从节点)领有最新的数据正本。这种模式称为 2-3 写,能帮忙防止抖动,提供更好的提早稳定性,有时候也叫称为半同步。
在 RocketMQ 的场景中,异步复制也被广泛应用在音讯读写比极高,从节点数量多或者异地多正本场景。同步复制和异步复制是通过 Broker 配置文件里的 brokerRole 参数进行设置的,这个参数能够被设置成 ASYNC_MASTER、SYNC_MASTER、SLAVE 三个值中的一个。理论利用中要联合业务场景正当设置长久化形式和主从复制形式,通常,因为网络的速度高于本地 IO 速度,采纳异步长久化和同步复制是一个衡量性能与可靠性的设置。
c. 正本组自适应降级
同步复制的含意是一条数据同时被主备确认才返回用户操作胜利,能够保障主宕机后音讯还在备中,适宜可靠性要求较高的场景,同步复制还能够限度未同步的数据量以缩小 ha 链路的内存压力,毛病则是正本组中的某一个备呈现假死就会影响写入。异步复制无需期待备确认,性能高于同步复制,切换时未提交的音讯可能会失落(参考前文的日志分叉)。在三正本甚至五正本且对可靠性要求高的场景中无奈采纳异步复制,采纳同步复制须要每一个正本确认后才会返回,在正本数多的状况下重大影响效率。对于一条音讯须要被多少正本确认这个问题,RocketMQ 服务端会有一些数量上的配置来进行灵便调整:
- TotalReplicas:全副正本数
- InSyncReplicas:每条音讯至多要被这个数量的 Broker 确认(如果主为 ASYNC_MASTER 或者 AllAck 模式则该参数不失效)
- MinInSyncReplicas:最小的同步正本数,如果 InSyncReplicas < MinInSyncReplicas 会对客户端疾速失败
- AllAckInSyncStateSet:主确认长久化胜利,为 true 示意须要 SyncStateSet 中所有备确认。
因而,RocketMQ 提出了正本组在同步复制的模式下,也能够反对正本组的自适应降级(参数名称为 enableAutoInSyncReplicas)来适配音讯的非凡场景。当正本组中存活的正本数缩小或日志流水位差距过大时进行主动降级,最小降级到 minInSyncReplicas 正本数。比方在两正本下配置。对于失常状况下,两个正本会处于同步复制,当备下线或假死时,会进行自适应降级,保障主节点还能失常收发音讯,这个性能为用户提供了一个可用性优先的抉择。
d. 轻量级心跳与疾速隔离
在 RocketMQ v4.x 版本的实现中,Broker 周期性的(距离 30 秒)将本身的所有 Topic 序列化并传输到 NameServer 注册进行保活。因为 Broker 上 Topic 的元数据规模较大,带来了较大的网络流量开销,Broker 的注册距离不能设置的太短。同时 NameServer 对 Broker 是采取提早隔离机制,避免 NameServer 网络抖动时可能霎时移除所有 Broker 的注册信息,引发服务的雪崩。默认状况下异样主宕机时超过 2 分钟,或者备切换为主从新注册后才会替换。容错设计的同时导致 Broker 故障转移迟缓,RocketMQ v5.0 版本引入轻量级心跳(参数 liteHeartBeat),将 Broker 的注册行为与 NameServer 的心跳进行了逻辑拆分,将心跳距离减小到 1 秒。当 NameServer 距离 5 秒(可配置)没有收到来自 Broker 的心跳申请就对 Broker 进行移除,使异样场景下自愈的工夫从分钟级缩短到了秒级。
RocketMQ 高可用架构演进路线
无切换架构的演进
最早的时候,RocketMQ 基于 Master-Slave 模式提供了主备部署的架构,这种模式提供了肯定的高可用能力,在 Master 节点负载较高状况下,读流量能够被重定向到备机。因为没有选主机制,在 Master 节点不可用时,这个正本组的音讯发送将会齐全中断,还会呈现提早音讯、事务音讯、Pop 音讯等二级音讯无奈生产或者提早。此外,备机在失常工作场景下资源使用率较低,造成肯定的资源节约。为了解决这些问题,社区提出了在一个 Broker 过程内运行多个 BrokerContainer,这个设计相似于 Flink 的 slot,让一个 Broker 过程上能够以 Container 的模式运行多个节点,复用传输层的连贯,业务线程池等资源,通过单节点主备穿插部署来同时承当多份流量,无内部依赖,自愈能力强。这种形式下隔离性弱于应用原生容器形式进行隔离,同时因为架构的复杂度减少导致了自愈流程较为简单。
切换架构的演进
另一条演进路线则是基于可切换的,RocketMQ 也尝试过依靠于 Zookeeper 的分布式锁和告诉机制进行 HA 状态的治理。引入内部依赖的同时给架构带来了复杂性,不容易做小型化部署,部署运维和诊断的老本较高。另一种形式就是基于 Raft 在集群内主动选主,Raft 中的正本身份被透出和复用到 Broker Role 层面去除内部依赖,然而强统一的 Raft 版本并未反对灵便的降级策略,无奈在 C 和 A 之间灵便调整。两种切换计划都是 CP 设计,就义高可用优先保障一致性。主正本下线时选主和路由定时更新策略导致整个故障转移工夫仍然较长,Raft 自身对三正本的要求也会面临较大的老本压力,RocketMQ 原生的 TransientPool,零拷贝等一些用来防止缩小 IO 压力的计划在 Raft 下无奈无效应用。
RocketMQ DLedger 交融模式
RocketMQ DLedger 交融模式是 RocketMQ 5.0 演进中联合上述两条路线后的一个零碎的解决方案。外围的个性有以下几点:
- 利用可内嵌于 NameServer 的 Controller 进行选主,无内部依赖,对两正本反对敌对。
- 引入 Epoch-StartOffset 机制来计算日志分叉位点。
- 音讯在进行写入时,提供了灵便的配置来协调系统对于可用性还是一致性优先的诉求。
- 简化日志复制协定使得日志复制为高效。
几种实现比照表如下:
与其余音讯零碎的比照
管制节点
- 是否强制要求选主
Kafka 的 Controller 是 Broker 选举产生,这须要有一个存储节点间的服务发现机制。RocketMQ 的 Controller 能够作为管控节点独自存在。对 Kafka,Pulsar 而言必须抉择主正本进行写入,随着工夫的运行节点之间负载须要通过简单的计划进行再平衡。对 RocketMQ 的交融架构而言,因为选主是可选的,动态布局的计划(例如无需依赖简单的动静调度就能够较为平衡的实现跨机架跨可用区),并且无切换与切换架构能够互相转换。 - Controller 的逻辑复杂度
RocketMQ Controller 相比 Kafka Controller 更加轻量,Kafka 的 Controller 承当 Partition 粒度的 ISR 保护和选举等性能,而 RocketMQ 的 Controller 保护的数据是正本组粒度的,对于元数据只保护节点身份,更加简略。RocketMQ Controller 能够独立部署,也能够内嵌 NameServer 运行。 - Controller 依赖水平
RocketMQ Broker 的同步正本集保护是 Master Broker 节点上报,因为不强依赖核心节点来提供租约,controller 宕机时尽管无奈为同时有主故障的正本组选举,但不影响绝大部分正本组可用性。Pulsar 中通过 fencing 机制避免有多个 writer(pulsar 中的计算节点称为 broker)同时写同一个 partition,是对外部有依赖的。
数据节点
- 正本存储构造的形象与最小粒度不同,在这一点上其实三者的设计各有劣势
-
- Kafka 的存储形象粒度是 Partition,对每个分区进行保护多正本,扩容须要进行数据复制,对于冷读反对更好。
- RocketMQ 的日志流是 Broker 粒度的,程序写盘效率更高,在磁盘空间有余时个别抉择程度扩容,只需复制元数据。
- Pulsar 其实形象了一个分布式日志流 Journal,分区被进一步分成分片,依据配置的工夫或大小进行滚动,扩容只需复制元数据。
- 简单的参数配置被收敛至服务端
Kafka 和 RocketMQ 都反对灵便的配置单条音讯的 ack 数,即衡量数据写入灵活性与可靠性。RocketMQ 在向云原生演进的过程心愿简化客户端 API 与配置,让业务方只需关怀音讯自身,抉择在服务端配置对立配置这个值。 - 正本数据的同步形式不同
Pulsar 采纳星型写:数据间接从 writer 写到多个 bookeeper。适宜客户端与存储节点混部场景。数据门路只须要 1 跳,提早更低。毛病是当存储计算拆散时,星型写须要更多的存储集群和计算集群间网络带宽。
RocketMQ 和 Kafka 采纳 Y 型写:client 先写到一个主正本,由其再转发给另外 Broker 正本。尽管服务端外部带宽富余,但须要 2 跳网络,会减少提早。Y 型写利于解决文件多客户端写的问题,也更容易利用 2-3 写克服毛刺,提供更好的提早稳定性。
高可用架构的将来
仔细阅读 RocketMQ 的源码,其实大家也会发现 RocketMQ 在各种边缘问题解决上细节满满,节点生效,网络抖动,正本一致性,长久化,可用性与提早之间存在各种轻微的衡量,这也是 RocketMQ 多年来在生产环境下所积攒的外围竞争力之一。随着分布式技术的进一步倒退,更多更有意思的技术,如基于 RDMA 网络的复制协定也跃然纸上。RocketMQ 将与社区协同提高,倒退为“音讯,事件,流”一体化的交融平台。
参考文档:
- Paxos design
https://lamport.azurewebsites.net/pubs/paxos-simple.pdf
- SOFA-JRaft
https://github.com/sofastack/sofa-jraft
- Pulsar Geo Replication
https://pulsar.apache.org/zh-CN/docs/next/concepts-replication
- Pulsar Metadata
https://pulsar.apache.org/zh-CN/docs/next/administration-meta…
- Kafka Persistence
https://kafka.apache.org/documentation/#persistence
- Kafka Balancing leadership
https://kafka.apache.org/documentation/#basic_ops_leader_balancing
- Windows Azure Storage: A Highly Available Cloud Storage Service with Strong Consistency
https://azure.microsoft.com/en-us/blog/sosp-paper-windows-azure-storage-a-highly-available-cloud-storage-service-with-strong-consistency/
- PolarDB Serverless: A Cloud Native Database for Disaggregated Data Centers
https://www.cs.utah.edu/~lifeifei/papers/polardbserverless-sigmod21.pdf
RocketMQ 学习社区体验地址
RocketMQ 学习社区重磅上线!AI 互动,一秒理解 RocketMQ 性能源码。RocketMQ 学习社区是国内首个基于 AIGC 提供的常识服务社区,旨在成为 RocketMQ 学习路上的“贴身小二”。
PS:RocketMQ 社区以 RocketMQ 5.0 材料为次要训练内容,继续优化迭代中,答复内容均由人工智能模型生成,其准确性和完整性无奈保障,且不代表 RocketMQ 学习社区的态度或观点。
点击此处,立刻体验 RocketMQ 学习社区(倡议 PC 端体验残缺性能)