软件开发领域有一个著名的“不可能三角”——质量、成本、时间,三者无法兼得。这也是 IT 行业没有银弹解决方案的根因所在,就好像分布式系统在带来高并发能力,突破 CPU 计算瓶颈与存储限制时,不可避免地带来了数据一致性的问题。 网上谈论数据一致性的文章不少,大多从算法的角度切入,本文作者选择了从服务架构的角度切入,详细拆解了主从架构、主主架构、无主架构三种架构模式下,数据一致性的难点与解决方案。
随着软件规模的不断扩大,服务也不得不从单体应用走向分布式部署,通过分布式应用的部署使我们极大程度地提高了系统的并发量,突破了 CPU 计算的瓶颈,也突破了磁盘存储的限制,看似我们使用了分布式技术就能够解决所有问题。但是任何技术都不是银弹。
解决问题的同时也带来相应的“副作用”——数据一致性问题。在分布式场景下,数据的处理存储读取由原先的单节点拓展成为了多节点,由于服务对外表现需要一致,意味着需要多节点协同数据保持一致,但是达到数据一致并不是一件简单的事情,后面其实需要做很多的努力。
下面我们来看下分布式场景下是如何影响数据一致性的。
1.1 数据存储读取
数据库是我们日常需要打交道的最常见的中间件,承载着数据存储的任务。但是在分布式场景下,这确实会带来不小的挑战。如图所示,当小明给自己的女朋友小红情人节发了520的红包,但是此时女朋友却没有收到感到非常生气,小明也很委屈,明明刚发的红包去哪里呢?
其实是因为小明发红包之后写入的数据节点和小红读取的数据节点不一致,并且小明发送的红包的数据节点并没有及时同步到小红读取的数据节点。
下面我们从数据存储读取的角度分析可能带来的问题以及解决方法。
在讨论这个问题我们先来想下,分布式环境下导致存储不稳定的本质原因其实是“无法读到正确的写”。那么为了解决“读正确的写”这个问题,就很有必要讨论下分布式环境下的存储节点之间是如何相互工作的,因为只有掌握了正确的数据流向,才能更好地剖析问题。
我们再来思考一个问题,为什么单体应用不存在这个问题?这是因为单体情况下数据的存储和读取的节点是唯一的,但是在分布式的场景下,情况就不一样了,多个节点之间就涉及到数据同步问题,而数据同步问题又和服务的部署架构有关系,因此我们从服务架构入手来分类讨论。
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(从连接的最大延迟时间)。
2.2 主主架构
主主架构解决了主从架构下单点写的问题,可以由多个节点承载着写和读的流量,并且完成从节点的数据同步动作,常见的应用场景主要见于:
在这种架构下数据脱离了单节点写入的控制,但是带来的副作用就是如何协同多主间的数据同步问题,数据的写入得到了保障,但是数据间的同步以及冲突却成了另外一个我们需要解决的棘手的问题。
2.2.1 冲突解决
这里我们可以解决思路可以从这几方面入手
由上述分析可得,主主架构下虽然能够解决单节点写入的问题,但是问题矛盾转移为主主间数据冲突解决的问题,这个问题虽然也有相应的解决方案,但是不同的解决方案只能针对部分场景进行解决,因此开发者在使用这种架构的数据存储方式的时候,应当根据自身的业务选择不同的策略解决。
2.3 无主架构
无主结构则更为激进,相比于主从和主主架构,放弃主节点,允许任何副本节点承载写和读的流量,写入的时候只要保证写入“大多数成功”即认为写入成功,那么这时候假设其中的一个节点写入失败,接着读取时又恰巧读取到了该节点的数据,是不是就会发生数据缺失问题?为了解决这个问题,就需要我们读取数据的时候不仅从一个节点进行读取,而是从多个节点进行,并且写入的数据中附带版本号,只要我们能够保证绝大部分节点可信任,就可以获取到正确的数据。
所以在无主架构下,主要矛盾转化为如何保证写入和读取正确的问题。
2.3.1 读写 quorum
即通过写入一定冗余节点的数据来保证数据的可靠性。即如果有 n 个副本,写入需要 w 个节点确认,读取必须至少查询 r 个节点,则只要满足 w+r>n 的条件,那么读取的节点中一定会包含最新值。这个结论举个简单的例子来说明,如图所示,一共五个节点,w 和 r 都为3,这样就可以保证读取的时候无论是读取哪几个节点,都至少有一个节点能读到写入成功的对应的节点。也就是只要我们能保证读和写的节点之间存在交叉即可。
2.3.2 quorum 问题
看似这个方案好像完美解决了数据一致性问题,但是我们仔细思考下,还是存在一定的问题。
当然,我们也可以采用宽松的 quorum 的方式,即在写入的节点不满足w的情况下,增加 n 意外的临时节点,再在网络回复的时候把临时节点的数据同步到主节点上,但是这样就牺牲了数据一致性来满足可用性。所以收到 CAP 理论的限制,无论怎么样都无法同时满足,只能根据具体的实际情况来做取舍。
由上述分析可得,在无主的情况下,数据的写入虽然不受到单节点的限制,但是在极端场景下同样无法保障数据的可靠性,需要能够容忍数据不一致的场景。
我们为了探究分布式环境下的数据一致性,从数据架构入手,分别探究了
这三种架构下数据是否能够保持写入和读取的一致,综上可知,每个方法都有对应的优缺点,在实际的开发过程中,我们应当要能够实际的业务情况,有所取舍地选择合适的架构,并且在实现的过程中注意这些可能出现的数据坑点,这样才能做到有的放矢,写出更加鲁棒的软件。