分布式数据库(十)

全球化部署

全球化部署本质就是全球范围下的异地多活。总体上看,异地多活的直接目标是要预防区域级的灾难事件,比如城市级的断电,或是地震、洪水等自然灾害。也就是说,在这些灾难发生时,要让系统还能保障关键业务的持续开展。

因此,这里的『异地』通常是指除同城机房外,在距离较远的城市配备的独立机房,在物理距离上跳出区域级灾难的覆盖范围。这个区域有多大呢?从银行业的实践来看,两地机房的布局通常会部署在南北或者东西两个大区,比如深圳到上海,或者北京到武汉,又或者北京到西安,距离一般会超过 1000 公里。

对于银行业的异地机房建设,监管机构是有具体要求的,也就是大中型银行的『两地三中心』布局。而对于互联网行业来说,虽然没有政策性要求,但业务本身的高可用需求,也推动了头部公司进行相应的布局。

异地多活是高可用架构的一种实现方式,它是以整个应用系统为单位,一般来说会分为应用和数据库两部分。

应用部分通常是无状态的,这个无状态就是说应用处理每个请求时是不需要从本地加载上下文数据的。这样启动多个应用服务器就没有什么额外的成本,应用之间也没有上下文依赖,所以就很容易做到多活。

数据库节点要最终持久化数据,所有的服务都要基于已有的数据,并且这些数据内容还在不断地变化。任何新的服务节点在接入这个体系后,相互之间还会存在影响,所以数据库服务有逻辑很重的上下文。因此数据库的多活的难度就大多了,也就产生了不同版本的解读。


单体数据库

数据库层面的异地多活,本质上是要实现数据库的高可用和低延迟,也就是『永不宕机』和『近在咫尺』。即便是单体数据库时代的技术方案,也是朝着这个方向努力的。

异地容灾

异地容灾是异地多活的低配版,它往往是这样的架构。

异地容灾架构

整个方案涉及同城和异地两个机房,都部署了同样的应用服务器和数据库,其中应用服务器都处于运行状态可以处理请求,也就是应用多活。只有同城机房的数据库处于运行状态,异地机房数据库并不启动,不过会通过底层存储设备向异地机房同步数据。然后,所有应用都链接到同城机房的数据库。

同城不可用

这样当同城机房整体不可用时,异地机房的数据库会被拉起并加载数据,同时异地机房的应用切换到异地机房的数据库。

显然,这个多活只是应用服务器的多活,两地的数据库并不同时提供服务。这种模式下,异地应用虽然靠近用户,但仍然要访问远端的数据库,对延迟的改善并不大。在系统正常运行情况下,异地数据库并没有实际产出,造成了资源的浪费。

按照正常的商业逻辑,当然不能容忍这种资源浪费,所以有了异地读写分离模式。


异地读写分离

在异地读写分离模式下,异地数据库和主机房数据库同时对外提供服务,但服务类型限制为只读服务,但只读服务的数据一致性是不保证的。

异地读写分离架构

当主机房完全不可用时,异地机房的运作方式和异地容灾模式大体是一样的。

读写分离模式下,异地数据库也投入了使用,不再是闲置的资源。但是很多场景下,只读服务在业务服务中的占比还是比较低的,再加上不能保证数据的强一致性,适用范围又被进一步缩小。所以,对于部分业务场景,异地数据库节点可能还是运行在低负载水平下。

于是,又有了进一步的双向同步模式。


双向同步

双向同步模式下,同城和异地的数据库同时提供服务,并且是读写服务均开放。但有一个重要的约束,就是两地读写的对象必须是不同的数据,区分方式可以是不同的表或者表内的不同记录,这个约束是为了保证两地数据操作不会冲突。因为不需要处理跨区域的事务冲突,所以两地数据库之间就可以采用异步同步的方式。

双向同步架构

这个模式下,两地处理的数据是泾渭分明的,所以实质上是两个独立的应用实例,或者可以说是两个独立的单元,也就是单元化架构。而两个单元之间又相互备份数据,有了这些数据可以容灾,也可以开放只读服务。当然,这个只读服务同样是不保证数据一致性的。

可以说,双向同步是单元化架构和异地读写分离的混合,异地机房的资源被充分使用了。但双向同步没有解决一个根本问题,就是两地仍然不能处理同样的数据,对于一个完整的系统来说,这还是有很大局限性的。


