首页 时政热点 科技头条 智能AI 安全攻防 数码硬件 开发者生态 汽车 游戏 社会热点 开源推荐 医疗健康 归档 标签 关于

处理了 4000 万条消息后,我们总结了 Kafka 有序处理的几个关键设计

摘要

本文要点: 对于要求按会话顺序处理的工作负载,仅靠 Kafka 分区内的顺序保证还不够。当多个会话共用一个分区时,应用层仍需要路由逻辑,既要保证每个会话内部的顺序,也要允许不同会话并行处理。一致性哈希和按会话划分的工作协程可以维持会话亲和性。同一会话的消息总会到达同一个工作协程,并按顺序处理;互不相关的会话则可以继续并行。在原处理链路中重试,可以保留消息顺序。按会话运行的 goroutine 可以...

goroutine Kafka DLQ 本文要点 对于要求按会话顺序处理的工作负载 分区内的顺序保证还不够 当多个会话共用一个分区时 应用层仍需要路由逻辑 既要保证每个会话内部的顺序 也要允许不同会话并行处理
2026-10-20 1 阅读 约10分钟阅读 作者:Joshua Oluikpe
分享:
字号:
本文要点: 对于要求按会话顺序处理的工作负载,仅靠 Kafka 分区内的顺序保证还不够。当多个会话共用一个分区时,应用层仍需要路由逻辑,既要保证每个会话内部的顺序,也要允许不同会话并行处理。一致性哈希和按会话划分的工作协程可以维持会话亲和性。同一会话的消息总会到达同一个工作协程,并按顺序处理;互不相关的会话则可以继续并行。在原处理链路中重试,可以保留消息顺序。按会话运行的 goroutine 可以在处理下一条消息之前,先对失败消息进行带退避的重试,避免消息顺序颠倒,也省去了单独的重试状态机。按连续水位线提交,可以让并发处理具备可恢复性。偏移量只会提交到最高的连续完成位置,避免崩溃后因为较晚的消息已完成,就跳过较早但尚未完成的消息。要在生产环境中保证顺序,还需要做好运维层面的加固。再均衡时排空任务、背压、待处理记录缓冲、卡住的偏移量检测、幂等性、优雅关停和死信队列处理,都是系统在真实故障中维持顺序所必需的。 引言 在对话式 AI 中,消息顺序有着实际的语义影响。用户先说“帮我订一张去伦敦的机票”,接着又说“等等,改成巴黎”。系统必须按顺序处理这两条消息。如果第二条先于第一条被处理,AI 就会基于错误的上下文作出回应。用户收到的可能是伦敦机票预订确认,而不是改订巴黎的结果。随着系统规模扩大,数千个并发会话同时流经多阶段管道,即使极少发生的顺序错误,也会损害产品体验。 我负责为一个面向企业客户的实时对话式 AI 平台设计并实现 Go 语言核心 Kafka 消息层。在与团队密切协作的过程中,我们构建了一条包含四个处理阶段的管道:接入、自然语言理解(NLU)、大语言模型推理和消息送达。这里的顺序要求很严格:无论单个阶段耗时多久,同一会话中的消息都必须依次经过这四个阶段处理。 这套系统已在生产环境中处理了超过 4000 万条消息,覆盖数千个并发聊天会话,没有观察到顺序错误。这里的顺序保证并非来自把吞吐量控制在某个安全范围内,而是由一致性哈希和每个会话一个 goroutine 的结构设计提供。此后,我们还在测试中将发送端吞吐量提升到了每秒 10 万条以上,且没有发送错误。 架构概览 核心难点在于,Kafka 只能保证分区内的顺序,而单个分区承载着来自多个会话、交错到达的流量。要在并行处理多个会话的同时保留会话内的顺序,应用层需要自行完成路由。 我们的第一个实现采用扁平、固定大小的工作协程池,并将会话哈希到不同工作协程上。在持续负载下,一个正在重试的会话可能会阻塞同一工作协程上的其他会话;而协调重试又会导致内存队列不断增长。后来,我们将它替换为两层工作协程架构。 当前设计采用两层架构,同时解决了这两个问题。 第一层:SessionWorkers(分发层) 一组轻量级分发工作协程负责从 Kafka 接收记录,并根据会话 ID 进行路由。一致性哈希可以确保同一会话的消息总是到达同一个工作协程;不同会话则可以分散到多个工作协程上并行处理。 第二层:按会话划分的 goroutine(处理层) 每个活跃会话都由自己的 goroutine 处理,按消息到达的顺序逐条处理。这些 goroutine 按需创建,并在一段时间不活跃后移除,使资源消耗与活跃会话数量相匹配。 每个会话 goroutine 一次只处理一条消息。不同会话之间的并行度在设计上不设上限;同一会话内部的串行处理则由架构本身保证。 一个自然会被问到的问题是:为什么不完全依赖 Kafka 分区来保证顺序?“一个会话对应一个分区”并不可行,因为并发会话数达到数千时,分区数量会超过运维承受能力,并拖累 broker 性能。如果采用每个分区单线程处理,而分区中又交错承载多个会话的流量,效率也会很低,因为会话级别的顺序仍需要应用层协调。一致性哈希既能严格保证会话内顺序,也能让不同会话保持较高的并行度,同时避免分区数量无限增长。 不破坏顺序的重试 最初的重试实现使用了单独的重试 goroutine,并把后续消息暂存在内存中。这让状态机变得复杂,也带来了内存无限增长的风险。 现在的模型更简单:重试在会话 goroutine 内原地进行。对于每条消息,会话 goroutine 都会尝试处理。如果处理成功,就将消息标记为完成,并立即处理该会话的下一条消息。如果遇到可恢复的故障,例如下游服务超时,goroutine 就会等待一段时间,再按照带随机抖动的指数退避策略重试同一条消息。 由于重试期间 goroutine 会被阻塞,该会话的后续消息会留在队列中,无法越过失败的消息。如果故障不可恢复,例如消息数据无效,消息就会直接送入死信队列(DLQ),不再重试。失败消息成功处理或交由 DLQ 处理后,系统才会继续处理该会话的下一条消息。 由于会话 goroutine 在退避等待期间处于休眠状态,消息 3 处理完之前,消息 4 根本不可能开始执行。没有待排空的队列,没有重试通道,也没有需要重置的状态。goroutine 会被阻塞,新消息则会在会话通道中排在它后面,这正是我们想要的效果。 这种做法也省去了原始设计中必需的外部重试 goroutine。重试由会话 goroutine 自行处理。 错误分为两类: 第一类:可恢复错误——采用带随机抖动的指数退避策略重试。第二类:有毒消息/不可恢复错误——跳过重试,直接送入 DLQ。 偏移量提交与崩溃恢复 多个会话 goroutine 可能以不同顺序完成处理,因此直接提交最新完成的偏移量很危险。如果偏移量 104 先于 102 完成,提交 104 后一旦发生崩溃,102 就会被整个跳过。 我们通过一个按分区管理偏移量的跟踪组件来维护连续水位线。对于每个分区,该组件都会跟踪 InFlight(正在处理的偏移量)和 Completed(已完成的偏移量)。提交循环会按固定间隔推进水位线。 关键在于,提交位置只能推进到最高的连续已完成偏移量。尚未完成消息造成的空缺会阻止水位线不安全地前进。 发生崩溃后,系统会从偏移量 101 开始重放。偏移量 100 已经处理过,而偏移量 102 和 104 可能会被重新处理。为了让重放尽可能安全,管道会携带稳定的事件 ID 用于去重。这可以减少重复处理,但外部副作用仍取决于下游服务提供的幂等性保证,因此重放并不等同于恰好一次执行。 这种设计有意用少量额外重放换取确定性的恢复行为。在分布式系统中,安全地重放数据,胜过冒着静默丢失数据的风险。 在少数情况下,中断的交接可能会让提交水位线留下一个永久空缺。为了避免分区因此无限期停滞,协调器会跟踪尚未解决的空缺;超过配置的处理超时时间后,就会越过那些已无法完成的空缺。这是最后手段,是一种用可用性换取完整性的取舍:系统可能会放弃处理一条永久卡住的消息,以恢复分区进度。这类情况应视为运维故障,而非正常成功的消息交付。 运维层面的加固 四个核心机制:一致性哈希、按会话划分的 goroutine、连续水位线提交和原地重试,解决了顺序问题。要让系统在生产环境中具备韧性,还需要增加几个层次的保护。 再均衡安全 发生再均衡时,消费者会先将分区标记为正在撤销,避免继续向该分区路由新记录。随后,它会等待正在处理的会话任务完成,每 50 毫秒检查一次,并遵守 broker 的再均衡超时限制。处理
这篇文章对您有帮助吗?

订阅66必读

每日精选科技资讯,直达你的邮箱