一个公司的产品随着时间的发展单机的架构肯定无法满足其负载状态,产品迭代也是随着用户量进行的,当把单体架构拆分成多个微服务时就考虑一些数据同步问题,例如两个服务同时操作缓存中的一条数据或者是数据库中的数据,都会带来一些数据上的问题。这篇文章主要是针对的数据库系统,主从架构,主从切换、主从数据同步一些问题展开,也是DDIA第5章开始的数据复制和分布式系统设计一些挑战问题的总结。

当单机服务不能满足需求时系统往往需要扩容,扩容之前讲过有两种,一种垂直扩容的方式进行的,就是无条件的堆机器硬件,这种方式即使把机器性能加到很强的状态也不一定能满足系统高可用的设计;另外一种就是水平扩展,通过将程序代码部署到不同地理位置的数据中心里运作,这些节点通过网络进行数据交换,这种方式有良好系统扩展性,但是会依赖网络,网络是很不可靠的,这时系统设计时就要考虑单节点故障时能保证整体可靠性。

分布式系统节点进行数据复制可不像是在本机共享内存,本机复制数据只需要操作系统提供的API即可;而分布式系统重数据量太多的时候,往往会将其数据分区到不同的子系统上,也可以不同的网络节点上,这些数据如何同步?如果有主从关系如何保证数据的完备性?


主从架构

当一个数据库表记录达到某个上限可能就会考虑分库分表来缓解外部事物读写请求压力,有时候没有读写请求压力时也会考虑做主从架构考虑数据库的高可用性。例如主节点用来作为事务处理,而从库做一些数据分析相关的工作,另外就是当线上业务读事务大于写事务的时候,可以部署多台从节点来帮助主节点分担读压力。

关系数据库做主从架构有一个很大问题就是如何保证事务的 ACID ,因为主从架构都是通过网络连接的,虽然大部分业界主流的数据库都提供了主从架构配置设置选项。但是不能保证多台节点做联机事务处理,目前支持这种也就是 NewSQL 系列的数据库。且大部分写请求是操作主数据库的,如何把主数据库的数据记录同步到从节点上?如何设计同步规则?怎么实现数据多节点同步完整性?目前主从架构的另外一个问题就是如何在主节点失效情况下,其他子节点如何晋升为主节点?如何处理节点失效问题?如何处理添加节点问题?


数据同步

即架构为分布式主从结构就要考虑解决数据多节点同步问题,目前数据同步分为3种:同步复制、异步复制、半同步复制;

同步复制当用户向主节点写入一条数据记录时,此记录就会同步到其他节点,同步过程必须是当所有子节点写入成功之后才能返回请求,一段向用户确认说明主从节点数据都是最新版本的,缺点也很明显如果出现网络故障一直迟迟没有成功写入子节点请求就一直不会返回,其他写请求也会被阻塞。另外一种方式就是异步复制,将集群内部少数节点的数据复制设置同步的方式,其他多节点为异步的方式,异步复制可以在后台进行,当一条数据被同步到部分从节点时就可以返回写入成功请求,这样可以节约写入响应时间,这种同步和异步一起使用我更多认为是半同步复制。

当集群中某个节点失效时,如需要增加新的子节点提高系统整体容错能力时,如何同步之前的数据?因为主节点是在并行运行着的,而新加入节点想快速同步全量数据该如何做?如何保证数据一致性?

数据同步光靠着即使同步数据文件是无效的,因为此时主节点也在写入其他事务请求,而新加入的节点只能同步数据文件中数据而不是及时的;这里 Redis 主从架构设计的方式采用的是数据快照和增量同步方式,当需要同步时主节点快速生成数据快照通过网络传递给从节点,从节点则恢复数据,至于主节点新写入数据采用记录日志的方式同步,从节点为追赶模式,直到数据达到一致性;增量同步方式也适用于少量的网络抖动情况,当出现暂时性节点失联情况数据也能完成增量同步。

不管是同步还是异步都会产生一些矛盾的地方,例如增加从节点是为增高读的吞吐量和系统整体可用性,如果将集群中的所有节点都设置为同步模式也会带来另外一个问题,每添加一个从节点,那么每次往主节点写入一条记录需要同步的数据记录就要写入到其他子节点中,子节点越多那么系统整体可用性就会下降,因为是全同步模式导致一次写请求要多次复制写入情况,每多一次复制写入就会多一次请求写失败概率,同步式要等着所有节点写入成功才能返回响应。

这里可能有人会想全部换成异步模式不好吗?当把数据记录请求写入到主库中,其他节点就后台异步复制数据,但是然而因为是异步所以数据同步肯定会出现时间偏差的情况;当因为业务方可能是并发操作的不会立即等待异步数据写入才去读取,这时不现实的,当主节点写入的数据的成功,但下一个请求读取落到子节点而此时子节还没有完全同步数记录,导致读取不到数据记录情况发生,时序情况为下图:

