原文链接:CockroachDB Change Data Capture: Transactionally and Horizontally Scalable
作者:Daniel Harrison
CockroachDB是一个优秀的NewSQL数据库,但没有技术存在于真空中。我们的一些用户希望将他们的数据保存在全文检索系统中,以支持自然语言搜索。其他人希望使用分析引擎和大数据管道来运行大量查询,而不会影响生产流量。还有一些人希望发送移动推送通知以响应数据更改,而无需自己进行簿记。
CockroachDB旨在简化数据,有时这意味着与他人合作。
行业标准解决方案是Change Data Capture(通常缩写为CDC)。每个数据库的处理方式略有不同,但它通常看起来像一个消息流,每个消息都包含有关数据更改的信息。我们称之为changefeed。
CockroachDB `CHANGEFEED`是一个或多个表发生变化数据的实时流。当SQL语句执行并更改存储的数据时,消息将发送到外部系统,我们将其称为“接收器”。执行INSERT INTO users (1, "Carl"), (2, "Petee")可能发送{"id": 1, "name": "Carl"}`和`{"id": 2, "name": "Petee"}。
我们可以支持直接发送到我们想要使用的所有内容,并且最终可能发生,但这涉及每个客户端驱动程序和性能调整。相反,我们发出一个“消息代理”,它被设计成一个中间人,正是这种事情。用户反馈促使我们选择Kafka作为第一个支持(译者注:目前在最新的cockroachDB v19.1.0版本上,基于kafka的changefeed已经具备生产环境使用)。
早期挑战
建立CockroachDB changefeed的最大挑战从一开始就很明显。我们希望我们的changefeed可以横向扩展,但我们也希望它们保持强大的事务语义。
在单节点数据库中,这在概念上很容易。我所知道的每个数据库都使用写前日志(WAL)来处理磁盘故障或断电时的持久性(ACID中的D)。WAL本身只是每次更改的磁盘上的有序日志,因此构建changefeed的工作主要是以合理的方式公开此日志。实际上,Postgres有一个用于追随(tailing)WAL的插件系统,Postgres的各种changefeed实现都是作为插件实现的。其他数据库也类似。
但是,CockroachDB具有独特的分布式架构。它存储的数据被分解为大约64MB的“ranges”。这些ranges每个都被复制成N个“副本”以获得高可用。CockroachDB事务可以涉及任何或所有这些ranges,这意味着它可以跨越集群中的任何或所有节点。
这与在水平扩展其他SQL数据库时使用的“分片”设置形成对比,其中每个分片是完全独立的复制单元,并且事务不能跨越分片。然后,分片SQL群集上的changefeed只是每个分片的changefeed,通常由分片的领导者运行。由于每个事务完全发生在一个分片中,因此分片之间事务的相对排序并不那么值得特别关注(或者说大家很多时候不在乎这种分片之间的事务排序)。它还意味着各个分片的feeds可以完全并行化(每个分片一个Kafka主题或分区是典型的)。
由于CockroachDB事务可以使用集群中的任何range集合(考虑跨分片事务),因此事务排序要复杂得多。特别是,并不总是可以将事务划分为独立的流。这里简单的答案是将每个事务放入一个流中,但我们对此并不满意。CockroachDB旨在水平扩展到大量节点,因此我们当然希望我们的changefeed也可以水平扩展。
灵光时刻(A Lightbulb Moment)
CockroachDB中的SQL表可以跨越多个range,但该表中的每一行始终包含在一个range内。(当range变大时系统可以移动,系统将其分成两部分以及range变小并且系统将其合并到相邻range时,但这些可以单独处理。)此外,每个range都是单个筏raft group(raft共识组),因此有自己的WAL,我们可以追随这个WAL。这意味着我们可以为每个SQL行生成有序的changefeed。为了实现这一目标,我们开发了一种内部机制,将这些变化直接从我们的raft group中推出,而不是轮询它们。它被称为RangeFeed,但它对于自己的博客文章来说是一个足够大的话题,所以我不会详细介绍。
每个行流都是独立的,这意味着我们可以水平缩放它们。使用我们的分布式SQL框架,我们将处理器放置在正在观察的数据旁边发出行更改,从而消除不必要的网络跃点。如果一个节点完成所有观看和发送,它还可以避免我们遇到的单点故障。
对于许多changefeed用途,这就足够了; 每条消息都可以触发移动推送通知,某些数据存储不支持事务。有序的行流对这两者都很有用。
对于其他用途,这还不够; 将数据镜像到分析数据库当然不希望应用部分事务(译者注:每个range独立向kafka推送changefeed,那么跨range的事务在消费端就很难得直接到原子性保证)。
每个CockroachDB事务已经使用相同的HLC时间戳提交每一行。在每个消息中为更改的行暴露此时间戳足以获得事务信息(按时间戳分组行集合)[1]以及总排序(按时间戳排序行)。在我们现有的事务时间戳之上构建意味着我们的changefeed与CockroachDB中的其他所有内容具有相同的可序列化保证。
最后一部分是知道何时进行这一组或排序。如果hlc1从一个CockroachDB节点随时间发出更改的行,那么在对其进行操作之前,您需要等待多长时间才能确保其他任何节点都没有更改hlc1?
我们用一个我们称之为“resolved”的时间戳消息的概念来解决这个问题。这是一个承诺,即不会发出新的行更改,其时间戳小于或等于已解析的时间戳消息中的时间戳。这意味着上述用户可以在hlc1从每个节点[2]接收到已解决的时间戳之后进行操作>= hlc1。
在图4中,想象两个独立的流都已经通过读取X。hlc1在其中一个stream上已经“resolved”,但在另外一个stream上没有“resolved”,所以hlc1还没有“resolved。
现在想象一下,在稍后的某些时候,消息已经被读过了Y。两个stream都已“resolved”了hlc1,所以我们知道我们已收到所有已发生的变化,包括hlc1。如果我们按时间戳对消息进行分组,我们可以恢复交易。在这种情况下,只有(B->1,C->2),承诺在hlc1。此事务现在可以发送到分析数据库。
请注意,(A->3)更改发生在hlc2,因此尚未“resolved”。这意味着changefeed用户需要继续缓存它。
我们还可以随时重建数据库的状态,包括hlc1保持每行的最新值。这甚至适用于范围和节点。在这种情况下,hlc1数据库时B=1,C=2。
最后,想象一下稍后Z读取所有消息的时间。通过相同的两个进程再次获取数据库的事务和状态。在这种情况下,交易(A->3,B->4)承诺hlc2和(C->5)承诺hlc3。在hlc3包含的数据库中A=3,B=1,C=5。请注意,hlc2如有必要,我们还可以重建数据库。
每当我现在解释这一切时,它似乎是如此明显,但其实这只是那些回想起来才能明显的想法中的一个罢了。(至少对我们来说。)其实,啊哈!这个时刻来自与工程师同事讨论关于分布式增量计算的一篇非常有趣的论文,其中包括将数据添加到具有特定时间戳的系统并定期“关闭”该时间戳的想法(以后不会引入新的承诺)带有时间戳<=的数据,关闭的时间戳)。这允许增量计算完成到那个时间戳的所有内容,并且没有理由我们不能在CockroachDB中使用相同的想法。顺便说一句,我们的一位工程师对这篇论文非常兴奋,他最近离开了共同创建了materialize.io 并与该论文的作者之一围绕它建立了一家公司。
管道和更多的管道(Sinks and More Sinks)
到目前为止,我们已经讨论过Kafka消息代理,但这不是我们支持的唯一接收器。许多流行的分析数据库,包括Google BigQuery,Snowflake和Amazon Redshift,都支持从云存储加载数据。如果这是用户唯一需要CDC的东西,那么他们就没有理由需要在中间运行Kafka,特别是如果他们还没有在其他地方使用它。
除了Kafka之外,还有许多消息代理选项。我们将根据需求为他们添加支持,但与此同时,我们也支持HTTP作为接收器。HTTP加JSON(我们的默认格式)是互联网的通用语言,因此可以轻松地将CockroachDB changefeed复制到您能想象到的任何内容上。我们所拥有的一些想法包括我们尚不支持的消息代理以及“无服务器”计算,但我们对用户会想到的消息更加兴奋。
最后,对于CockroachDB Core的用户,我们提供了一个新的` CHANGEFEED FOR`语句,它通过SQL连接传回消息。这在精神上类似于RethinkDB的changefeeds(他们的社区非常喜欢)。我们(还)不允许您将它们用作RethinkDB等查询中的数据源,但我们没有理由在将来不添加它。
这三个接收器最初在我们的19.1.0版本中作为实验性功能公开,因此我们可以确保在提交它们之前获得API。
新道路
CockroachDB的SQL语言具有数十年的历史。这意味着对于大多数功能,我们已经拥有面向用户的外部表面区域应该具有的强大先例。但偶然的我们独特的分布式架构意味着我们会开辟一条新的道路,我们的改变就是其中的一个例子。
我们认为我们的方法可以使简单的事情更加简单(推送通知和非事务数据存储的有序行更新),同时使困难的事情成为可能(具有强大的多节点事务和排序保证的水平可伸缩性)。我们希望您同意。我们正在积极寻求反馈,请尝试一下,让我们知道您的想法!
备注
1:近乎完美(Well almost)。两个不重叠的事务可以使用相同的时间戳进行提交,但它们具有纳秒精度,因此在实践中这种情况很少见。我们还没有找到任何需要更多粒度的人,但如果有人这样做,我们可以公开我们内部的唯一事务ID。
2:与往常一样,现实比这更复杂。changefeed用户通常不直接从CockroachDB接收数据,而是从消息代理(如Kafka)接收数据。这意味着“resolved”时间戳实际上需要在Kafka消费侧处理,而不是CockroachDB节点。
译者评论
CockroachDB的changefeed设计还是非常的新颖,传统的CDC方案并不特别适合分布式数据库,不过把事务的排序交给用户我不认为是一个特别好的设计,这增加了CockroachDB的使用难度。