为什么单体不用考虑一致性而分布式需要?

以云看科技2024-04-02 16:22:18  141

软件开发领域有一个著名的“不可能三角”——质量、成本、时间,三者无法兼得。这也是 IT 行业没有银弹解决方案的根因所在,就好像分布式系统在带来高并发能力,突破 CPU 计算瓶颈与存储限制时,不可避免地带来了数据一致性的问题。

网上谈论数据一致性的文章不少,大多从算法的角度切入,本文作者选择了从服务架构的角度切入,详细拆解了主从架构、主主架构、无主架构三种架构模式下,数据一致性的难点与解决方案。

01

背景

随着软件规模的不断扩大,服务也不得不从单体应用走向分布式部署,通过分布式应用的部署使我们极大程度地提高了系统的并发量,突破了 CPU 计算的瓶颈,也突破了磁盘存储的限制,看似我们使用了分布式技术就能够解决所有问题。但是任何技术都不是银弹。

解决问题的同时也带来相应的“副作用”——数据一致性问题。在分布式场景下,数据的处理存储读取由原先的单节点拓展成为了多节点,由于服务对外表现需要一致,意味着需要多节点协同数据保持一致,但是达到数据一致并不是一件简单的事情,后面其实需要做很多的努力。

下面我们来看下分布式场景下是如何影响数据一致性的。

1.1 数据存储读取

数据库是我们日常需要打交道的最常见的中间件,承载着数据存储的任务。但是在分布式场景下,这确实会带来不小的挑战。如图所示,当小明给自己的女朋友小红情人节发了520的红包,但是此时女朋友却没有收到感到非常生气,小明也很委屈,明明刚发的红包去哪里呢?

其实是因为小明发红包之后写入的数据节点和小红读取的数据节点不一致,并且小明发送的红包的数据节点并没有及时同步到小红读取的数据节点。

下面我们从数据存储读取的角度分析可能带来的问题以及解决方法。

02

数据存储一致性

在讨论这个问题我们先来想下,分布式环境下导致存储不稳定的本质原因其实是“无法读到正确的写”。那么为了解决“读正确的写”这个问题,就很有必要讨论下分布式环境下的存储节点之间是如何相互工作的,因为只有掌握了正确的数据流向,才能更好地剖析问题。

我们再来思考一个问题,为什么单体应用不存在这个问题?这是因为单体情况下数据的存储和读取的节点是唯一的,但是在分布式的场景下,情况就不一样了,多个节点之间就涉及到数据同步问题,而数据同步问题又和服务的部署架构有关系,因此我们从服务架构入手来分类讨论。

2.1 主从架构

主从架构是数据库常见的部署架构,即主节点负责写入数据,从节点来分担读流量,在这种架构模式下,主从间的数据同步存在三种方式:

同步复制;

半同步复制;

异步复制。

2.1.1 同步复制

同步复制指的是数据在主节点写入成功之后,还需要确保各个从节点同步数据成功之后才向上游返回成功 ack。一般用于对数据可靠性要求较高的场景,如金融级的数据库 tdsql,生产环境一般使用一主两备强同步的方式,这种同步复制的方式存在相应的优缺点:

优点:主从间的数据同步强一致,数据可靠性高。

缺点:数据同步环境对网络的延时要求较高,并且从节点越多,写入的效率会越低,受网络波动影响越大,相比于半同步和异步复制来说,数据复制效率较低。

因此在这种同步模式下,数据写入和读取是对等的,因为一旦回复了成功的 ack 之后,代表主从间的数据保持一致,数据稳定性高。

2.1.2 半同步复制

半同步复制指的是数据在主节点写入成功之后,从节点同步只要同步成功一个即返回成功 ack。一般用于对数据写入效率要求较高的场景,据作者了解,shopee 的 db 同步主要采用这种半同步复制的方式,这种同步复制方式存在相应的优缺点

优点:主从同步效率高,对网络延时较不敏感。

缺点:在数据同步过程中,由于只要一个节点同步成功即算写入成功,如果此时其他数据节点异步复制失败,恰好此时路由到该节点,可能出现幻写(即刚写入的数据再读取的时候发生丢失),数据可靠性较低。