异步数据同步数据一不是像同步一样做强一致性的,而类似于 DNS 域名解析生效过程,采用的最终一致性,这也就是复制滞后问题,如果采用异步复制的模式可以为数据记录设置版本号。我个人想法是在写入更新请求之后一段时间内读请求还是发送给主节点,当过了异步复制时间窗口期时才读取从节点并且可以配合缓存来使用,例如 SingleFight 模式;如果是用户修改自己资料场景,可以将用户自己的新的读请求放到主节点上,而其他用户的读请求则在从节点上资料显示可以允许不是及时更新的,但是这时针对特定的场景不适应所有场景。所以在数据同步问题上没有十全十美的解决方法,只有根据业务场景做定制化设计。

同步和异步问题都是由于请求顺序和时间关系引起的,关于时间在分布式系统设计里面的问题太多了,因为基于时间每分每秒都是在流动的,但是你不能从一个时间点跳到另外一个时间点,但是数据读取可能就会出现这种超自然情况,这种场景只能在科幻片里才能看到的;在这时间窗口内就会出现访问到老数据和新数据交叉的情况,业界很多相关讨论:单调读、写后读一致性、前缀一致读


节点切换

在分布式系统难免会遇到某个节点出现问题的情况,在主从数据库架构,最大问题就是主节点出现了故障无法正常运作,此时业务方的写入事务请求就要转向新的节点了,此时一个问题是如何选择集群中其他的节点晋升为主节点?怎么提前发现主节点出现了故障?

如何设计一个故障检查算法?目前最常见就是通过心跳包方式判断节点是否存活着,心跳包是有时间间隔的,如何设计这个间隔时间?分布式最大的问题就是依赖于网络,网络是非常不可靠的,可能出现网络拥堵或者抖动,导致心跳包超时,如果心跳包设置时间间隔过长会导致出现故障时难以即使发现处理,时间太短又频繁发送心跳包;如果发现故障了如何选择新的主节点?这也是分布式选举共识问题,业界有很多主流的算法Raft和Paxos,低端一点就是通过运维手动控制。

如果是异步复制模式下主从架构,在主节点出现故障时很有可能出现一个问题,主节点的数据并没有完全同步到从节点导致从节点上的数据不是最新版本,此时从节点晋升为主节点后会出现一些莫名奇妙后果;例如主数据库在故障之前使用了自增 ID 做表记录,而一些新写入请求在原主节点执行成功,但是由于故障切换到新主节点上,由于异步复制未能同步到自增 ID 的计数器原值,导致后面其他请求到了重复的ID数据记录,这样会影响整个系统数据。


日志复制

分布式架构下数据怎么实现同步也是一种挑战,主流的设计方案是:请求同步转发、日志复制、逻辑日志复制。

最简单的方式就是主节点把收到的 SQL 语句重新发给子节点,然后子节点重新运行一遍,这是最简单的实现方式。但是问题也很明显,如果 SQL 语句里面使用了 nowrand 函数或者存储过程,这时就会参数某些不确定数据记录,因为是两台并行运行机器无法预判具体函数产生的值。另外一个问题就是SQL语句有前后执行顺序问题也要考虑,例如多个并发事务时要注意。

另外一种基于WAL日志复制传递的,主从通过复制日志文件的方式同步数据,通过网络把文件传输到子节点上,子节点收到数据文件就开始处理完成数据记录同步工作,数据文件格式依赖于特定版本,如果两台机器的版本不一致可能会出现数据文件无法兼容情况。

如果想要兼容和数据记录可靠性就得使用行逻辑日志复制技术,这也是大多数数据库实现的方式,具体复制与存储引擎剥离,逻辑日志是通过一些列的数据表行的记录,对应写请求日志会包含所有相关的列的新值,删除记录有足够的条件来表示符合条件记录,更新也是通过唯一表示更新行以及所有列的新值。

MySQL 的 5.1 之后主从同步架构就是通过语句复制和逻辑复制混合使用,如果语句中存在不确定性,就会切换成逻辑复制紧凑模式,这样所有的节点都复制正确并且一致的结构了。这是都是基于数据库存储引擎内部实现的,还有一种需求是针对于自定义数据复制的需求,复制部分的数据到另外一个节点上或者解析某部分数据记录的需求,这时大部分数据库都有通过SQL触发器功能),开发者可以使用触发器。


多主架构

主从结构如果是单一主节点架构算是只能为了提高读吞吐量和高可用设计的,并没有考虑到用户所在地理位置请求响应延迟等问题。如果是主节点和从节点不是在同一个数据中心,那么一个写请求的数据记录复制要跨数据中心才能完成,且读数据的时候也要跨数据中心,没有考虑到把在地理位置上均匀分布。这个问题可以在每个数据中心采用主从架构,而多个数据中心连接起来就是分布式多主架构,每个地区的数据中心都有自己的主节点写入数据时就不需要跨区写入延迟问题,此架构如下图:

