对于应用程序来说,使用多个数据存储是一种常见的模式,其中每个数据存储都用于满足特定的需求,如存储形式化数据(MySQL等)、提供高级搜索功能(ElasticSearch等)、缓存(Memcached等)等等。通常,在使用多个数据存储时,其中一个用作主存储,其他用作次存储。现在的挑战是如何保持这些数据存储的同步。
我们已经观察到了一系列不同的模式,试图解决多数据存储的同步问题,比如双写、分布式事务等等。然而,这些方法在可行性、健壮性和可维护方面有局限性。除了数据同步之外,一些应用程序还需要通过调用外部服务来增强数据。
为了应对这些挑战,我们开发了Delta。Delta是一个最终一致的、事件驱动的数据同步和增强平台。
为了保持两个数据存储的同步,可以执行双写操作,即在对一个数据存储执行写操作之后,对另一个数据存储执行写操作。第一个写操作可以重试,如果第一个写操作在用完重试次数之后失败,则可以中止第二个写操作。但是,如果第二个数据存储写入失败,这两个数据存储就会失去同步。一种常见的解决方案是构建一个修复例程,周期性地将第一个存储区中的数据重新应用到第二个存储,或者只有在检测到差异时才这样做。
问题:
实现修复例程通常是专用的,可能无法重用。另外,在应用修复例程之前,存储之间的数据是不同步的。如果涉及两个以上的数据存储,则解决方案会变得越来越复杂。最后,修复例程会在主数据源活动期间给其增加大量的压力。
当一组表发生变动(如插入、更新和删除)时,更改项会作为同一事务的一部分添加到日志表中。另一个线程或进程不断轮询日志表中的事件,并将它们写入一个或多个数据存储中,在所有数据存储确认后可选择从日志表中删除事件。
问题:
这需要作为一个库来实现,并且在理想情况下不需要对使用它的应用程序进行代码更改。在多语言环境中,需要对每种支持的语言重复实现这个库,并且很难确保跨语言时特性和行为的一致性。
模式更改的捕获还存在另一个问题,有些系统(如MySQL)不支持事务性模式更改[1][2]。因此,执行更改(如模式更改)并以事务方式将其写入变更日志表的模式并不总是有效。
分布式事务可用于实现跨多个异构数据存储的事务,以便将写操作提交给所有相关存储或不提交。
问题:
事实证明,分布式事务跨异构数据存储是有问题的。本质上讲,它们只能依赖于参与系统的最小公分母。例如,如果应用程序进程在准备阶段失败,XA事务将阻塞执行;此外,XA不提供死锁检测,也不支持乐观并发控制方案。而且,某些系统(如ElasticSearch)不支持XA或其他任何异构事务模型。因此,对于应用程序[3]来说,要保证跨不同存储技术的写操作的原子性仍然是一个具有挑战性的问题。
开发Delta是为了解决现有数据同步解决方案的局限性,并允许动态地增强数据。我们的目标是从应用程序开发人员中抽象出这些复杂性,这样他们就可以专注于实现业务特性。下面,我们将描述“电影搜索”,这是Netflix内部使用Delta的一个实际用例。
在Netflix,微服务架构被广泛采用,每个微服务通常只处理一种类型的数据。核心电影数据驻留在一个称为Movie Service的微服务中,而诸如电影交易、人才、供应商等相关数据则由多个其他的微服务(例如Deal Service、Talent Service和Vendor Service)提供。Netflix工作室的企业用户通常需要根据不同的标准搜索电影以便跟踪制作情况,因此,对他们来说,能够搜索与电影相关的所有数据至关重要。
在采用Delta之前,电影搜索团队在索引电影数据之前必须从多个其他的微服务获取数据。此外,团队必须构建一个系统,通过查询其他人的更改来定期更新他们的搜索索引,即使根本没有更改。这个系统很快就变得非常复杂且难以维护。
图1 采用Delta之前的轮询系统
在上了Delta之后,系统被简化为一个事件驱动系统,如下图所示。CDC (Change-Data-Capture)事件由Delta-Connector发送到Keystone Kafka主题。使用Delta流处理框架构建的Delta应用程序会消费该主题中的CDC事件,然后调用其他微服务来增强每个事件,并最终将增强后的数据发送到Elasticsearch中的搜索索引。整个过程几乎是实时的,这意味着只要将更改提交到数据存储,搜索索引就会更新。
图2 使用Delta实现的数据管道
在接下来的部分中,我们将描述连接到数据存储并将CDC事件发布到传输层的Delta-Connector。传输层则将CDC事件路由到Kafka主题的实时数据传输基础设施。最后,我们将描述应用程序开发人员可以用来构建他们的数据处理和增强逻辑的Delta流处理框架。
我们开发了一个名为Delta-Connector的CDC服务,它能够实时捕获数据存储中提交的更改并将其写入流。实时更改是从数据存储的事务日志和转储中捕获的。之所以采用转储,是因为事务日志通常不包含更改的完整历史记录。更改通常被序列化为Delta事件,因此,如果更改来自事务日志或转储,使用者就无需担心。
连接器提供多种先进的功能,如:
我们目前支持MySQL和Postgres,包括部署在AWS RDS及其Aurora版本中的时候。此外,我们支持Cassandra(多主机)。我们将在以后的博文中更详细地介绍Delta-Connector。
Delta事件的传输层基于Keystone平台的消息服务构建。
从历史上看,Netflix的消息发布是针对可用性而不是持久性进行优化的(参见以前的博客)。折中的结果是各种边缘场景中可能出现代理数据不一致。例如,不洁群首选举将导致消费者可能重复或丢失事件。
对于Delta,我们需要更强的持久性保证,以确保CDC事件能够到达次存储。为了实现这一点,我们提供了一个特殊用途的Kafka集群作为一个一等公民。下面是一些代理配置。
在Keystone Kafka集群中,不洁群首选举通常有利于生产者可用性。当一个不同步的副本被选为群首时,可能会导致消息丢失。对于新的高耐久性Kafka集群,为了防止这样的信息丢失,不洁群首选举被禁用。
我们还将复制因子从2增加到3,并将最小同步副本从1增加到2。写入此集群的生产者需要所有存储的应答,以确保3个副本中有2个拥有由生产者写入的最新消息。
当代理实例终止时,一个新实例将替换终止的代理。然而,这个新的代理将需要更新不同步的副本,这可能需要几个小时。为了提高此场景的恢复时间,我们开始使用块存储卷(Amazon Elastic block Store)代替代理上的本地磁盘。当新实例替换已终止的代理时,它现在就会附加已终止实例拥有的EBS卷,并开始捕捉新消息。这个过程将捕获时间从几小时减少到几分钟,因为新实例不再需要从空白状态复制。通常,存储和代理生命周期的独立大大降低了代理替换的影响。
为了进一步最大化我们的交付保证,我们使用了消息跟踪系统来检测由于极端情况造成的任何消息丢失(如分区群首的时钟漂移)。
Delta的处理层基于Netflix SPaaS平台构建,该平台提供Apache Flink与Netflix生态系统的集成。该平台提供了一个自助服务UI,在我们的容器管理平台Titus上管理Flink作业部署和Flink集群编排。自助服务UI还管理作业配置,并允许用户进行动态配置更改,而不必重新编译Flink作业。
Delta提供了一个基于Flink和SPaaS的流处理框架,该框架使用注解驱动的DSL(领域特定语言)来进一步抽象技术细节。例如,要定义一个通过调用外部服务来增强事件的步骤,用户只需编写以下DSL,框架会将其转换为一个由Flink执行的模型。
图3 Delta应用程序中用于增强事件的DSL示例
处理框架不仅缩短了学习曲线,还提供了常见的流处理功能,如重复数据删除、规范化、弹性和容错,以解决一般的操作问题。
Delta流处理框架由两个关键模块组成:DSL&API模块和运行时模块。DSL&API模块提供基于注解的DSL和UDF(用户定义函数)API,供用户编写自定义处理逻辑(如过滤器和转换)。运行时模块提供DSL解析器实现,用于构建DAG模型中处理步骤的内部表示。执行组件解释DAG模型,初始化实际的Flink操作符并最终运行Flink应用程序。
图4 Delta流处理框架架构
这种方法有几个好处:
Delta已经运行了一年多,在Netflix Studio的许多应用程序中都扮演了重要角色。它帮助团队实现搜索索引、数据仓库和事件驱动的工作流等用例。下面是Delta平台的高层架构图。
图5 Delta高层架构图
我们将在后续的博文中发布关于关键组件(如Delta-Connector和Delta流处理框架)的技术细节。敬请期待。如果你有任何问题,也可以随时联系作者。
本文最初发布于Netflix 技术博客,由InfoQ中文站翻译并分享。
原文链接:
领取专属 10元无门槛券
私享最新 技术干货