共计 5440 个字符,预计需要花费 14 分钟才能阅读完成。
简介: 在异地多活的实现上,数据可能在三个及以上核心间进行双向同步,才是解决真正异地多活的核心技术所在。本文基于三核心且跨海内的场景,分享一种多核心容灾架构及实现形式,介绍几种分布式 ID 生成算法,以及在数据同步上最终一致性的实现过程。
一 背景
为什么称之为真正的异地多活?异地多活曾经不是什么陈腐词,但仿佛始终都没有实现真正意义上的异地多活。个别有两种模式:一种是利用部署在同城两地或多地,数据库一写多读(次要是为了保证数据一致性),当主写库挂掉,再切换到备库上;另一种是单元化服务,各个单元的数据并不是全量数据,一个单元挂掉,并不能切换到其余单元。目前还能看到双核心的模式,两个核心都是全量数据,但双跟多还是有很大差距的,这里其实次要受限于数据同步能力,数据可能在 3 个及以上核心间进行双向同步,才是解决真正异地多活的核心技术所在。
提到数据同步,这里不得不提一下 DTS(Data Transmission Service),最后阿里的 DTS 并没有双向同步的能力,起初有了云上版本后,也只限于两个数据库之间的双向同步,做不到 A <->B<->C 这种模式,所以咱们自研了数据同步组件,尽管不想反复造轮子,但也是没方法,前面会介绍一些实现细节。
再谈谈为什么要做多核心容灾,以我所在的 CDN& 视频云团队为例,首先是海内业务的须要,为了可能让海内用户就近拜访咱们的服务,咱们须要提供一个海内核心。但大多数业务还都是以国内为主的,所以国内要建双核心,避免外围库挂掉整个管控就都挂掉了。同时海内的环境比较复杂,一旦海内核心挂掉了,还能够用国内核心顶上。国内的双核心还有个十分大的益处是能够通过一些路由策略,扩散单核心零碎的压力。这种三个核心且跨海内的场景,应该是目前异地多活最难实现的了。
二 零碎 CAP
面对这种全球性跨地区的分布式系统,咱们不得不谈到 CAP 实践,为了可能多核心全量数据提供服务,Partition tolerance(分区容错性)是必须要解决的,然而依据 CAP 的实践,Consistency(一致性)和 Availability(可用性)就只能满足一个。对于线上利用,可用性自不用说了,那面对这样一个问题,最终一致性是最好的抉择。
三 设计准则
1 数据分区
抉择一个数据维度来做数据切片,进而实现业务能够离开部署在不同的数据中心。主键须要设计成分布式 ID 模式,这样当进行数据同步时,不会造成主键抵触。
上面介绍几个分布式 ID 生成算法。
SnowFlake 算法
1)算法阐明
+--------------------------------------------------------------------------+
| 1 Bit Unused | 41 Bit Timestamp | 10 Bit NodeId | 12 Bit Sequence Id |
+--------------------------------------------------------------------------+
- 最高位是符号位,始终为 0,不可用。
- 41 位的工夫序列,准确到毫秒级,41 位的长度能够应用 69 年。工夫位还有一个很重要的作用是能够依据工夫进行排序。
- 10 位的机器标识,10 位的长度最多反对部署 1024 个节点。
- 12 位的计数序列号,序列号即一系列的自增 ID,能够反对同一节点同一毫秒生成多个 ID 序号,12 位的计数序列号反对每个节点每毫秒产生 4096 个 ID 序号。
2)算法总结
长处:
- 齐全是一个无状态机,无网络调用,高效牢靠。
毛病:
- 依赖机器时钟,如果时钟谬误比方时钟回拨,可能会产生反复 Id。
- 容量存在局限性,41 位的长度能够应用 69 年,个别够用。
- 并发局限性,每毫秒单机最大产生 4096 个 Id。
- 只实用于 int64 类型的 Id 调配,int32 位 Id 无奈应用。
3)实用场景
个别的非 Web 应用程序的 int64 类型的 Id 都能够应用。
为什么说非 Web 利用,Web 利用为什么不能够用呢,因为 JavaScript 反对的最大整型就是 53 位,超过这个位数,JavaScript 将失落精度。
RainDrop 算法
1)算法阐明
为了解决 JavaScript 失落精度问题,由 Snowflake 算法革新而来的 53 位的分布式 Id 生成算法。
+--------------------------------------------------------------------------+
| 11 Bit Unused | 32 Bit Timestamp | 7 Bit NodeId | 14 Bit Sequence Id |
+--------------------------------------------------------------------------+
- 最高 11 位是符号位,始终为 0,不可用,解决 JavaScript 的精度失落。
- 32 位的工夫序列,准确到秒级,32 位的长度能够应用 136 年。
- 7 位的机器标识,7 位的长度最多反对部署 128 个节点。
- 14 位的计数序列号,序列号即一系列的自增 Id,能够反对同一节点同一秒生成多个 Id,14 位的计数序列号反对每个节点每秒单机产生 16384 个 Id。
2)算法总结
长处:
- 齐全是一个无状态机,无网络调用,高效牢靠。
毛病:
- 依赖机器时钟,如果时钟谬误比方时钟不同步、时钟回拨,会产生反复 Id。
- 容量存在局限性,32 位的长度能够应用 136 年,个别够用。
- 并发局限性,低于 snowflake。
- 只实用于 int64 类型的 Id 调配,int32 位 Id 无奈应用。
3)实用场景
个别的 Web 应用程序的 int64 类型的 Id 都根本够用。
分区独立调配算法
1)算法阐明
通过将 Id 分段调配给不同单元独立治理。同一个单元的不同机器再通过共享 redis 进行单元内的集中调配。
相当于每个单元事后调配了一批 Id,而后再由各个单元内进行集中式调配。
比方 int32 的范畴从 -2147483648 到 2147483647,Id 应用范畴 1,2100000000),前两位示意 region,则每个 region 反对 100000000(一亿)个资源,即 Id 组成格局能够示意为[0-20。
即 int32 位能够反对 20 个单元,每个单元反对一亿个 Id。
2)算法总结
长处:
- 区域之间无状态,无网络调用,具备牢靠唯一性
毛病:
- 分区容量存在局限性,须要事后评估业务容量。
- 从 Id 中无奈判断生成的先后顺序。
3)实用场景
实用于 int32 类型的 Id 调配,单个区域内容量下限可评估的业务应用。
集中式调配算法
1)算法阐明
集中式能够是 Redis,也能够是 ZooKeeper,也能够利用数据库的自增 Id 集中调配。
2)算法总结
长处:
- 全局递增
- 牢靠的唯一性 Id
- 无容量和并发量限度
毛病:
- 减少了零碎复杂性,须要强依赖核心服务。
3)实用场景
具备牢靠的核心服务的场景能够选用,其余 int32 类型无奈应用分区独立调配的业务场景。
总结
每一种调配算法都有各自的实用场景,须要依据业务需要抉择适合的调配算法。次要须要思考几个因素:
- Id 类型是 int64 还是 int32。
- 业务容量以及并发量需要。
- 是否须要与 JavaScript 交互。
2 核心关闭
尽量让调用产生在本核心,尽量避免跨数据中心的调用,一方面为了用户体验,本地调用 RT 更短,另一方面避免同一个数据在两个核心同时写入造成数据抵触笼罩。个别能够抉择一种或多种路由形式,如 ADNS 依据地区路由,通过 Tengine 依据用户属性路由,或者通过 sidecar 形式进行路由,具体实现形式这里就不开展说了。
3 最终一致性
后面两种其实就是为了最终一致性做铺垫,因为数据同步是就义了一部分实时的性能,所以咱们须要做数据分区,做核心关闭,这样能力保障用户申请的及时响应和数据的实时准确性。
后面提到了因为 DTS 反对的并不是很欠缺,所以我基于 DRC(一个阿里外部数据订阅组件,相似 canal)本人实现了数据同步的能力,上面介绍一下实现一致性的过程,两头也走了一些弯路。
程序接管 DRC 音讯
为了保障对于 DRC 音讯程序的接管,首先想到的是采纳单机生产的形式,而单机带来的问题是数据传输效率慢。针对这个问题,波及到并发的能力。大家可能会想到基于表级别的并发,然而如果单表数据变更大,同样有性能瓶颈。这里咱们实现了主键级别的并发能力,也就是说在同一主键上,咱们严格保序,不同主键之间能够并发同步,将并发能力又进步了 N 个数量级。
同时单机生产的第二个问题就是单点。所以咱们要实现 Failover。这里咱们采纳 Raft 协定进行多机选主以及对主的申请。当单机挂掉之后,其余的机器会主动选出新的 Leader 执行同步工作。
音讯跨单元传输
为了很好的反对跨单元数据同步,咱们采纳了 MNS(阿里云音讯服务),MNS 自身是个分布式的组件,无奈满足音讯的程序性。起初为了保障强一致性,我采纳音讯染色与还原的形式,具体实现见下图:
通过实际咱们发现,这种客户端排序并不牢靠,咱们的零碎不可能有限去期待一个音讯的,这里波及到最终一致性的问题,在第 3 点中持续探讨。其实对于程序音讯,RocketMQ 是有程序音讯的,然而 RocketMQ 目前还没有实现跨单元的能力,而单纯的就数据同步而言,咱们只有保障最终一致性就能够了,没有必要为了保障强一致性而就义性能。同时 MNS 音讯如果没有生产胜利,音讯是不会丢掉的,只有咱们去显示的删除音讯,音讯才会丢,所以最终这个音讯肯定会到来。
最终一致性
既然 MNS 无奈保障强程序,而咱们做的是数据同步,只有可能保障最终一致性就能够了。2012 年 CAP 实践提出者 Eric Brewer 撰文回顾 CAP 时也提到,C 和 A 并不是齐全互斥,倡议大家应用 CRDT 来保障一致性。CRDT(Conflict-Free Replicated Data Type)是各种根底数据结构最终统一算法的实践总结,能依据肯定的规定主动合并,解决抵触,达到强最终统一的成果。通过查阅相干材料,咱们理解到 CRDT 要求咱们在数据同步的时候要满足交换律、结合律和幂等律。如果操作自身满足以上三律,merge 操作仅须要对 update 操作进行回放即可,这种模式称为 op-based CRDT,如果操作自身不满足,而通过附带额定元信息可能让操作满足以上三律,这种模式称为 state-based CRDT。
通过 DRC 的拆解,数据库操作有三种:insert、update、delete,这三种操作不论哪两种操作都是不能满足交换律的,会产生抵触,所以咱们在并发级别(主键)加上额定信息,这里咱们采纳序号,也就是 2 中提到的染色的过程,这个过程是保留的。而主键之间是并发的,没有程序而言。当接管音讯的时候咱们并不保障强程序,采纳 LWW(Last Write Wins)的形式,也就是说咱们执行以后的 SQL 而放弃后面的 SQL,这样咱们就不必思考替换的问题。同时咱们会依据音讯的唯一性(实例 + 单元 + 数据库 +MD5(SQL))对每个音讯做幂等,保障每个 SQL 都不会反复执行。而对于结合律,咱们须要对每个操作独自剖析。
1)insert
insert 是不满足结合律的,可能会有主键抵触,咱们把 insert 语句变更 insert ignore,而收到 insert 操作阐明之前并不存在这样一条记录,或者后面有 delete 操作。而 delete 操作可能还没有到。这时 insert ignore 操作返回后果是 0,但这次的 insert 数据可能跟已有的记录内容并不统一,所以这里咱们将这个 insert 操作转换为 update 操作再执行一次。
2)update
update 操作人造满足结合律。然而这里又要思考一种非凡状况,那就是执行后果为 0。这阐明此语句之前肯定存在一个 insert 语句,但这个语句咱们还没有收到。这时咱们须要利用这条语句中的数据将 update 语句转成 insert 再从新执行一次。
3)delete
delete 也是人造满足结合律的,而无论之前都有什么操作,只有执行就好了。
在 insert 和 update 操作外面,都有一个转换的过程,而这里有个前提,那就是从 DRC 拿到的变更数据每一条都是全字段的。可能有人会说这里的转换能够用 replace into 替换,为什么没有应用 replace into 呢,首先因为程序错乱的状况毕竟是多数,而且咱们并不单纯复制数据,同时也是在复制操作,而对于 DRC 来说,replace into 操作会被解析为 update 或 insert。这样无奈保障音讯唯一性,也无奈做到防循环播送,所以并不举荐。咱们看看上面的流程图兴许会更清晰些:
四 容灾架构
依据下面的介绍,咱们来看下多核心容灾架构的状态,这里用了两级调度来保障核心关闭,同时利用自研的同步组件进行多核心双向同步。咱们还能够制订一些快恢策略,例如疾速摘掉一个核心。同时还有一些细节须要思考,例如在摘掉一个核心的过程中,在摘掉的核心数据还没有同步到其余核心的过程中,应该禁掉写操作,避免短时间呈现双写的状况,因为咱们同步的工夫都是毫秒级的,所以影响很小。
五 结束语
架构须要一直的演进,到底哪种更适宜你还须要具体来看,上述的多核心架构及实现形式欢送大家来探讨。
咱们的数据同步组件 hera-dts 已在 BU 外部进行应用,数据同步的逻辑还是比较复杂的,尤其是实现双向同步,其中波及到断点续传、Failover、防丢数据、防音讯重发、双向同步中防循环复制等十分多的细节问题。咱们的同步组件也是经验了一段时间的优化才达到稳固的版本。
作者:开发者小助手_LS
原文链接
本文为阿里云原创内容,未经容许不得转载