Netflix计划于2020年开源的数据库数据复制重器:DBLog,一个类似Oracle OGG的通用的变更数据捕获CDC框架 - Netflix


计划于2020年开源的DBLog的早期研究,其目标使异构数据存储保持同步。
变更数据捕获(CDC)允许从数据库中实时捕获已提交的变更,并将这些变更传播给下游使用者,CDC在需要保持多个异构数据存储同步(例如MySQL和ElasticSearch之间数据同步)并解决传统技术(如双重写入和分布式事务)的挑战的用例中变得越来越流行。
在MySQL和PostgreSQL等数据库中,事务日志是CDC事件源。由于事务日志的保留期限通常有限,因此不能保证它们包含完整的更改历史记录。因此,需要转储以捕获事件源的完整状态。有几个开源CDC项目,通常使用相同的基础库,数据库API和协议。
但是,我们发现了许多无法满足我们要求的限制,例如,在转储完成之前暂停日志事件的处理,缺少按需触发转储的能力或通过使用表锁来阻止写入流量的实现。这激发了DBLog的开发,后者在通用框架下提供日志和转储处理。为了获得支持,需要数据库来实现一组功能,这些功能在MySQL,PostgreSQL,MariaDB等系统中通常可用。
DBLog的某些功能包括:

  • 按顺序处理捕获的日志事件。
  • 可以随时在所有表之间针对特定表或特定表的主键进行转储。
  • 通过将转储分块进行交织并记录转储事件。这样日志处理可以与转储处理一起进行。如果处理过程终止,则可以在最后完成的块之后继续执行,而无需从头开始。如果需要,这还可以限制和暂停转储。
  • 永远不会获得表上的锁,这可以防止影响源数据库上的写流量。
  • 支持任何类型的输出,因此输出可以是流,数据存储甚至是API。
  • 设计时要考虑高可用性。因此,下游消费者有信心在源头上发生变化事件。

需求
在先前的博客文章中,我们讨论了数据丰富和同步平台Delta。Delta的目标是使多个数据存储保持同步,其中一个存储是事实的来源(如MySQL),而其他则是派生的存储(如ElasticSearch)。关键要求之一是从真相源到目的地的传播延迟低,并且事件流高度可用。无论同一团队使用多个数据存储,还是一个团队拥有另一个团队正在使用的数据,这些条件都适用。在Delta博客文章中,我们还描述了数据同步之外的用例,例如事件处理。
对于数据同步和事件处理用例,除了实时捕获更改之外,我们还需要满足以下要求:

  • 捕获完整状态。派生存储区(例如ElasticSearch)最终必须存储源的完整状态。我们通过源数据库中的转储提供此功能。
  • 随时触发维修。我们的目标不是在任何时候都将转储视为一次性设置活动,而是旨在在任何时候启用它们:跨所有表,特定表或特定主键。当数据丢失或损坏时,这对于下游修复至关重要。
  • 为实时事件提供高可用性。实时更改的传播对可用性有很高的要求;如果事件流停止较长时间段(例如几分钟或更长时间),则这是不希望的。即使在进行维修时也必须满足此要求,以使它们不会使实时事件停顿。我们希望将实时事件和转储事件交织在一起,以便两者都能取得进展。
  • 最小化数据库影响。连接到数据库时,重要的是要确保其带宽和为应用程序提供读写服务的能力受到的影响尽可能小。因此,最好避免使用会阻塞写流量(例如表上的锁)的API。除此之外,还必须放置控件以允许限制日志和转储处理,或者在需要时暂停处理。
  • 将事件写入任何输出。对于流技术,Netflix利用了多种选择,例如Kafka,SQS,Kinesis,甚至是Netflix特定的流解决方案(例如Keystone)。尽管将流作为输出可能是一个不错的选择(例如有多个使用方时),但这并不总是一个理想的选择(好像只有一个使用方)。我们希望提供直接写入目标而不通过流的功能。目的地可以是数据存储或外部API。
  • 支持关系数据库。Netflix上有一些服务通过AWS RDS使用RDBMS类型的数据库,例如MySQL或PostgreSQL。我们希望支持这些系统作为源,以便它们可以提供其数据以供进一步使用。