分布式数据库

分布式数据库的数据组织单位是更细粒度的分片,又有了 Raft 协议的加持,所以就有了更加灵活的模式。

机房级容灾(两地三中心五副本)

比较典型的分布式数据库部署模式是两地三中心五副本。这种模式下,每个分片都有 5 个副本,在同城的双机房各部署两个副本,异地机房部署一个副本。

两地三中心五副本

这个模式有三个特点:

  1. 异地备份:保留了『异地容灾』模式下的数据同步功能,但因为同样要保证低延迟,所以也做不到 RPO(Recovery Point Objective, 恢复点目标)为零。

  2. 容灾能力:如果同城机房有一个不可用或者是同城机房间的网络出现故障,异地机房节点的投票就会发挥作用,依然可以和同城可用的那个机房共同达成多数投票,那么数据库的服务就仍然可以正常运行,当然这时提交过程必须要异地通讯,所以延迟会受到一定程度影响。

  3. 同城访问低延迟:由于 Raft 或 Paxos 都是多数派协议,那么任何写操作时,同城的四个副本就能够超过半数完成提交,这样就不会因为与异地机房通讯时间长而推高数据库的操作延迟。

两地三中心虽然可以容灾,但对于异地机房来说 RPO 不为零,在更加苛刻的场景下,仍然受到挑战。这也就催生了三地五副本模式,来实现 RPO 为零的城市级容灾。


城市级容灾(三地五副本)

三地五副本模式是两地三中心模式的升级版。两个同城机房变成两个相临城市的机房,这样总共是在三个城市部署。

这种模式在容灾能力和延迟之间做了权衡,牺牲一点延迟,换来城市级别的容灾。比如,在北京和天津建立两座机房,两个城市距离不算太远,延迟在 10 毫秒以内,还是可以接受的。不过,距离较近的城市对区域性灾难的抵御能力不强,还不是真正意义上的异地。

顺着这思路,还有更大规模的三地五中心七副本。但无论如何,只要不放弃低延迟,真正异地机房就无法做到 RPO 为零。

无论是两地三中心五副本还是三地五副本,它们更像是单体数据库异地容灾的加强版。因为,其中的异地机房始终是一个替补的角色,而那些异地的应用服务器也依然花费很多时间访问远端的数据库。

这个并不满足『近在咫尺』的要去。


架构问题

为什么会这样呢?因为有些分布式数据库会有一个限制条件,就是所有的 Leader 节点必须固定在同城主机房,而这就导致了资源使用率大幅下降。TiDB 和 OceanBase 都是这种情况。

Raft 协议下,所有读写都是发送到 Leader 节点,Follower 节点是没有太大负载的。Raft 协议的复制单位是分片级的,所以理论上一个节点可以既是一些分片的 Leader,又是另一些分片的 Follower。也就是说,通过 Leader 和 Follower 混合部署可以充分利用硬件资源。

但是如果主副本只能存在同一个机房,那就意味着另外三个机房的节点,也就是有整个集群五分之三的资源,在绝大多数时候都处于低负载状态。这显然是不经济的。

这个限制条件是怎么来的,一定要有吗?

其实,这个限制条件就是全局时钟导致的。具体来说,就是单时间源的授时服务器不能距离 Leader 太远,否则会增加通讯延迟,性能就受到很大影响,极端情况下还会出现异常。

增加延迟比较好理解,因为距离远了嘛。那异常是怎么回事呢?

架构问题异常

这种异常称为『远端写入时间戳异常』,它的发生过程是这样的:

  1. C2 节点与机房 A 的全局时钟服务器通讯,获取时间。此时绝对时间(At)是 500,而全局时钟(Ct)也是 500。
  2. A3 节点也与全局时钟通讯,获取时间。A3 的请求晚于 C2,拿到的全局时钟是 510,此时的绝对时钟也是 510。
  3. A3 节点要向 R2 写入数据,这个动作肯定是晚于取全局时钟的操作,所以绝对时间来到了 512,但是 A3 使用的时间戳仍然是 510。写入成功。
  4. 轮到 C2 节点向 R2 写入数据,由于 C2 在异地,通讯的时间更长,所以虽然 C2 先开始写入动作的流程,但却落后于 A3 将写入命令发送给 R2,此时绝对时间来到了 550,而 C2 使用的时间戳是 500。A3 与 C2 都要向 R2 写入数据,并且是相同的 Key,数据要相互覆盖的。这时候问题来了,R2 中已经有了一条记录时间戳是 510,已经提交成功,稍后又收到了一条时间戳是 500 的记录,这是 R2 只能拒绝 500 的这条记录。因为后写入的数据使用更早的时间戳,整个时间线就会乱掉,否则读取的进程会先看到 510 的数据,再看到 500 的数据,数据一致性显然有问题。