因此在此同步模式下,数据的一致性不高,只能通过未同步节点异步追赶主节点日志来达到最终数据一致的目的,此时如果对于数据一致性要求较高的场景,可以通过“主写主读”的方式来实现。半同步复制通过牺牲了一部分数据稳定性来换取同步写入的高效,是比较好的折中方式。

2.1.3 异步复制

异步复制指的是数据在主节点写入成功后立即返回,从节点异步复制主节点的数据。一般用于对数据写入效率要求较高的场景,如此时 db 做灾备以及异地多活场景下,可能涉及到跨区的数据复制,可以采用这种方式进行同步:

优点:主从同步效率很高,从节点的网络延时对主节点的写入完全没有影响。

缺点:从节点无法和主节点保持完全同步,通过异步同步的方式很有可能出现主从间数据不一致的场景。

因此在这种同步模式下,数据的可靠性较低,如果对于数据一致性要求较高的场景,同样可以通过“主写主读”的方式来实现。

通过上述的讨论可得主从模式下数据的可靠性主要是通过主从间的数据复制机制来保证的,同时,主节点存在单点问题,承载了写入流量。那么为了保证数据的一致性,我们应该考虑如何处理节点失效?

2.1.4 处理节点失效

这个问题我们可以从两方面考虑:

从节点失效:追赶主节点。这里我觉得在全同步模式下可以借鉴 kafka 的 ISR 机制,去维护一个网络效率高的同步节点队列,当延时较大时主动剔除该队列并进行数据追赶,等到追赶完成之后再加入到该队列中。

主节点失效:重新选主。重新选主的话也需要注意以下几个问题。选举的时候可能会导致选举的节点有数据缺失问题;还有可能存在脑裂问题,参考 Redis 解决脑裂的方式,通过参数配置 min-slaves-to-write(最小从服务器数) 和 min-slaves-max-lag(从连接的最大延迟时间)。

min-slaves-to-write 是指主库最少得有 N 个健康的从库存活才能执行写命令。这个配置虽然不能保证 N 个从库都一定能接收到主库的写操作,但是能避免当没有足够健康的从库时,主库无法正常写入,以此来避免数据的丢失 ,如果设置为 0 则表示关闭该功能。

min-slaves-max-lag :是指从库和主库进行数据复制时的 ACK 消息延迟的最大时间;可以确保从库在指定的时间内,如果 ACK 时间没在规定时间内,则拒绝写入。

2.2 主主架构

主主架构解决了主从架构下单点写的问题,可以由多个节点承载着写和读的流量,并且完成从节点的数据同步动作,常见的应用场景主要见于:

多数据中心。为了容忍整个数据中心级别故障或者更接近用户,可以把数据库的副本横跨多个数据中心。

离线客户端操作。如手机或其他电子设备的笔记软件设备,能够在多端写入以及在多端进行数据同步。

协同编辑。如在线文档操作,多用户同时编辑文档,当用户编辑更改,会立即在其他用户端数据界面生效。

在这种架构下数据脱离了单节点写入的控制,但是带来的副作用就是如何协同多主间的数据同步问题,数据的写入得到了保障,但是数据间的同步以及冲突却成了另外一个我们需要解决的棘手的问题。

2.2.1 冲突解决

这里我们可以解决思路可以从这几方面入手

处理写冲突。在发生同时写入某行的数据冲突情况下能够有机制及时发现,并且阻塞其他的写请求,直到该写入完成再进行后续节点的写入,但是这么做会极大降低并发效率,如果是多设备的数据同步场景下,冲突概率不大,因为理论上用户当前时间只会针对某个设备下进行编辑;但是对于协同编辑的场景下,这种冲突的概率就很大,因此写入的效率就会大打折扣。

避免冲突。解决冲突最好的方式就是避免冲突,这种思想在很多产品上都可以得以体现,如 MySQL的 mvcc 模式,在事务中读取的是 mvcc 的快照,隔离了不同事务之间对于动态更改数据的访问,转而请求快照,避免了冲突场景下的数据读取,只在数据提交的时候对改行的冲突以及一致性约束进行检测。同样,在多数据中心的场景下,我们也可以将用户路由到最近的数据中心进行写入,以此来降低数据冲突的概率。

