Netflix万亿级架构基石:如何使用预写日志构建弹性数据平台?
创始人
2025-12-26 10:20:26
0

引言

Netflix 在巨大的规模下运营,为数亿用户提供多样化的内容和功能。在幕后,确保跨不同服务的数据一致性、可靠性和高效运营是一个持续的挑战。许多关键功能的核心在于预写日志抽象的概念。在 Netflix 的规模下,每一个挑战都会被放大。我们遇到的一些关键挑战包括:

  • 数据库中的意外数据丢失和数据损坏
  • 跨不同数据存储的系统熵(例如,写入 Cassandra 和 Elasticsearch)
  • 处理对多个分区的更新(例如,在 NoSQL 数据库之上构建二级索引)
  • 数据复制(区域内和跨区域)
  • 大规模实时数据管道的可靠重试机制
  • 批量删除数据库导致键-值节点内存不足

以上所有挑战要么导致了生产事件或服务中断,消耗了大量工程资源,要么导致了定制解决方案和技术债务。在一个特定的事件中,开发人员发出了一条 ALTER TABLE 命令,导致数据损坏。幸运的是,数据前端有缓存,因此快速延长缓存 TTL 的能力,加上应用程序将变更写入 Kafka,使我们得以恢复。如果应用程序没有这些弹性功能,将会发生永久性的数据丢失。作为数据平台团队,我们需要提供弹性和保证,不仅要保护这个应用程序,还要保护我们在 Netflix 的所有关键应用程序。

关于实时数据管道的重试机制,Netflix 在巨大的规模下运营,故障(网络错误、下游服务中断等)是不可避免的。我们需要一种可靠且可扩展的方式来重试失败的消息,同时不牺牲吞吐量。

考虑到这些问题,我们决定构建一个系统,来解决所有上述问题,并继续满足 Netflix 在未来在线数据平台领域的需求。我们的预写日志是一个分布式系统,它捕获数据变更,提供强大的持久性保证,并可靠地将这些变更传递给下游消费者。这篇博文深入探讨了 Netflix 如何构建一个通用的 WAL 解决方案,以应对常见的数据挑战,提高开发人员效率,并支持高价值能力,如二级索引、为非复制存储引擎启用跨区域复制,以及支持广泛使用的模式,如延迟队列。

API

我们的 API 故意设计得很简单,只暴露必要的参数。WAL 有一个主要的 API 端点 WriteToLog,抽象了内部实现,确保用户可以轻松上手。

rpc WriteToLog (WriteToLogRequest) returns (WriteToLogResponse) {...}

/**

* WAL request message

* namespace: Identifier for a particular WAL

* lifecycle: How much delay to set and original write time

* payload: Payload of the message

* target: Details of where to send the payload

*/

message WriteToLogRequest {

string namespace = 1;

Lifecycle lifecycle = 2;

bytes payload = 3;

Target target = 4;

}

/**

* WAL response message

* durable: Whether the request succeeded, failed, or unknown

* message: Reason for failure

*/

message WriteToLogResponse {

Trilean durable = 1;

string message = 2;

}

命名空间 定义了数据存储的位置和方式,在抽象底层存储系统的同时提供了逻辑隔离。每个命名空间 可以配置为使用不同的队列:Kafka、SQS 或多个队列的组合。命名空间 还作为设置的中心配置,例如退避乘数或最大重试尝试次数等。这种灵活性允许我们的数据平台根据性能、持久性和一致性需求,将不同的用例路由到最合适的存储系统。

WAL 可以根据命名空间配置承担不同的角色。

角色一(延迟队列)

在下面的示例配置中,产品数据系统的命名空间 使用 SQS 作为底层消息队列,支持延迟消息。PDS 广泛使用 Kafka,故障(网络错误、下游服务中断等)是不可避免的。我们需要一种可靠且可扩展的方式来重试失败的消息,同时不牺牲吞吐量。那时 PDS 开始利用 WAL 处理延迟消息。

“persistenceConfigurations” : {

“persistenceConfiguration” : [

{

“physicalStorage” : {

“type” : “SQS” ,

} ,

“config” : {

“wal-queue” : [

“dgwwal-dq-pds”

] ,

“wal-dlq-queue” : [

“dgwwal-dlq-pds”

] ,

“queue.poll-interval.secs” : 10 ,

“queue.max-messages-per-poll” : 100

}

}

]

}

角色二(通用跨区域复制)

下面是使用 WAL 进行 EVCache 跨区域复制的命名空间配置,它将消息从源区域复制到多个目的地。它在底层使用 Kafka。

json

