关注“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 status 和 show 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