vlambda博客
学习文章列表

知道微服务架构下如何做数据复制吗?


关注Java后端技术栈

回复“面试”获取最新资料

回复“加群”邀您进技术交流群

前言

一个高可用的分布式系统,底层的存储也是需要高性能、高可用的。上一篇介绍了一些数据存储产品,如果数据存储服务都是单库的,那么在唯一的单库发生故障时,将导致上层的微服务系统也都无法正常运行。为了加强可用性,我们需要更多的数据库节点,一个节点挂了,可以快速切换到可用的节点提供服务。有更多的节点提供服务可以带来很多好处,如高可用、高性能,基于地理分布的数据中心可以提升用户访问速度。

为了提供更好的性能和可用性,也需要解决一些复杂的问题,尤其是多 Node 的数据复制问题。第一篇文章中有介绍到网络延迟的问题,在多节点间进行数据复制必然会遇到网络延迟,也需要能处理节点上下线、一致性问题。本篇内容将介绍分布式存储系统数据复制的问题和解决方案,以及以 MySQL 为例介绍一下主从复制模型,最后延伸介绍一下多数据中心使用的主主复制。

复制的模型

数据库的复制主要是为了让多个数据库实例的节点都保存同一份数据,数据的传输有同步和异步两种方式。假设我们有三个数据库节点,通过下图示意可以看到两种方式的不同。

同步数据复制

同步的数据复制一般是阻塞的,保证了所有的 Node 都保持最新的数据。不过同步复制的方案一般不会被实践使用,同步的方式对性能的牺牲是比较大的,而且可用性低,如果要保证强一致性,则会牺牲可用性,一个节点的故障,会导致集群的同步任务阻塞。

异步的数据复制

知道微服务架构下如何做数据复制吗?

异步的数据复制常用于主-从(有时也叫 Master-Slaves,Leaders-Followers)复制。客户端确认数据保存到主库之后即可返回,对于其他从库的节点是异步的方式同步的。异步复制缩小了响应时间,提高了服务系统的吞吐量,一致性方面保证最终一致性。

为了提高可用性,对于有多个从库的数据复制,可以同步异步结合。主库同步给一个从库,然后直接返回,跟主库实时同步的从库再跟其他从库间进行异步复制,这样可以保证当主库挂掉时,有一个从库可以保证有最新的数据。如果这个同步的从库挂了,可以将同步任务切换到其他正常运行的从库。这种同步保证了至少两个 Node 有最新的数据,进一步增强了一致性。虽然增强一致性听起来很美好,但大多数的数据库集群还是以“全异步”实现数据复制。在大的分布式集群的环境下,尤其是多数据中心,虽然异步复制有“弱持久化”的风险,但是可用性强,避免了同步复制阻塞在故障节点,而且对于网络延迟敏感型的系统来说,同步复制对系统的性能将会造成更大影响。

数据复制实现

无论是同步或是异步,不同节点的数据复制需要一些“媒介”,因为数据库复制不会像应用服务,提供个接口调用来进行同步异步。比较广泛使用的实现都是基于主库提供的复制日志(Replication Log),一般复制日志都是以二进制数据存储,有的文献也会叫二进制日志。

实现方式

日志复制的方式每个数据库可能会稍微有差别,主要的实现方式有以下几种:

(1)基于语句的复制(Statement-based Replication)

这种比较好理解,就是主库关于写入的语句,update、insert、delete 等都在持久化后同步到复制日志里,然后将语句执行的请求发送给其他从库按序执行,这种基于语句执行一般会引发一些问题,比如当执行语句中有调用本地资源的函数是,如 NOW(),通过第一篇文章可以了解到,时钟同步的问题,各个节点时间各不相同,所以会导致各个节点执行后时间列(如 utc_create)的数据不一致,MySQL 5.7.7 版本之前的 binlog 默认使用的是基于语句的复制。

(2)基于 WAL(Write-ahead log)

在第二篇文章中介绍了 WAL 一般用于 SSTables 结构的存储服务。B-tree 结构的服务也一样,所有“写”操作需要先写入到一个 write-ahead log 中,然后再执行 DB 的持久化。WAL 一般是以字节流存储,并且顺序存储所有“写”操作,关系型数据库中 PostgreSQL 使用 WAL 进行数据复制和故障恢复。WAL 也有不足,在需要进行数据库升级的时候,需要确保新版本的 DB 的复制协议能够向下兼容,这样就可以先升级从库,再升级主库来确保不停服务的滚动升级。如果 DB 不支持向下兼容的话,需要停止数据库服务来升级,以及启动后进行数据恢复。

(3)基于行的复制(Row-based Replication)

基于行的日志属于一种逻辑日志,不依赖存储引擎的版本,存储引擎可以在不停服务情况下进行滚动升级,不同的节点也可以运行着不同的引擎版本。逻辑日志中一般包含如下信息:

  • insert 语句所有插入的值信息

  • update 的主键以及更新(前)后的值

  • delete 包含唯一主键信息