"persistence_configurations": {

"persistence_configuration": [

{

"physical_storage": {

"type": "KAFKA"

},

"config": {

"consumer_stack": "consumer",

"context": "This is for cross region replication for evcache_foobar",

"target": {

"euwest1": "dgwwal.foobar.cluster.eu-west-1.netflix.net",

"type": "evc-replication",

"useast1": "dgwwal.foobar.cluster.us-east-1.netflix.net",

"useast2": "dgwwal.foobar.cluster.us-east-2.netflix.net",

"uswest2": "dgwwal.foobar.cluster.us-west-2.netflix.net"

},

"wal-kafka-dlq-topics": [],

"wal-kafka-topics": [

"evcache_foobar"

],

"wal.kafka.bootstrap.servers.prefix": "kafka-foobar"

}

}

]

}

角色三(处理多分区变更)

下面是支持 Key-Value 中的 mutateItems API 的命名空间配置,其中多个写入请求可以发送到不同的分区,并且必须是最终一致的。下面配置中的一个关键细节是存在 Kafka 和 durable_storage。这些数据存储是为了支持两阶段提交语义所必需的,我们将在下面详细讨论。

"persistence_configurations": {

"persistence_configuration": [

{

"physical_storage": {

"type": "KAFKA"

},

"config": {

"consumer_stack": "consumer",

"contacts": "unknown",

"context": "WAL to support multi-id/namespace mutations for dgwkv.foobar",

"durable_storage": {

"namespace": "foobar_wal_type",

"shard": "walfoobar",

"type": "kv"

},

"target": {},

"wal-kafka-dlq-topics": [

"foobar_kv_multi_id-dlq"

],

"wal-kafka-topics": [

"foobar_kv_multi_id"

],

"wal.kafka.bootstrap.servers.prefix": "kaas_kafka-dgwwal_foobar7102"

}

}

]

}

一个重要的注意事项是,由于底层实现,对 WAL 的请求支持至少一次语义。

底层原理

核心架构由几个关键组件协同工作构成。

消息生产者和消息消费者分离: 消息生产者接收来自客户端应用程序的传入消息并将其添加到队列中,而消息消费者处理来自队列的消息并将其发送到目标。由于这种分离,其他系统可以根据其用例引入自己的可插拔生产者或消费者。WAL 的控制平面允许可插拔模型,根据用例,这允许我们在不同的消息队列之间切换。

默认带有死信队列的 SQS 和 Kafka:每个 WAL 命名空间 都有自己的消息队列,并且默认都有一个死信队列,因为可能存在瞬时错误和硬错误。使用 [Key-Value] 抽象的应用程序团队只需切换一个标志即可启用 WAL 并获得所有这些功能,而无需了解底层的复杂性。

  • Kafka 支持的命名空间:处理标准消息处理
  • SQS 支持的命名空间:支持延迟队列语义(我们添加了自定义逻辑,以超越在延迟、大小限制等方面强制执行的标准默认值)
  • 复杂多分区场景:使用队列和持久存储
  • 目标灵活性:添加到 WAL 的消息被推送到目标数据存储。目标可以是 Cassandra 数据库、Memcached 缓存、Kafka 队列或上游应用程序。用户可以通过命名空间配置和在 API 本身中指定目标。

WAL 架构图

部署模型

WAL 使用数据网关基础设施进行部署。这意味着 WAL 部署自动附带 mTLS、连接管理、身份验证、运行时和部署配置。

每个数据网关抽象(包括 WAL)都作为一个分片 部署。分片 是一个物理概念,描述一组硬件实例。WAL 的每个用例通常作为单独的分片 部署。例如,广告事件服务将请求发送到 WAL 分片 A,而游戏目录服务将请求发送到 WAL 分片 B,从而实现关注点分离并避免邻居干扰问题。

WAL 的每个分片 可以有多个命名空间。命名空间 是一个逻辑概念,描述一种配置。对 WAL 的每个请求都必须指定其命名空间,以便 WAL 可以将正确的配置应用于该请求。每个命名空间 都有自己的队列配置,以确保每个用例的隔离。如果一个 WAL 命名空间 的底层队列成为吞吐量的瓶颈,操作员可以通过修改命名空间 配置来动态添加更多队列。分片 和命名空间 的概念在所有数据网关抽象中共享,包括键值、计数器、时间序列等。命名空间 配置存储在全局复制的关系型 SQL 数据库中,以确保可用性和一致性。

WAL 部署模型

基于某些 CPU 和网络阈值,每个分片 的生产者组和消费者组将(分别)自动扩展实例数量,以确保服务具有低延迟、高吞吐量和高可用性。WAL 以及其他抽象,也使用 Netflix 自适应负载脱落库和 Envoy 来自动脱落超出特定限制的请求。WAL 可以部署到多个区域,因此每个区域都将部署自己的实例组。

无需改变核心架构即可解决不同种类的问题

WAL 无需改变核心架构即可解决多个数据可靠性挑战:

数据丢失预防: 在数据库停机的情况下,WAL 可以继续保存传入的变更。当数据库再次可用时,将变更回放到数据库。权衡是最终一致性而不是即时一致性,并且没有数据丢失。

通用数据复制: 对于像 EVCache(使用 Memcached)和 RocksDB 这样默认不支持复制的系统,WAL 提供了系统化的复制(包括区域内和跨区域)。目标可以是另一个应用程序、另一个 WAL 或另一个队列——它完全可以通过配置进行插拔。

系统熵和多分区解决方案: 无论是处理跨两个数据库(如 Cassandra 和 Elasticsearch)的写入,还是处理一个数据库中多个分区的变更,解决方案都是一样的——先写入 WAL,然后让 WAL 消费者处理变更。不再需要异步修复;WAL 自动处理重试和退避。

数据损坏恢复: 在数据库损坏的情况下,恢复到最后一个已知的良好备份,然后从 WAL 回放变更,省略有问题的写入/变更。

使用 WAL 和直接使用 Kafka/SQS 之间存在一些主要区别。WAL 是底层队列上的抽象,因此底层技术可以根据用例进行交换,而无需更改代码。WAL 强调简单而有效的 API,使用户免于复杂的设置和配置。当需要时,我们利用控制平面来转换 WAL 背后的技术,而无需应用程序或客户端干预。

WAL 在 Netflix 的使用情况

延迟队列

WAL 最常见的用例是作为延迟队列。如果应用程序希望在未来的某个时间发送请求,它可以将其请求卸载到 WAL,WAL 保证它们的请求将在指定的延迟后到达。

Netflix 的 Live Origin 处理并交付 Netflix 直播流视频块,将其视频数据存储在由 Cassandra 和 EVCache 支持的键-值抽象中。当 Live Origin 决定在事件完成后删除某些视频数据时,它会向键-值抽象发出删除请求。然而,短时间内的大量删除请求会干扰更重要的实时读/写请求,导致 Cassandra 性能问题和对传入直播流量的超时。为了解决这个问题,键-值首先将删除请求发送到 WAL,为每个删除请求设置随机延迟和抖动。WAL 在延迟之后,将删除请求发送回键-值。由于删除现在是在时间上分布更平的请求曲线,键-值随后能够毫无问题地将请求发送到数据存储。

通过延迟请求将请求随时间分散开

此外,许多使用 Kafka 流式传输事件的服务也使用 WAL,包括广告、游戏、产品数据系统等。每当 Kafka 请求因任何原因失败时,客户端应用程序将向 WAL 发送请求,以延迟重试 Kafka 请求。这为许多团队抽象了 Kafka 的退避和重试层,提高了开发人员效率。

为生产到 Kafka 的客户端提供退避和延迟重试

为从 Kafka 消费的客户端提供退避和延迟重试

跨区域复制

WAL 也用于全球跨区域复制。WAL 的架构是通用的,允许任何数据存储/应用程序加入以进行跨区域复制。目前,最大的用例是 [EVCache],我们正在努力让其他存储引擎加入。

EVCache 通过在多个区域部署 Memcached 实例集群进行部署,每个区域中的每个集群共享相同的数据。每个区域的客户端应用程序将读取、写入或删除同一区域 EVCache 集群的数据。为确保全局一致性,一个区域的 EVCache 客户端会将写入和删除请求复制到所有其他区域。为了实现这一点,发起请求的 EVCache 客户端将请求发送到对应于 EVCache 集群和区域的 WAL。

由于在这种情况下 EVCache 客户端充当消息生产者组,WAL 只需要部署消息消费者组。从那里开始,设置了多个消息消费者,每个对应一个目标区域。它们将从 Kafka 主题读取,并将复制的写入或删除请求发送到其目标区域的一个写入器组。然后,写入器组将继续将请求复制到同一区域的 EVCache 服务器。

通过 WAL 实现的 EVCache 全球跨区域复制

与我们的旧架构相比,这种方法的最大好处是能够为对延迟最敏感的应用程序从多租户架构迁移到单租户架构。例如,Live Origin 将拥有自己专用的消息消费者和写入器组,而对延迟不太敏感的服务可以是多租户的。这有助于我们减少问题的爆炸半径,并防止邻居干扰问题。

多表变更

WAL 被 [Key-Value] 服务用于构建 MutateItems API。WAL 通过在底层实现 2 阶段提交语义,使该 API 能够进行多表和多 ID 变更。对于本次讨论,我们可以假设键-值服务由 Cassandra 支持,其每个命名空间 代表 Cassandra 数据库中的某个表。

当键-值客户端向键-值服务器发出 MutateItems 请求时,该请求可以包含多个 PutItems 或 DeleteItems 请求。这些请求中的每一个都可以发送到不同的 ID 和命名空间,或 Cassandra 表。