现有解决方案
我们评估了一系列现有的开源产品,包括:MaxwellSpinalTap,Yelp的MySQL StreamerDebezium。现有的解决方案在捕获源自事务日志的实时更改方面相似。例如,通过使用MySQL的binlog复制协议或PostgreSQL的复制插槽。
在转储处理方面,我们发现现有解决方案至少具有以下限制之一:

  • 在处理转储时停止日志事件处理。如果在进行转储时未处理日志事件,则适用此限制。因此,如果转储的容量很大,日志事件处理将停滞较长时间。当下游用户依赖实时更改的短传播延迟时,这是一个问题。
  • 缺少按需触发转储的能力。大多数解决方案最初都是在引导阶段或在事务日志中检测到数据丢失时执行转储。但是,按需触发转储的功能对于引导下游的新使用者(例如新的ElasticSearch索引)或数据丢失时的修复至关重要。
  • 通过锁定表来阻止写流量。一些解决方案使用表上的锁来协调转储处理。根据实现和数据库的不同,锁定的时间可以很短,也可以在整个转储过程中持续[5]。在后一种情况下,写流量将被阻止,直到转储完成。在某些情况下,可以配置专用的只读副本,以避免影响主服务器上的写入。但是,此策略不适用于所有数据库。例如,在PostgreSQL RDS中,更改只能从主数据库中捕获。
  • 使用专有的数据库功能。我们发现某些解决方案使用了不可转移到其他系统的高级数据库功能,例如:使用MySQL的黑洞引擎或从创建PostgreSQL复制插槽中获取转储的一致快照。这样可以防止跨数据库的代码重用。

最终,我们决定采用其他方法来处理转储。其中之一:
  • 交织日志与转储事件,以便两者都能取得进展
  • 允许随时触发转储
  • 不使用表锁
  • 使用标准化的数据库功能

DBLog框架​​​​​​​
DBLog是基于Java的框架,能够实时捕获更改并进行转储。转储是按块进行的,因此它们与实时事件交织,并且不会长时间停止实时事件处理。可以随时通过提供的API进行转储。这允许下游使用者最初或在以后的时间捕获完整的数据库状态以进行修复。
我们设计了框架以最大程度地减少数据库影响。转储可以根据需要暂停和恢复。这与失败后的恢复以及在数据库达到瓶颈时停止处理有关。我们也不对表进行锁定,以免影响应用程序的写入。
DBLog允许将捕获的事件写入任何输出,即使它是另一个数据库或API。我们使用Zookeeper来存储与日志和转储处理相关的状态,并用于组长选举。我们在构建DBLog时考虑到了可插入性,允许根据需要交换实现(例如,用其他东西代替Zookeeper)。
以下小节将更详细地说明日志和转储处理。

日志处理
该框架要求数据库实时为每个更改的行提交事件。假定事务日志是这些事件的起源。数据库正在将它们发送到DBLog可以使用的传输方式。对于该传输,我们使用术语“ 更改日志”。事件的类型可以是:create,update或delete。对于每个事件,需要提供以下内容:日志序列号,操作时的列状态以及操作时应用的架构。
每个更改都被序列化为DBLog事件格式,并发送给写入器,以便可以将其传递到输出。向写入器发送事件是非阻塞操作,因为写入器程序在其自己的线程中运行,并在内部缓冲区中收集事件。缓冲的事件按顺序写入输出。该框架允许插入自定义格式化程序,以将事件序列化为自定义格式。输出是一个简单的界面,允许插入任何所需的目标,例如流,数据存储甚至API。

转储处理
由于转储日志的事件保留时间有限,因此需要重新转储,这会阻止转储用于重构完整的源数据集。转储事件以大块形式进行,以便它们可以与日志事件交错,从而允许两者都进行。将为块的每个选定行生成一个事件,并以与日志事件相同的格式对其进行序列化。这样,如果事件源自日志或转储,则无需担心下游使用者。日志和转储事件都通过同一写入器发送到输出。
可以随时通过API为所有表,特定表或特定表主键安排转储。每个表的转储请求以已配置大小的块执行。另外,可以配置一个延迟来阻止对新块的处理,从而仅允许在这段时间内进行日志事件处理。块大小和延迟允许在日志和转储事件处理之间取得平衡,并且可以在运行时更新这两个设置。
通过按升序对主表进行排序并包括行的方式来选择块,其中主键大于上一个块的最后一个主键。数据库需要有效地执行此查询,这通常适用于对主键执行范围扫描的系统。
需要采用这样的方式进行分块处理:长时间不停止日志事件处理,并保留日志更改的历史记录,以便具有较早值的选定行无法覆盖日志事件中的较新状态。
为了实现这一点,我们在更改日志中创建了可识别的水印事件,以便可以对块选择进行排序。水印是通过源数据库中的表实现的。该表存储在专用名称空间中,因此不会与应用程序表发生冲突。存储UUID字段的表中仅包含一行。通过将该行更新为特定的UUID来生成水印。行更新导致更改事件,该事件最终通过更改日志接收。
通过使用水印,将使用以下步骤进行转储:

  1. 短暂暂停日志事件处理。
  2. 通过更新水印表来生成低水印。
  3. 对下一个块运行SELECT语句,并将结果集存储在内存中,并按主键索引。
  4. 通过更新水印表来生成高水印。
  5. 恢复将接收到的日志事件发送到输出。监视日志中的高低水印事件。
  6. 收到低水印事件后,开始从低水印之后接收的所有日志事件主键的结果集中删除条目。
  7. 收到高水位标记事件后,在处理新的日志事件之前,将所有剩余的结果集条目发送到输出。
  8. 如果存在更多块,请转到步骤1。