这个例子说明,如果远端计算节点距离时钟节点过远,那么当并发较大且事务冲突较多时,异地机房就会出现频繁的写入失败。这种业务场景并不罕见,当网购付款时就会出现多个事务在短时间内竞争修改商户的账户余额的情况。


全球化部署

全球化部署的前提是多时间源、多点授时,这样不同分片的主副本就可以分散在多个机房。那么数据库服务可以尽量靠近用户,而应用系统也可以访问本地的分片主副本,整体效果达到等同于单元化部署的效果。

当出现跨多地分片参与同一个分布式事务的情况,全球化部署模式也可以很好地支持。由于参与分片跨越更大的物理范围,所以延迟就受到影响,这一点是无法避免的。还有刚刚提到的『远端写入时间戳异常』,因为每个机房都可以就近获得时钟,那么发生异常的概率也会大幅下降。

全球化部署

全球化部署模式下,异地机房所有的节点都是处于运行状态的,异地机房不再是替补角色,和同城机房一样提供对等的支持能力,所有机房同等重要。

在任何机房发生灾难时,主副本会漂移到其他机房,整个系统处于较为稳定的高可用状态。而应用系统通过访问本地机房的数据库分片主副本就可以完成多数操作,这些发生在距离用户最近的机房,所以延迟可以控制到很低。这样就做到了『永不宕机』和『近在咫尺』。


同城双机房

还有一些例外情况,比如下面这种架构。

同城双机房

主机房保留了过半的副本,这意味着即使是同城备用机房,也不能实现 RPO 为零。那么主备机房之间就退化成了异步复制,这不更像一个单体数据库的主备模式吗?这样部署的意图是什么呢?

有人这么解释这个部署架构:这样可以保证其他机房不存在时,主机房能够单独工作。持这种观点的并不是极少数。还有人会提出来:当主机房只有少数副本时,是不是可以继续工作呢?如果对 Raft 协议有所了解,你会觉得这个要求不可思议,少数副本还继续工作就意味着可能出现脑裂,这怎么可以呢?这里的脑裂是指发生网络分区时,两个少数节点群仍可以保持内部的通讯,然后各自选出的 Leader,分别对外提供服务。

但是换个角度去想,就会发现这些人也有他的理由。既然数据还是完整的,为什么不能提供服务呢?虽然,损失掉了备份机房,但这不影响主机房的工作呀。


恶意攻击

通常来说,架构设计的高可用都是面对正常情况的,机器、网络的不可用都是源于设备自身故障不是外力损坏。但是,有没有可能发生恶意攻击呢?

回到两地三中心的模式,如果三个机房之间的光纤网络被挖断,整个数据库就处于不可用状态,因为这时已经不可能有过半数的节点参与投票了。自然状态下,这个事情发生的概率太低了,三中心之间同时有三条路线,甚至有些机构为了提高安全性,会设置并行的多条线路。但是,如果真的是恶意攻击,多搞几台挖掘机就能让银行的系统瘫痪掉,这比黑客攻击容易多了,而且成本也很低的。

同城双机房的设计,其实一种比较保守的方案,它力图规避了主机房之外因素的干扰因素。为了系统平稳运行,甚至可以放弃 RPO 为零这个重要的目标。虽然我并不认为这是最优的方案,但也确实可以引发一些思考。

RPO 为零是一种保障手段,而持续服务才是目标。那么,也就不应该为了追求 RPO 为零这个手段,而让原本还能正常运行的服务终止掉。这是为了手段而放弃了目标,不就成了舍本求末吗?在必要的时候,还是要保证主要目标舍弃次要的东西。

