架构 (IT)系统架构师

详细阐述一个行星级、有状态流处理平台的架构,该平台能够提供事件时间窗口聚合的准确一次语义,支持无限数据流,能够在拓扑改变期间自动重新平衡,并保持亚秒级的检查点恢复,同时支持多种处理语言和跨区域的主动-主动复制,而无需共享存储依赖?

用 Hintsage AI 助手通过面试

问题历史

流处理架构从Apache Storm的至少一次处理演变到由Apache FlinkSpark Structured Streaming引入的现代准确一次保证。随着企业从批处理Lambda架构迁移到持续的Kappa流,复杂性从简单的转换转向管理用于窗口聚合和会话化的分布式状态。数据主权要求和区域延迟限制的出现使得在不依赖共享NFSSAN存储的情况下进行主动-主动部署,带来了地理故障转移期间状态一致性的新挑战。

问题

有状态流处理需要在处理节点上本地维护数GB的操作员状态(键控窗口、会话存储),同时每秒接收数百万个事件。准确一次语义要求在三个组件之间进行原子提交:源偏移量跟踪、状态后端更新和汇写入。没有共享存储的跨区域主动-主动复制在网络分区发生时引入了分脑风险,而自动扩缩容则需要在不中断飞行中记录或违反处理时间保证的情况下进行实时状态迁移。支持多种语言(JavaPythonGo)通常会导致序列化开销或特定语言的运行时锁定。

解决方案

该架构采用解耦设计,使用Apache KafkaApache Pulsar作为统一日志,处理节点在Kubernetes上运行,利用语言无关的gRPC边车实现多语言支持。状态管理使用嵌入式RocksDB,配合异步增量检查点到S3兼容的对象存储,由轻量级的分布式协调服务(etcdZooKeeper)协调。通过Chandy-Lamport快照算法实现准确一次语义,使用两阶段提交(2PC)协议用于事务性汇(Kafka事务或幂等JDBC写入)。跨区域复制则利用基于日志的状态传输,通过Kafka MirrorMaker 2Pulsar Geo-Replication实现,并通过基于CRDT的可交换计数器进行聚合和版本化主键所有权来解决冲突。

问题的答案

该平台由四个逻辑层次组成:摄取、处理、状态管理和协调。

摄取层

Apache Kafka集群在多个区域中运行,使用MirrorMaker 2实现双向主题复制。生产者幂等性和事务ID确保即使在生产者故障转移期间,也能实现准确一次摄取。

处理层

Apache Flink或类似的流处理器作为Kubernetes有状态集群运行。每个TaskManager暴露一个gRPC边车,接受Protobuf序列化的任务,使得PythonGo用户定义函数(UDF)能够在gRPC容器内执行,同时Java运行时管理状态和检查点。JobManager使用基于记录键的恒定哈希将拓扑分片到TaskManagers。

状态管理

操作员状态后端使用支持增量检查点的RocksDB。检查点每15秒异步写入增量状态变化到区域S3存储桶。为了跨区域一致性,主动-主动部署使用LWW-Element-Set CRDT用于单调聚合(计数、求和)和主键亲和性用于非交换操作。在区域故障转移期间,备用TaskManagers从S3使用Savepoints恢复状态。

准确一次保证

系统通过以下方式实现端到端的准确一次:

  • 两阶段提交:汇参与Flink的TwoPhaseCommitSinkFunction,在检查点期间预提交到KafkaPostgreSQL,并在成功的检查点通知后提交。
  • 幂等生产者:上游Kafka生产者使用带有序列号的幂等交付进行去重重试。
  • 事务隔离:检查点充当事务边界;未提交的数据对下游消费者保持不可见。

生活中的实例

一个全球共享乘车平台需要实时冲击定价计算,聚合各个地理哈希区域的司机可用性和乘车需求,跨越AWS us-east-1AWS eu-west-1。之前的架构使用单主Redis集群,存在复制延迟,导致在区域故障期间存在2秒的故障恢复窗口,使得定价计算产生过时或重复的冲击倍数,导致错误的费用计算和客户投诉。