假定SELECT从一致的快照返回状态,该快照表示直到历史记录特定点的已提交更改。或等效地:SELECT在更改日志的特定位置执行,考虑到该点为止的更改。数据库通常不公开与select语句执行相对应的日志位置(MariaDB是一个例外)。
我们方法的核心思想是确定更改日志上的一个窗口,该窗口保证包含SELECT。由于确切的选择位置未知,因此将删除所有选择的行,这些行与该窗口内的日志事件发生冲突。这确保了块选择不会覆盖日志更改的历史记录。通过写低水印打开窗口,然后运行选择,最后,通过写高水印关闭窗口。为了使它起作用,SELECT 必须从低水位标记时间或更晚的时间开始读取最新状态(如果选择还包括在低水位标记写入之后且在读取之前提交的写入,则可以)。
...块算法点击标题见原文与图

数据库支持
为了使用DBLog,数据库需要根据提交的更改和非失效读取的线性历史记录提供更改日志。这些条件可以通过MySQL,PostgreSQL,MariaDB等系统来满足,因此可以在此类数据库中统一使用该框架。
到目前为止,我们添加了对MySQL和PostgreSQL的支持。由于每个数据库使用专有协议,因此需要使用不同的库来集成所需的日志事件。对于MySQL,我们使用shyiko / mysql-binlog-connector来实现binlog复制协议,以便从MySQL主机接收事件。对于PostgreSQL,我们在wal2json插件中使用了复制插槽。更改是通过PostgreSQL jdbc驱动程序实现的流复制协议接收的。在MySQL和PostgreSQL之间,确定每个捕获的更改的模式有所不同。在PostgreSQL中,wal2json包含列名称和类型以及列值。对于MySQL模式,必须跟踪作为binlog事件接收的更改。
转储处理通过使用SQL和JDBC进行集成,仅需要实现块选择和水印更新。相同的代码用于MySQL和PostgreSQL,也可以用于其他类似的数据库。转储处理本身不依赖SQL或JDBC,并且允许集成满足DBLog框架要求的数据库,即使它们使用不同的标准也是如此。

高可用性
DBLog使用主-备架构。一个实例是主动的,其他实例是被动的。我们利用Zookeeper进行领导者选举来确定活动实例。领导权是一种租约,如果没有及时刷新,就会丢失,从而允许另一个实例接管。当前,我们每个AZ部署一个实例(通常有3个AZ),因此,如果一个AZ发生故障,则另一个AZ中的一个实例可以以最小的总体停机时间继续进行处理。跨区域的被动实例也是可能的,尽管建议将其与数据库主机在同一区域中运行,以保持较低的变更捕获延迟。

生产用途
DBLog是Netflix的MySQL和PostgreSQL连接器的基础,后者在Delta中使用。自2018年以来,Delta已在Netflix Studio应用程序的生产中用于数据存储同步和事件处理用例。在DBLog之上,Delta连接器正在使用自定义事件序列化程序,因此在将事件写入输出时将使用Delta事件格式。Netflix特定的流用作Keystone等输出。
除Delta之外,DBLog还用于为其他具有自己的数据格式的Netflix数据移动平台构建连接器。

DBLog具有本博客文章未涵盖的其他功能,例如:

  • 无需使用锁即可捕获表模式的能力。
  • 模式存储库集成。存储发送到输出的每个事件的模式,并在每个事件的有效负载中具有对模式存储的引用。
  • 单调写入模式。确保一旦为特定行写入了状态,之后就不能再写入较新的状态。这样,下游消费者仅在向前的方向上经历状态转换,而不会来回移动。

我们计划在2020年开源DBLog,并包括其他文档。