所以,Raft 协议还需要一个降级机制,也就是说不一定要过半投票,仍然维持服务。类似这样的设计在有些分布式数据库中已经可以看到了。因此,我觉得三地五副本模式加上 Raft 降级,应该算是目前比较完善的方案了。


Follower Read

全球化部署还有一个远端读的性能问题。如果分片的主副本在主机房,而异地机房要读取这些数据,如何高效实现呢?读写分离当然可以,但这损失了数据一致性,在有的场景下是不能接受的。CockroachDB 目前支持的 Follower Read 虽然提升了性能,但也是不保证数据一致性的;而 TiDB 的 Follower 目前还不支持跨机房部署。

一个思路就是利用 Raft 协议无『日志空洞』的特点,等到日志时间戳超过查询时间戳,数据就足够新了。但这仅限于写入操作密集的分片,如果分片上的数据比较冷,根本等不到时间戳增长,又该怎么办呢?

其实还有可优化的地方,那就是利用 Raft 协议的合并发送机制。事实上,在真正实现 Raft 协议时,因为每个 Raft 组单独通讯的成本太高,通常会将同一节点的多个 Raft 协议打包后对外发送,这样可以考虑增加其他分片的最后更新时间戳,再通过协议包的发送时间戳来判断包内分片的最新状态。由于节点级别的 Raft 协议是源源不断发送的,这样只要冷分片和热分片在同一个包内,就可以及时得到它的状态。


全服务的意义

异地多活的终极目标应该是让异地机房提供全服务,也就是读写服务。这样的意义在于让备用机房的设备处于全面运行的状态,这不仅提升了资源利用率,也时刻确保了各种设备处于可运行的状态,它们的健康状态是实时可知的。

而在异地容灾模式下,备机房必须通过定期演练来确认是可用的,这耗费了人力物力,但并没有转化为真正的生产力,而且仍然存在风险。演练的业务场景足够多吗?出现问题时,这个定期演练的系统真的能够顶上去吗?很多人心中或许也是一个问号。显然,一个时刻运行着的系统比三个月才演练一次的系统更让人放心。

所以,我认为一个具有全球化部署能力,或者说是能真正做到异地多活的分布式数据库,是有非常重要的意义的。


小结

  1. 全球化部署就是全球范围下的异地多活。异地多活通常是指系统级别,包括了应用和数据库,难点在于数据库的多活。
  2. 单体数据库的异地多活主要有三个版本,异地容灾、异地读写分离和双向同步。但是都无法让应用在同城和异地同时操作相同的数据,没有解决数据库大范围部署的问题。
  3. 分布式数据库常采用的部署方式是两地三中心五副本,可以实现机房级别的容灾和异地备份数据但 RPO 不为零。在此基础上,还可以升级到三地三中心五副本,提供城市级别容灾,在邻近城市实现 RPO 为零。使用单点授时的分布式数据库,必须将所有分片的主副本集中在主机房,这一方面是由于访问全局时钟的通讯成本高,另外是为了避免异常现象。
  4. 如果从恶意攻击的角度看,基于 Raft 协议的多中心部署反而会带来数据库的脆弱性,因为机房间的通讯链路将成为致命的弱点。所以,应在一定条件下允许对 Raft 协议做降级处理,保证少数副本也可以对外提供服务。
  5. 缩短从节点读操作延迟对于异地多活也有重要的意义,目前尚没有完善的解决方案,我们探讨了一些优化的可能性。真正的异地多活必须是异地机房提供全服务,这样才能在本质上提升系统可用性,比定期演练更加可靠。

加餐:Raft 的协议降级处理,它允许数据库在仅保留少数副本的情况下,仍然可以继续对外提供服务。这和标准 Raft 显然是不同的,应该如何设计这种降级机制呢?

这个机制并不复杂。首先是固定主副本的位置,只有主机房的副本才能被选举为 Leader,这样就能保证主机房的数据足够新;其次是引入一个『低水位线』的概念,约定节点数量的下限,允许节点数量少于半数但高于下限时,仍然可以对外提供服务。

事实上,这两点不单是理论探讨。第一点在很多分布式数据库的实际应用中都有实现,因为这种约束本身就能降低运维的复杂度;第二点在 GoldenDB 等数据库中已经可以看到类似的设计。


容灾与备份

在系统领域,逃生通道是指让业务能够脱离已经不可用的原有系统,在一个安全的备用系统中继续运转。