解决方案 1:主动-被动与共享存储

团队考虑在各区域挂载EFS(共享NFS)用于状态存储。优点:简化故障转移,具有单一写入语义,强一致性。缺点EFS延迟超过100ms,跨区域访问,违反了50ms的处理SLA;此外,NFS写入一致性问题导致网络分区期间检查点损坏。

解决方案 2:Lambda架构

使用Kafka Streams实现速率层,使用Spark实现批处理层进行修正。优点:通过不可变日志提供容错,简化恢复。缺点:维护双代码路径的操作复杂性;批处理修正到达时间太晚,不适合需要亚秒级准确性的冲击定价。

解决方案 3:主动-主动流处理与CRDTs

在两个区域内部署Apache Flink,使用RocksDB状态、增量S3检查点和基于CRDT的计数器进行乘车数量跟踪。优点:本地处理延迟低于20ms,自动解决并发区域更新的冲突,零停机故障转移。缺点:需要对聚合进行重构以满足交换性要求(使用G-CountersPN-Counters),区域检查点的存储成本增加。

团队选择了解决方案 3,因为业务要求99.99%的可用性并具备亚秒故障转移,无法容忍解决方案 1的2秒窗口或共享存储的延迟。他们实现了用于司机计数的G-Counters和用于最新定价倍数的LWW-Registers

结果

系统实现了准确一次的冲击定价计算,在两个区域的p99延迟为15ms。在模拟的us-east-1故障期间,eu-west-1无缝继续处理本地复制的状态,没有重复的费用计算。检查点恢复时间平均为800ms,远低于亚秒的要求。

候选人常常忽视的内容

检查点间隔调整如何与有状态流处理器中的背压机制交互?

许多候选人优化检查点间隔以提高恢复时间,而不考虑背压传播。当检查点障碍因背压而缓慢对齐时,Chandy-Lamport算法会暂停管道执行,可能导致级联超时。正确的方法是使检查点超时与背压阈值保持一致,在高负载时使用未对齐检查点(障碍超过缓冲区),并分离同步与异步检查点阶段。需利用RateLimiter配置进行流行的增量检查点,防止SST压实超负荷磁盘I/O,加剧背压。

至少一次交付与幂等汇的结合与真正的准确一次处理语义之间的根本区别是什么?

幂等汇保证重复处理产生相同的输出状态(例如,PostgreSQLHBase中的UPSERT操作),但在重试过程中会暴露中间状态。如果一个汇写入记录A、B,然后崩溃并重试写入A、B、C,下游观察者暂时会看到A、B、A、B、C的顺序,随后进行去重。真正的准确一次(有效一次)使用事务隔离,其中预提交的数据在检查点完成之前保持不可见。这需要汇支持事务(例如,Kafka事务与隔离级别=read_committed)或两阶段提交协议。候选人通常会错过幂等性解决了正确性问题,但在恢复过程中的一致性/可见性问题仍未得到解决。

事件时间窗口如何在跨区域故障转移场景中处理迟到数据?

当故障转移从区域A转移到区域B时,区域A的网络缓冲中的飞行记录可能丢失或延迟超出水印边界。候选人常常建议无限期延长水印,这会打破窗口完整性保证。正确定义的架构使用侧输出(在Flink术语中)用于迟到数据捕获,结合允许的延迟规范。在故障转移过程中,系统应使用带时间戳的S3 Savepoints来恢复窗口,然后将故障区域的死信队列中的迟到记录合并到后续窗口或触发特定的迟到数据处理程序。此外,水印生成必须在跨区域之间保持幂等;使用墙钟时间来生成水印会导致故障转移期间的差异,因此水印必须基于两个活动区域的单调事件时间提取。