单主从架构缺点帮你充当解决异地容灾问题,最多当主节点失效的时候从节点零时升级为主节点继续为用户提供服务;而多主从架构可以分散保证可用性,也能提供地理位置的就近提供访问服务的问题,当A数据中心出现问题, B 数据中心可以继续提供着服务,多主数据中心架构对网络依赖很强因为主节点的写入也同步到不同数据中心主节点中,但是问题也很明显会出现脑裂的情况。最大的问题是有多个主节点时,用户们并发去操作其中一个主节点时会发生数据冲突问题,可能同时修改每条数据但是这条数据存在多个主节点中副本,这时就要考虑引入分布式锁🔐来解决这个问题,分布式锁是分布式系统设计一个难点。


无主架构

上面的多主架构因为数据写入并行操作导致数据记录存储时间先后顺序问题,如果要解决就要使用和本地锁一样的方案来解决处理写入冲突问题,如果多个用户在操作一条记录必须等待其他人释放锁才能正常执行操作,后面请求要么阻塞要么就丢弃不执行;另外一种方式就是将其路由到同一个主节点上执行,但是这违背了按地里位置就近服务的原则,然后自定义冲突解决逻辑,在写入数据的时候处理冲突通过用户自己决定,这个设计就和 GIT 版本控制软件很像;另外一种就是服务器返回多个数据版本,由读取客户端自己决定从多个数据记录中挑选需要的版本。

与主从架构相比无主架构又是另外一种架构方式,所有的服务器节点没有主从关系都为对等关系,也就是P2P模式即去中心化的架构和区块链架构很像。这种架构最典型的例子就是 Amazon DynamoDB 它是分布式 NoSQL 数据库的代表,主要特点就是一个数据记录写入到集群中的多个节点中,而读取则可以从集群中多个节点中读取数据记录,读写流畅如下:

假设整个数据库集群有3个节点组成的,当有数据记录写入到集群中 2 以上就认为写入成功则,当读取的时候客户端可以从多个节点中获取数据副本,然后根据本地算法查看数据记录版本号,例如上图读取回来有一个版本为 6 和两个版本为 7 的,少数服从多数,那么最终数据记录就为 7 ,客户端并且将返回 6 的数据记录端设置 7 放回去,保证最终一致性。另外再每个存储引擎上后台都会一个线程在和进群中的其他节点做数据协调,检测数据记录版本的差异,自动修复缺少的数记录,自动从一个副本复杂到另外一个副本,而与复制日志形式不同的是不能保证写入顺序也要明显的数据同步滞后问题。


Quorum一致性

这种方式做就是数据记录冗余存储,如果有相同数据记录副本存储在集群中多数节点中,那么读取的时候大概率会读取到最新版本记录,如果配置的写入节点越多,那么整体系统可用率就越高,这是根据一个Quorum一致性原则推理过来的实现方案。 Quorum 机制是一种分布式系统中常用的,用来保证数据冗余和最终一致性的投票算法,其主要数学思想来源于鸽巢原理,有 10 只🕊鸽子但是有9个鸽笼,如果把这10只鸽子要全部放到笼子里,那么其中一个笼子里面肯定为两只,如下图:

根据这个规律,换到分布式数据库中,笼子就相当于节点数 nw 表示鸽子数,r读取数据访问笼子数据,断定只要 w + r > n 那么一定能放到最新的数据记录,在 Amazon Dynamo 风格的数据库中是可以通过配置进行修改这些参数的,到达法定仲裁条件,场景设置方案是n为奇数通常为 3 或者 5, w = r = (n + 1) / 2 向上舍入。

另外一种 Sloppy Quorum 提供配置整体系统可容忍的某些节点故障个数,也不需要执行故障切换。本质上在 Quorum 就没有主节点这种说法,可以容忍部分网络故障导致客户端连接到n个节点读取数据,对于系统高可用性和低延迟要求来是这种方式确实不错;但是因为网络原因和地里分布的原因,可能一部分客户端能连接到某些节点,一部分连接不上某些节点,导致某些节点再客户端面前就等于失效状态,或者将不能正常连接的节点请求转移到其他节点之上。


小结

多集群数据同步是一个复杂的问题,因为多个节点的数据写入和操作都是并行执行着并且都是有先后顺序的和时间顺序的,主从同步需要数据写入主节点再写入从节点,写只能操作主节点,读只能操作从节点,但是会出现写放大问题,一个写要被复制到多个节点上;异步复杂提供响应速度失去了数据一致性问题,会出现读滞后问题;而P2P模式则需要更强一致性算法保证数据记录,因为数据存储多个版本,要协调多个数据版本问题;分布式系统设计最大的问题就是将多个异步的操作变成同步并且保证数据一致性,依赖于网络并且还依赖于时间,处理时间的时候是属于物理学的东西会和狭义相对论联系起来,因为大部分操作都是异步进行的。

其他资料

便宜 VPS vultr
最后修改:2023 年 07 月 05 日
如果觉得我的文章对你有用,请随意赞赏 🌹 谢谢 !