可以说逃生通道就是系统高可用的一种特殊形式。它的特别之处在于,备用系统要提供差异化的、更高级别的可靠性。为什么备用系统能够提供更高级别的可靠性呢?这是主要由于它采用了异构方案。

对于分布式数据库来说,逃生通道存在的意义应该更容易理解。作为一种新兴的技术,分布式数据库还没有足够多的实践案例来证明自身的稳定性,即使是它本身的高可用架构也不足以打消用户的顾虑。这时候就需要设计一种异构的高可用方案,采用更加稳定、可靠的数据库作为备用系统。通常把这个备用数据库称为『逃生库』。

在分布式数据库的实践没有成为绝对主流前,逃生通道都是一个不容忽视的用户需求。它可以降低实施风险,打消用户的顾虑,减少新技术应用中遇到的各种阻力。


CDC

作为一个数据库的高可用方案,首先要解决的是数据恢复的完整性,也就是 RPO。这就需要及时将数据复制到逃生库。通常异构数据库间的数据复制有三种可选方式:

  1. 数据文件
  2. ETL(Extract-Transform-Load)
  3. CDC(Change Data Capture)

数据文件是指,在数据库之间通过文件导入导出的方式同步数据。这是一种针对全量数据的批量操作,如果要实现增量加载,则需要在数据库表的结构上做特殊设计。显然,数据文件的方式速度比较慢,而且对表结构设计有侵入性,所以不是最优选择。

ETL 是指包含了抽取(Extract)、转换(Transform)和加载(Load)三个阶段的数据加工过程,可以直连两端的数据库,也可以配合数据文件一起用,同样必须依赖表结构的特殊设计才能实现增量抽取,而且在两个数据库之间的耦合更加紧密。

最后,CDC 其实是一个更合适的选择。CDC 的功能可以直接按照字面意思理解,就是用来捕获变更数据的组件,它的工作原理是通过读取上游的 redo log 来生成 SQL 语句发送给下游。它能够捕捉所有 DML 的变化,比如 delete 和 update,而且可以配合 redo log 的设置记录前值和后值。

相比之下,CDC 的业务侵入性非常小,不改动表结构的情况下,下游系统就可以准确同步数据的增量变化。另外,CDC 的时效性较好,对源端数据库的资源占用也不大,通常在 5%-10% 之间。

CDC 是常规的配套工具,技术也比较成熟,很多流式数据的源头就是 CDC。CDC 工具的提供者通常是源端的数据库厂商,传统数据库中知名度较高的有 Oracle 的 OGG(Gold Gate)和 DB2 的 Inforsphere CDC。


逃生方案

对于分布式数据库来说,要选择一个更加成熟、稳定的逃生库,答案显然就是单体数据库了。那具体要选择哪种产品呢?从合理性角度来说,肯定就要选用户最熟悉、最信任、运维能力最强的单体数据库,也就是能够『兜底』的数据库。

按照这个思路,这个异构高可用方案就由分布式数据库和单体数据库共同组成,分布式数据库向单体数据库异步复制数据。使用异步复制的原因是不让单体数据库的性能拖后腿。

异步复制数据

这样,当分布式数据库出现问题时,应用就可以切换到原来的单体数据库,继续提供服务。

异步复制实现

这个方案看似简单,但其实还有一些具体问题要探讨。

1、日志格式适配

首先是单体数据库的日志适配问题。

逃生方案的关键设计就是数据异步复制,而载体就是日志文件。既然是异构数据库,那日志文件就有差别吧,要怎么处理呢?

PGXC 风格分布式数据库,它的数据节点是直接复用了 MySQL 和 PostgreSQL 单体数据库。这就意味着,如果本来熟悉的单体数据库是 MySQL,现在又恰好采用了 MySQL 为内核的分布式数据库,比如 GoldenDB、TDSQL,那么这个方案处理起来就容易些,因为两者都是基于 Binlog 完成数据复制的。

而如果选择了 NewSQL 分布式数据库也没关系。虽然 NewSQL 没有复用单体数据库,但为了与技术生态更好的融合,它们通常都会兼容某类单体数据库,大多数是在 MySQL 和 PostgreSQL 中选择一个,比如 TiDB 兼容 MySQL,而 CockroachDB 兼容 PostgreSQL。TiDB 还扩展了日志功能,通过 Binlog 组件直接输出 SQL,可以加载到下游的 MySQL。