text

message MutateItemsRequest {

repeated MutationRequest mutations = 1;

message MutationRequest {

oneof mutation {

PutItemsRequest put = 1;

DeleteItemsRequest delete = 2;

}

}

}

MutateItems 请求在最终一致性模型上运行。当键-值服务器返回成功响应时,它保证 MutateItemsRequest 中的每个操作最终都会成功完成。单个 put 或 delete 操作可能会根据请求大小被分区成更小的块,这意味着单个操作可能产生多个必须按特定顺序处理的块请求。

存在两种确保键-值客户端请求成功的方法。同步方法涉及客户端重试,直到所有变更完成。然而,这种方法带来了重大挑战;数据存储可能本身不支持事务,并且不提供整个请求成功的保证。此外,当一个请求涉及多个副本集时,延迟会以意想不到的方式发生,并且必须重试整个请求链。同时,同步处理中的部分失败可能会使数据库处于不一致状态,如果某些变更成功而其他变更失败,则需要复杂的回滚机制或导致数据完整性受损。最终采用异步方法来解决这些性能和一致性问题。

鉴于键-值的无状态架构,该服务无法在内部维护变更成功状态或保证顺序。相反,它利用预写日志来保证变更完成。对于每个 MutateItems 请求,键-值在收到各个 put 或 delete 操作时将其转发到 WAL,每个操作都带有一个序列号以保持顺序。在传输所有变更之后,键-值发送一个完成标记,指示完整请求已提交。

WAL 生产者接收这些消息,并将内容、状态和排序信息持久化到持久存储中。然后,消息生产者仅将完成标记转发到消息队列。消息消费者从队列中检索这些标记,并通过读取存储的状态和内容数据来重建完整的变更集,按照指定的顺序对操作进行排序。失败的变更会触发完成标记重新入队以进行后续重试尝试。

通过 WAL 实现多表变更的架构

通过 WAL 实现多表变更的序列图

结语

构建 Netflix 的通用预写日志系统教会了我们几个关键经验,这些经验指导了我们的设计决策:

可插拔架构是核心: 能够通过配置而不是代码更改来支持不同的目标(无论是数据库、缓存、队列还是上游应用程序),这对于 WAL 在各种用例中的成功至关重要。

利用现有的构建模块: 我们已经拥有了控制平面基础设施、键-值抽象和其他组件。在这些现有抽象的基础上进行构建,使我们能够专注于 WAL 需要解决的独特挑战。

关注点分离实现扩展: 通过将消息处理与消费分离,并允许每个组件独立扩展,我们可以更优雅地处理流量激增和故障。

系统会故障——仔细考虑权衡: WAL 本身也有故障模式,包括流量激增、慢消费者和非瞬时错误。我们使用抽象和操作策略(如数据分区和背压信号)来处理这些问题,但必须理解其中的权衡。

未来工作

我们计划利用 WAL 在键-值服务中添加二级索引。

WAL 也可以被服务用来保证向多个数据存储发送请求。例如,同时发送到数据库和备份,或者数据库和队列等。

作者丨Prudhviraj Karumanchi、Samuel Fu、Sriram Rangarajan、Vidhya Arvind、王云、John Lu 编译丨Rio

来源丨网址:https://netflixtechblog.com/building-a-resilient-data-platform-with-write-ahead-log-at-netflix-127b6712359a

dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn

相关内容

热门资讯

2025北京旅游“避坑”指南:... 做新媒体这么多年,后台私信里关于北京旅游的“血泪史”简直能出本书。“带爸妈去北京圆梦,结果在玉器店关...
启点创新智慧文旅小镇票务系统,... 启点创新智慧文旅小镇票务系统:全场景赋能,开启文旅新体验 在文旅产业从“规模扩张”向“质量提升”转型...
寒假限时告急!北京雪后红墙限时... 说句心里话,作为一个在南方艳阳里看了二十几年“假冬”的博主,我对北京冬天的向往,一直源于那句“一下雪...
商丘市梁园区:非遗美食好滋味 ... 12月24日,商丘市梁园区的街头寒意渐浓,而高老太烧鸡门店内却暖意融融。保鲜柜中一只只琥珀色的烧鸡泛...
四大古都联动打造世界级旅游目的... 本报讯(记者 郭歌)近日,省政府办公厅正式印发《黄河古都群世界级旅游目的地建设规划(2025—203...
匈牙利签证办理时间全解析,避坑... 近年来,匈牙利凭借性价比超高的留学资源、小众绝美的中欧风光,成为不少国人出境的热门选择,但签证办理周...
环历山生态旅游发展暨《历山保护... 12月25日,环历山生态旅游发展暨《历山保护区生态旅游方案》编制研讨会在沁水县召开。生态环境部相关专...