一致性状态收敛。理想情况下,我们还是希望数据能够对外保持一致,那么最直接的方式就是解决冲突,即在用户提交数据时由服务端检测各节点之间的数据差异,主动或者被动进行数据合并,可以使用以下几种方式:

给写入分配时间戳,并且以后写入的数据为准。

为每个同步节点分配唯一 ID,指定规则如序号高的副本始终优先于序号低副本的写入。

按照预定义的合并规则自动将数据进行合并。

将合并控制权交给用户,让用户自行决定数据合并。

由上述分析可得,主主架构下虽然能够解决单节点写入的问题,但是问题矛盾转移为主主间数据冲突解决的问题,这个问题虽然也有相应的解决方案,但是不同的解决方案只能针对部分场景进行解决,因此开发者在使用这种架构的数据存储方式的时候,应当根据自身的业务选择不同的策略解决。

2.3 无主架构

无主结构则更为激进,相比于主从和主主架构,放弃主节点,允许任何副本节点承载写和读的流量,写入的时候只要保证写入“大多数成功”即认为写入成功,那么这时候假设其中的一个节点写入失败,接着读取时又恰巧读取到了该节点的数据,是不是就会发生数据缺失问题?为了解决这个问题,就需要我们读取数据的时候不仅从一个节点进行读取,而是从多个节点进行,并且写入的数据中附带版本号,只要我们能够保证绝大部分节点可信任,就可以获取到正确的数据。

所以在无主架构下,主要矛盾转化为如何保证写入和读取正确的问题。

2.3.1 读写 quorum

即通过写入一定冗余节点的数据来保证数据的可靠性。即如果有 n 个副本,写入需要 w 个节点确认,读取必须至少查询 r 个节点,则只要满足 w+r>n 的条件,那么读取的节点中一定会包含最新值。这个结论举个简单的例子来说明,如图所示,一共五个节点,w 和 r 都为3,这样就可以保证读取的时候无论是读取哪几个节点,都至少有一个节点能读到写入成功的对应的节点。也就是只要我们能保证读和写的节点之间存在交叉即可。

2.3.2 quorum 问题

看似这个方案好像完美解决了数据一致性问题,但是我们仔细思考下,还是存在一定的问题。

当此时数据节点出现宕机的情况下,即 n

即使在 w+r>n 的场景下,也可能存在数据返回异常的情况。

写操作同时发生,无法确定先后顺序,因为可能存在时钟偏移的情况。

写和读操作同时发生,写操作可能仅在一部分副本上完成,此时,读取时返回旧值和新值不确定。

写操作在节点中部分成功,部分失败,但是由于已成功的数据无法回滚,可能引入脏数据。

当然,我们也可以采用宽松的 quorum 的方式,即在写入的节点不满足w的情况下,增加 n 意外的临时节点,再在网络回复的时候把临时节点的数据同步到主节点上,但是这样就牺牲了数据一致性来满足可用性。所以收到 CAP 理论的限制,无论怎么样都无法同时满足,只能根据具体的实际情况来做取舍。

由上述分析可得,在无主的情况下,数据的写入虽然不受到单节点的限制,但是在极端场景下同样无法保障数据的可靠性,需要能够容忍数据不一致的场景。

03

总结

我们为了探究分布式环境下的数据一致性,从数据架构入手,分别探究了

主从架构。

主主架构。

无主架构。

这三种架构下数据是否能够保持写入和读取的一致,综上可知,每个方法都有对应的优缺点,在实际的开发过程中,我们应当要能够实际的业务情况,有所取舍地选择合适的架构,并且在实现的过程中注意这些可能出现的数据坑点,这样才能做到有的放矢,写出更加鲁棒的软件。

作者:李滨

转载此文是出于传递更多信息目的。若来源标注错误或侵犯了您的合法权益,请与本站联系,我们将及时更正、删除、谢谢。
https://www.414w.com/read/134630.html
0
最新回复(0)