如果很不幸,选择的分布式数据库并没有兼容原有的单体数据库规范,也没有提供开放性接口,那么就需要做额外的转换工作。


2、处理性能适配

第二个问题是性能匹配问题。用单体数据库来做逃生库,这里其实有一个悖论。那就是,选择分布式的多数原因就是单体不能满足性能需求,那么这个高可用方案要再切换回单体,那单体能够扛得住这个性能压力呢?

比如,用 MySQL+x86 来做 TiDB+x86 的逃生库,这个方案就显得有点奇怪。事实上,在分布式数据库出现前,性能拓展方式是垂直扩展,导致相当一部分对性能和稳定性要求更高的数据库已经切换到 Oracle、DB2 等商用数据库加小型机的模式上。所以,对于金融行业来说,往往需要用 Oracle 或 DB2+ 小型机来做逃生库。

当然,性能问题还有缓释手段,就是增加一个分布式消息队列来缓存数据,降低逃生库的性能压力。这样就形成下面的架构。

分布式消息队列缓存数据

另外,有了分布式消息队列的缓冲,就可以更加方便地完成异构数据的格式转换。当然,日志格式的处理是没有统一解决方案的,如果没有能力做二次开发,就只能寄希望于产品厂商提供相应的工具。

这样,完成了日志格式的适配,增加了消息队列做性能适配,但是并不意味者逃生方案就搞定了!


3、事务一致性

数据库事务中操作顺序是非常重要的,而日志中记录顺序必须与事务实际操作顺序严格一致,这样才能确保通过重放日志恢复出相同的数据库。可以说,数据库的备份恢复完全建立在日志的有序性基础上。逃生库也是一样的,要想准确的体现数据,必须得到顺序严格一致的日志,只不过这个日志不再是文件的形式,而是实时动态推送的变更消息流(Change Feed)。

如果一个单体数据库向外复制数据,这是很容易实现的,因为只要将 WAL 的内容对外发布就能形成了顺序严格一致的变更消息流。但对于分布式数据库来说,就是一个设计上的挑战了。

如果事务都在分片内完成,那么每个分片的处理逻辑和单体数据库就是完全一样的,将各自的日志信息发送给 Kafka 即可。

对于 NewSQL 来说,因为每个分片就是一个独立的 Raft Group,有对应的 WAL 日志,所以要按分片向 Kafka 发送增量变化。比如,CockroachDB 就是直接将每个分片 Leader 的 Raft 日志发送出去,这样处理起来最简单。

Kafka 发送

但是,分布式数据库的复杂性就在于跨分片操作,尤其是跨分片事务,也就是分布式事务。一个分布式事务往往涉及很多数据项,这些数据都可能被记录在不同的分片中,理论上可以包含集群内的所有分片。如果每个分片独立发送数据,下游收到数据的顺序就很可能与数据产生的顺序不同,那么逃生库就会看到不同的执行结果。

通过下面的例子来说明。

小明打算购买银行的自有理财产品。之前他的银行账号有 60,000 元活期存款,他在了解产品的收益情况后,购买了 50,000 元理财并支付了 50 元的手续费。最后,小明账户上有 50,000 元的理财产品和 9,950 元的活期存款。

假设,银行的系统通过下面这段 SQL 来处理相应的数据变更:

begin;
// 在小明的活期账户上扣减50,000元,剩余10,000元
update balance_num = balance_num - 50000 where id = 2234; 
// 在小明的理财产品账户上增加50,000元
update fund_num = fund_num + 50000 = where id = 2234;
// 扣减手续费50元
update balance_num = balance_num - 50 where id = 2234;
// 其他操作
……
commit;

小明的理财记录和活期账户记录对应两个分片,这两个分片恰好分布在 Node1 和 Node2 这两个不同节点上,两个分片各自独立发送变更消息流。

数据对应两个分片

同一个事务中发生变更的数据必定拥有用相同的提交时间戳,所以使用时间戳就可以回溯同一事务内的相关操作。同时,晚提交的事务一定拥有更大的时间戳。