当然基于 Row 复制也有一些缺点,比如对于批量更新操作,基于 Statement 的在日志中只需要一句,在基于 Row 的日志则需要很多行,占用的日志空间大,用来做备份、恢复的时间也会相对较长。所以基于 Row 的二进制日志适用于小的事务的业务存储。另外,Statement-based 和 WAL 的区别主要是写入的时间的不同。WAL 是在语句执行前先写入 log 确保了即使持久化执行失败也可以用WAL进行恢复;Statement-based 是在执行本地持久化之后再同步到日志中。

MySQL 的主从(Master-Slave)集群复制

关系型数据库,以 MySQL 为例,提供的数据复制是基于二进制日志(binlog)的异步复制实现,MySQL 提供了三种模式:Statement-based、Row-based(V5.1 版本之后提供)及 Mixed。Mixed 即同时有 Statement-based 和 Row-based 的混合模式,Mixed 默认使用 Statement 记录,当有一些 Statement 无法准确处理的函数时,会使用 Row 格式。MySQL 的 5.7.7 之后默认的 binlog 实现是基于 Row 格式的复制日志。具体使用哪种格式记录,需要结合服务的特点,两种日志对比的细节可以看 [Statement-Based 和 Row-Based 复制的利弊:

https://dev.mysql.com/doc/refman/5.7/en/replication-sbr-rbr.html

这篇文章。

MySQL 的复制任务主要是由以下三个线程完成。

(1)Master 的 Binlog dump 线程

Master 运行的线程,负责把 binlog 内容发送给 slave,如果是有 N 个在连接的 slaves,则会创建 N 个 binlog dump 线程分别处理复制任务。

(2)Slave 的 I/O 线程

在 Slave 上运行的线程,负责连接 Master 并且请求 Master 将更新的 binlog 记录发送过来,然后读取 Master 的 binlog dump 线程发送来的数据,复制到本地的一个中继日志(Relay Log)中。

(3)Slave 的 SQL 线程

Slave 创建的 SQL 线程用于读取 Relay log 中的记录,以及执行其中的事件任务。

下图简单示意了 MySQL 的主从复制流程:

知道微服务架构下如何做数据复制吗?

对于 Binlog 的一些优化参数可以详细查看 Binlog options,在 5.6 之后版本新增的 binlog_ row_image= minimal 参数可以让 binlog 的 Update 只记录影响后的行,一定程度也优化了 binlog 文件的大小。

在管理集群时,如果想查看当前延迟信息,可以通过 show slave statusshow master status 命令查看,主要观察 Slave 中的 Slave_IO_State 以及 Read_Master_Log_Pos(当前同步到的主库的 binlog 偏移量),对比 Master 的 binlog 偏移量信息,即可知道大概有多少 I/O 延迟。

分布式读写一致性问题

基于日志的异步复制提升了数据存储写数据的吞吐量,但在分布式的存储集群中,可能在同一时间点上,由于网络延迟等各种原因,不同数据节点读到的数据不同。所以尽管在单服务实例中的 InnoDB 支持事务的 ACID,但是在分布式集群中,使用异步复制的 ACID 也不再是绝对的。基于 Binlog 的实时复制提供的集群一致性是“最终一致性”,最终一致性简单可以理解为,如果不再往主库中写入数据,那么过一段时间,所有的数据库节点的数据将完全一致。

主从读一致性

对于主从集群,如果写主库、读从库,因为网络延迟所以一般无法读到最新的数据,可以通过一定方式,将一部分读请求到主库,如:

  • 通过业务场景区分,将修改数据的用户的读请求落在主库,比如个人中心,当读自己的个人中心时读主库,当读其他人的个人中心时读从库。

  • 通过时间区分,比如最近 N 分钟更新的数据从主库读,一定时间之前的数据读从库,不过需要不断监控从库的同步进度,是否可以控制在这段时间之内。

  • 如果是跨地域的多数据中心,需要尽量将用户读写请求路由到同一数据中心的主库。

还有另外一种方式,不从主库读数据,但要保证一个用户只从一个从库读。如果用户两次同样的请求落到两个从库,可能会因为数据没有同步,读到不同版本的数据。为了保证读取一致,可以通过用户 ID 来路由到一个从库,所有读请求都到这个从库,如果这个从库故障了,再重新路由到另外一个从库继续单调读取。

多主复制

主从复制的集群,写操作集中在主库,所以可以很好地处理并发写。如果是多主复制(Master-Master)或者多数据中心,则情况会更为复杂,比如多个 Master 都可以接受写操作时,需要能够处理写入冲突。

一般在单个数据中心的集群中很少会采用多主,因为多主会增加复杂度,也确实没有必要,但是对于多数据中心,一般都是需要每个数据中心至少有一台主库。设想一下如果北京和上海分别有个数据中心,但是主库在上海,北京从库需要异步复制数据,跨地域的网络会导致很高的数据同步延迟。如果上海库的主库挂了,作为容灾要处理的 Leader 选举也会更加复杂,并且 Leader 选举期间,北京从库们也只能等着,无法接受数据写入。所以,更多的情况下,多数据中心都会选择每个数据中心分别有一个主库,每个数据中心自身内部是主从复制结构。

多主复制的问题

多主复制虽然好,但是却比一主多从的结构复杂,有很多问题需要解决,问题主要源于多个主库都可以写入,而多个主库的数据源是一样的,并且跨集群存在不可预估的网络延迟,比如自增主键的冲突、写入数据的冲突、数据库唯一约束的冲突等。

冲突处理

下面介绍一些主主复制的冲突处理方案。

(1)同步写

多主的写入其实跟应用系统的并发很类似,一种简单的处理方式就是同步,如上文的“同步模型”,用户的写入操作,需要等待所有的主库都同步好之后再返回给客户端,这种方式一般不会被采用,因为这还不如用一主多从的同步复制。

(2)避免冲突

从业务场景思考,可以一定程度避免冲突,比如同一个维度的数据只在一个中心写入,例如“用户的昵称”必须是唯一的,解决方式可以是用户编辑、新增昵称的业务请求,只往一个数据中心写。但这种方式也会增加复杂度,并且在一些流量高峰的场景,没有办法让其他数据中心通过分担写请求来削峰。

(3)冲突检测-处理

类似 LWW(Last Write Win)的方式,给每个写操作一个唯一的 ID,如使用时间戳 timestamp,总是接受时间戳更大的值的写入,MySQL 的 NDB 集群 提供了内置的基于 LWW 的冲突解决,但 LWW 有数据丢失的风险。也有一种方式是记录冲突的信息,并且将冲突稍后返回给应用去解决。

对于冲突解决,还有一种方案是存储服务只进行冲突检测,然后将冲突信息交给应用端去处理。

  • 写检测:写检测是通过检测复制日志中的数据冲突,然后会调起一个后台线程来处理冲突信息,一般这种冲突不会反馈给用户,可以应用端实现一些冲突解决接口。

  • 读检测:读检测一般是写数据的冲突已经存储了,在数据被读取的时候,不同的数据的版本会一起返回给应用,然后应用可以自己选择或者反馈给用户来选择想提交的数据。

MySQL NDB 也支持读冲突检测,其检测到读取数据冲突时(比如一个节点要更新一行被另外一个集群节点删除的行数据),通过设置一个读排他锁,所有关于这个含有冲突数据的读取都会被记录(Logged)到一个异常表中。当然 NDB 也提供了一些 API 让应用层自己实现一些冲突处理方法。

多主复制的拓扑模型

目前很多数据库会自带多主复制的集群模型,如 MySQL 的 NDB 集群,也有一些实现多主复制的外部工具,比如 MySQL 的多数据中心集群复制工具 Tungsten。多主复制的模型跟主从的区别就是多个主库都可以接受写入请求,而原来主从同步的模型结构不变。

在多主复制的网络拓扑结构里,比较普遍的是一下几种:

  • 环形:MySQL 集群默认是环形,一个主库处理写数据后依次(单向传递)发送给其他 Leader 进行数据复制。因为有序,且在复制日志中会记录所有节点的 ID (每个节点的唯一标识),可以防止重复处理。

  • 星形:一般基于一个 Root 点的同步,比如 Tungsten 的 HA 集群实现。

  • All-to-All:每个主库都会把写入的消息传输给其他所有的主库。

三种方式各有利弊,环形和星星比较简单,不过在一个主库处理失败时,有可能需要人工介入处理。All-to-All 的模型可以避免单点故障,但是延时比较严重,会有写冲突导致一些一致性问题,不过事件传输的顺序也可以通过一些基于写冲突场景的处理方案来处理:如基于物理时间的 LWW、Happens before 关系、基于逻辑时钟的版本向量(Version Vectors)等。

小结

在分布式环境下,为了提高性能以及可扩展性,大规模集群需要进行数据的复制,本文主要介绍了分布式的数据库的增量的数据复制的一些模型和方案。对于数据的备份、恢复也是一个比较大的话题,不同的数据库提供了不同的 dump 工具,这里就不详细介绍了。对于分布式的复制,会存在一些复杂的问题,主从同步的一致性问题、主主复制的写冲突问题。最终解决方案需要根据具体的领域模型做判断,思考和分析的粒度越细,就会越清晰地知道该怎样去处理分布式的问题。下一篇内容还是从分布式的存储出发,介绍另一个维度的话题——分区。

资料

  • http://book.mixu.net/distsys/replication.html

  • https://dev.mysql.com/doc/refman/5.7/en/replication-implementation-details.html

  • https://dev.mysql.com/doc/refman/5.7/en/replication-sbr-rbr.html

  • https://dev.mysql.com/doc/refman/5.7/en/replication-options-binary-log.html

  • https://dev.mysql.com/doc/refman/5.7/en/mysql-cluster-replication-conflict-resolution.html