那么按照这个规则,逃生库在 T1 时刻发现已经收到了时间戳为 ts2 的变更消息,而且 ts2 > ts1,所以判断 ts1 事务的操作已经执行完毕。执行 ts1 下的所有操作,于是我们在逃生库看到小明的活期账户余额是 10,000 元,理财账户余额是 50,000 元。

但是,这个结果显然是不正确的。在保证事务一致性的前提下,其他事务看到的小明活期账户余额只可能是 60,000 元或 9,950,这两个数值一个是在事务开始前,一个是在事务提交后。活期账户余额从 60,000 变更到 10,000 对于外部是不可见的,逃生库却暴露了这个数据,没有实现已提交读(Read Committed)隔离级别,也就是说,没有实现基本的事务一致性。

产生这个问题原因在于,变更消息中的时间戳只是标识了数据产生的时间,这并不代表逃生库能够在同一时间收到所有相同时间戳的变更消息,也就不能用更晚的时间戳来代表前一个事务的变更消息已经接受完毕。那么,要怎么知道同一时间戳的数据已经接受完毕了呢?

为了解决这个问题,CockroachDB 引入了一个特殊时间戳标志『Resolved』,用来表示当前节点上这个时间戳已经关闭。结合上面的例子,它的意思就是一旦 Node1 发出“ts1:Resolved”消息后,则 Node1 就不会再发出任何时间戳小于或者等于 ts1 的变更消息。

Resolved

在每个节点的变更消息流中增加了 Resolved 消息后,逃生库就可以在 T2 时间判断,所有 ts1 的变更消息已经发送完毕,可以执行整个事务操作了。


小结

  1. 分布式数据库作为一种新兴技术,往往需要提供充分可靠的备用方案,用于降低上线初期的运行风险,这种备用方案往往被称为逃生通道。逃生通道的本质是一套基于异构数据库的高可用方案。用来作为备库的数据库称为逃生库,通常会选择用户熟悉的单体数据库。
  2. 整套方案要解决两个基本问题,分别是日志格式的适配和性能的适配。如果逃生库与分布式数据库本身兼容,则日志格式问题自然消除,这大多限于 MySQL 和 PostgreSQL 等开源数据库。如果日志格式不兼容,就要借助厂商工具或者用户定制开发来实现。传统企业由于大量使用商业数据库,这个问题较为突出。性能适配是因为分布式数据库的性能远高于基于 x86 的单体数据库,需要通过分布式消息队列来适配,缓存日志消息。
  3. 因为分布式数据库的日志是每个分片独立记录,所以当发生分布式事务时,逃生库会出现数据错误。如果有严格的事务一致性要求,则需要考虑多分片之间变更消息的顺序问题。CockroachDB 提供一种特殊的时间戳消息,用于标识节点上的时间戳关闭,这样在复制过程中也能保证多分片事务的一致性。

CDC 的主要工作场景是为分析性系统推送准实时的数据变更,支持类似 Kappa 架构的整体方案。但是对于分布式数据库来说,CDC 成为了逃生通道的核心组件,远远超过它在单体数据库中的重要性。目前很多分布式数据库已经推出了相关的功能或组件,例如 CockroachDB、TiDB、GoldenDB 等。


加餐:对单个数据分片来说 WAL 本身就是有序的,直接开放就可以提供顺序一致的变更消息。而逃生通道作为一个异构高可用方案,往往会使用 Kafka 这样的分布式消息队列。那么单分片的变更消息流在逃生通道中还能保证顺序一致吗?

答案是不能保证顺序一致,原因在于 Kafka 的分布式架构设计。

Kafka 按照 Topic 组织消息,而为了提升性能,一个 Topic 会包含多个 Partition,分布到不同节点(Broker)。那么,所有发送到 Kafka 的消息,实际上是会按照一定的负载均衡策略发送到不同的 Broker 上。

这样的设计意味着,在 Broker 内部消息是有序的,但多个 Broker 之间消息是无序的。作为消息的消费者,即使接收的只是单个分片的变更消息流,也不能用当前消息的时间戳来判定小于这个时间戳的消息已经发送完毕。


更新时间:2021-05-15 13:54:48

本文由 caroly 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载 / 出处外,均为本站原创或翻译,转载前请务必署名
原文链接:https://caroly.fun/archives/分布式数据库十
最后更新:2021-05-15 13:54:48

评论

Your browser is out of date!

Update your browser to view this website correctly. Update my browser now

×