Article: Building a Session-Ordered Kafka Pipeline in Go

TL;DR · AI 摘要
Building a Session-Ordered Kafka Pipeline in Go - InfoQ InfoQ Homepage Articles Building a Session-Ordered Kafka Pipelin...
核心要点
- 主题聚焦:Article: Building a Session-Ordered Kafka Pipeli
- 来源:InfoQ,建议结合原文判断细节。
- AI 分析暂不可用,本条为保底评分与摘要。
在 Go 中构建基于会话顺序的 Kafka 流水线 - InfoQ
InfoQ 首页 Articles 在 Go 中构建基于会话顺序的 Kafka 流水线
架构与设计
InfoQ 认证高绩效团队计划(在线,11 月 17 日):为您的团队构建更清晰的运营模型。
在 Go 中构建基于会话顺序的 Kafka 流水线
2026 年 10 月 7 日 11 分钟阅读
作者:
- Joshua Oluikpe
审阅者:
- Rafał Gancarz
#### 关注我们
YouTube
232K 粉丝
26K 粉丝
新
RSS
19K 读者
X
57.1k 粉丝
21K 喜欢
Bluesky
收听本文 -
0:00
音频准备播放
您的浏览器不支持音频元素。
正常
1.25x
1.5x
喜欢
新下拉阅读列表
- 阅读列表
关键要点
- 仅靠 Kafka 分区排序不足以满足会话有序的工作负载。当多个会话共享分区时,应用层仍需路由逻辑来在每个会话内保持顺序,同时允许跨会话并行处理。
- 一致性哈希和每会话工作者提供会话亲和性。同一会话的消息始终落在同一工作者上并按顺序处理,而无关会话则继续并行处理。
- 原地重试可保持顺序。每个会话的 goroutine 可在处理下一条消息前对失败消息进行重试并退避,避免消息超越并消除对独立重试状态机的需求。
- 连续的水印提交使并发处理可恢复。仅提交到最高连续完成的偏移量,防止后续完成的消息在崩溃后导致早期未完成的消息被跳过。
- 生产环境的排序保证需要操作加固。需要流量卸载、反压、待处理记录缓冲、卡住偏移检测、幂等性、优雅关闭和死信队列处理,以在真实故障条件下保持顺序。
介绍
在对话式 AI 中,消息顺序具有语义重要性。当用户发送“帮我预订飞往伦敦的航班”后又发送“实际上,改为巴黎”,系统必须按顺序处理这些消息。如果第二条消息超越第一条,AI 将响应错误的上下文。用户会收到伦敦的预订确认而非巴黎的重定向。在大规模场景下,当数千个并发会话流经多阶段流水线时,即使罕见的顺序违规也会损害产品体验。
我领导设计并实现了为实时对话式 AI 平台(服务企业客户)核心 Kafka 消息层的 Go 实现。与团队紧密合作,我们构建了一个流水线,每条消息都经过四个处理阶段:摄入、自然语言理解(NLU)、大语言模型(LLM)推理和交付。顺序要求非常严格:在所有四个阶段中,同一会话的消息必须按顺序处理,无论单个阶段耗时多长。
该系统在生产环境中已处理超过 4000 万条消息,数千个并发聊天会话中未观察到顺序违规。此处的顺序保证并非源于保持在某个安全吞吐量之下,而是结构性的,通过一致性哈希和每会话一个 goroutine 实现。此后,我们在测试中将发送端吞吐量推高至每秒 10 万条消息,且没有发送错误。
架构概述

核心挑战在于 Kafka 在分区内部保证顺序,但单个分区需要处理来自多个会话的交织流量。为了在并行处理会话的同时保持会话内部顺序,应用层需要实现自己的路由机制。
我们的首次实现采用扁平化的固定工作线程池,通过会话 ID 哈希分配到具体线程。在持续负载下,一个重试会话可能阻塞同一线程上的其他无关会话,而重试协调机制又会导致内存队列持续增长。我们将其替换为两级工作线程架构。
Figure 1. 会话有序处理的两级工作线程架构(图片来源:作者原创)
当前设计通过两级架构同时解决了这两个问题。
第一级:SessionWorkers(调度层)
一组轻量级的调度线程从 Kafka 接收记录,并根据会话 ID 进行路由。一致性哈希确保同一会话的消息始终发送到相同线程,而不同会话可以在不同线程上分布处理以实现并行。
第二级:每会话协程(处理层)
每个活跃会话由独立协程处理,按消息到达顺序逐条处理。这些协程按需创建,在一段时间无活动后销毁,使资源使用量与活跃会话数量成比例。
每个会话协程一次只处理一条消息。跨会话的并行性设计上是无限的;会话内部的串行化通过架构保证。
一个自然的问题是:为什么不完全依赖 Kafka 分区实现顺序?按会话划分分区不可行,因为数千个并发会话会导致分区数量超出运营限制并降低代理性能。单线程处理每个分区的模型在处理交织会话流量时效率低下,因为会话级排序仍需要应用层协调。一致性哈希在不无限增长分区数量的前提下,提供了严格的会话内顺序和高会话间并行性。
无序破坏的重试机制
首次重试实现使用独立的重试协程,并在内存中排队后续消息,这使状态机变得复杂并引入了无界内存风险。
当前模型更简洁。重试直接在会话协程内完成。对于每条消息,会话协程尝试处理。若处理成功,消息标记为完成,并立即处理会话中的下一条消息。若失败是临时性(如下游服务超时),协程会通过带抖动的指数退避等待后重试同一条消息。
由于协程在重试期间被阻塞,该会话的后续消息会保持排队状态,无法超越失败消息。若失败是不可恢复的(如消息数据无效),消息会直接路由到死信队列(DLQ)而不再重试。一旦失败消息成功处理或被 DLQ 处理,会话处理会继续处理下一条消息。
由于会话协程在退避期间处于休眠状态,消息 4 实际上无法在消息 3 完成前执行。没有需要清空的队列,没有重试通道,没有需要重置的状态。协程被阻塞,新消息会堆积在会话通道中,正好处于应有的位置。
图 2. 会话 goroutine 内部的原地重试(图片来源:作者创作)
这也消除了原始设计中所需的外部重试 goroutine。会话 goroutine 负责处理重试。
错误分类分为两个层级:
- 第一层级,瞬时错误:使用指数退避和抖动进行重试。
- 第二层级,毒药/不可恢复错误:跳过重试并直接路由到死信队列(DLQ)。
偏移提交与崩溃恢复
当许多会话 goroutine 以无序方式完成时,简单地提交最新完成的偏移量是危险的。如果偏移量 104 在 102 之前完成,提交 104 会导致崩溃时完全跳过 102。
我们通过每个分区的偏移量跟踪组件管理的连续水印来解决这个问题。对于每个分区,它跟踪 InFlight(当前正在处理的偏移量)和 Completed(已完成的偏移量)。在定期提交间隔期间,提交循环会推进水印。
图 3. 连续水印提交(图片来源:作者创作)
关键见解是仅提交到最高连续完成的偏移量。飞行中消息的间隙会作为阻塞器,防止任何不安全的推进。
在崩溃后,重放从偏移量 101 开始。偏移量 100 已经被处理,而偏移量 102 和 104 可能会被重新处理。管道设计通过携带稳定的事件 ID 用于去重,尽可能使重放安全。这减少了重复处理,但外部副作用仍依赖下游服务的幂等性保证,因此重放并不等同于精确一次执行。
该设计有意通过最小化重放工作来换取确定性恢复。在分布式系统中,安全重放优于冒着数据丢失的风险。
在极少数情况下,中断的交接可能导致提交水印中出现永久性间隙。为了避免分区无限期停滞,协调器会跟踪未解决的间隙,并在配置的处理超时后,推进那些无法完成的间隙。这是一种最后的可用性权衡:系统可能为了恢复分区进度而牺牲完整性,永久阻塞的消息应被视为操作故障而非正常成功交付。
操作加固
四个核心原语(一致性哈希、每会话 goroutine、连续水印提交和原地重试)解决了排序问题。生产环境的弹性需要额外的几层保障。
重新平衡安全性
在重新平衡期间,消费者首先将分区标记为撤销状态,以防止向其路由新记录。然后它等待正在进行的会话工作完成,每 50 毫秒检查一次,并遵守代理的重新平衡超时。一旦处理完成,最终的连续水印将被提交,分区特定状态将被清除,所有权可以安全地转移到另一个消费者。
图 4. Kafka 消费者重新平衡期间的安全分区交接(图片来源:作者创作)
反压机制
当工作者饱和时,系统不会丢弃消息或崩溃。它会暂停获取。消息路由使用非阻塞通道发送。如果工作者的分发通道已满,会触发反压:分区将被暂停,一个监控 goroutine 每秒轮询容量,当负载低于配置的恢复阈值时恢复。
仅暂停是不够的。暂停消息拉取虽然能阻止新记录从代理端到达,但当前批次中的记录可能已经被拉取。这些记录按分区进行缓冲,并保持在途状态,防止数据丢失或水印提前推进。
一个后台监控器会在缓冲区容量可用时清空缓冲区。只有当缓冲区为空且触发工作线程具备足够容量时,拉取操作才会恢复,从而防止新记录超越旧的缓冲消息。这提升了会话隔离性,但无法消除分区级别的耦合:暂停一个分区可能会暂时延迟分配给它的无关会话。
卡住偏移量检测
由于下游调用无法终止或死锁导致会话goroutine无限阻塞时,该分区的提交水印会被冻结。该分区之后的所有偏移量都将无法提交。
最初的检测方法将持续非空的在途偏移量集合视为卡住的分区,但在持续流量下会产生误报。当前方法会单独跟踪每个偏移量,仅当其处理时间超过配置的超时时才标记为异常。卡住的偏移量随后可以被强制完成并记录到DLQ中,使分区能够继续推进。
追踪与自动扩展信号
生产环境可观测性需要额外的两项保障:跨异步Kafka边界的追踪链接,以及在分区暂停期间独立的滞后报告。这能保持追踪准确性,并确保在背压期间自动扩展仍能接收到有用的滞后信号。
基于会话的有序生产
顺序保证不仅限于消费者。在异步发送时,生产者也需要会话亲和性。
异步生产使用与消费者相同的哈希策略,确保同一会话的消息被路由到相同的生产者工作线程,并按先进先出顺序发送。发送失败的消息会升级到代理的DLQ,而关闭时会先排空并刷新待处理写入,再关闭Kafka客户端。
优雅关闭与部署安全
关闭期间,消费者会停止拉取新记录,并为活动会话工作线程提供完成时间。排空完成后或达到配置超时后,会在Kafka客户端关闭前提交最终连续的水印和待处理的DLQ写入。任何未提交的偏移量在重启后会被重放,这是安全的,因为处理过程是幂等的。
基准测试与验证
我们通过两个阶段验证了架构:首先使用合成基准测试验证顺序性、重试行为、吞吐量和受控故障下的延迟,然后对部署的流水线进行生产规模负载测试。
测试环境
以下基准测试运行在AWS EC2 c5.2xlarge实例(8 vCPU,16GB RAM)上,包含一个3节点Kafka集群(每个主题10个分区,复制因子3)。每个消费者实例在组内3个消费者实例中处理4个分区。
200ms目标适用于摄入、路由、交接和交付过程中的消息开销;可变的LLM推理时间超出该预算范围。
合成基准测试结果
第一次验证阶段使用了模拟的下游处理而非真实服务,并注入了10%的瞬时错误以验证重试机制在部分失败情况下不会破坏顺序。因此表1中的延迟数据反映的是基准测试框架和消息传输路径的性能,而非实际LLM推理延迟。
| 指标 | 结果 | 目标 | |------------------|--------------|--------------| | 原始吞吐量 | 14,027 msg/s | >10k msg/s | | 端到端延迟p50 | 11.7ms | <200ms | | 端到端延迟p95 | 115ms | | | 端到端延迟p99 | 185ms | | | 顺序违规 | 0 | | | 会话亲和性 | 100% | |
表1. 合成基准测试结果 在50,000条消息、10个并发会话和10%故障注入的测试中,未出现任何违规情况。
生产级负载测试
合成基准测试验证了正确性,但未验证生产级行为。因此我们对部署的流水线进行了基于WebSocket的测试,使用1,000个并发会话将发送速率推向100,000 msg/s。
在目标发送速率为50,000 msg/s、1,000个并发会话持续4分钟多的测试中:
| 目标发送速率 | 50,000 msg/s | |--------------|--------------| | 实际发送速率 | 48,805 msg/s | | 发送消息数 | 11,959,510 | | 发送错误 | 0 (0.00%) |
446.8ms 2,633.9ms 2,843.1ms
表2. 生产级负载测试结果 在50,000 msg/s的目标下,部署的流水线实现了48,805 msg/s的发送速率且无发送错误。尾部延迟超出200ms的消息预算,原因是WebSocket路径超过了API网关的消息速率配额,而非会话排序机制导致。尽管测试提前终止,但仍证明了在50,000 msg/s测试之外存在额外的发送侧容量。
高吞吐量负载测试测量了吞吐量、延迟和错误率,但未独立验证每会话的顺序。合成基准测试中通过显式顺序验证,记录到在14,027 msg/s和10%故障注入下零违规。在全负载测试规模下重新运行相同的序列单调性检查仍然是验证步骤。
生产性能
合成基准测试验证了设计,生产环境验证了其操作可行性。自生产部署以来,我们在实际流量和真实场景下处理了超过4000万条消息,未观察到任何顺序违规,包括可变LLM推理时间、网络抖动和有机负载峰值等场景。此生产声明基于操作观察和监控,而非对每条消息序列的彻底检查。这4000万条消息中仅有460条被路由到DLQ(毒药消息、不可恢复错误和卡住偏移量强制完成的总和),占比约0.002%。
我们如何验证顺序
通过在生产端为每个会话分配单调递增的序列号来检测顺序违规。消费者端的测试框架会记录每个会话处理消息的顺序:
会话亲和性通过独立验证:每条消息记录处理它的工作者节点,测试会断言特定会话的所有消息均由且仅由一个工作者处理。
在生产环境中,消费者延迟、处理时长、重试次数、DLQ计数、待处理缓冲区深度和反压事件可提供水印停滞或意外会话-工作者重新分配的操作信号。这些是有用的指标,但并非直接的序列级顺序检查。
为什么不使用Redis Streams?
在确定使用Kafka并添加自定义消费者层之前,我们曾考虑过Redis Streams。Redis Streams提供了消费者组、消息确认和合理的顺序保证,对于更简单的任务负载来说可能是一个不错的选择。
对于我们的任务负载,Redis Streams仍然需要在应用层进行会话亲和性的协调,并为恢复操作添加额外的记录管理。而Kafka的分区和偏移量模型,结合应用层的会话感知路由,为严格的会话级顺序、重放操作和跨会话的高并行性提供了更坚实的基础。
结论
严格的会话顺序要求不仅需要Kafka的分区级保证。在本设计中,一致性哈希提供了会话亲和性,每个会话的goroutine序列化处理和重试操作,连续的水印使并发处理具备可恢复性,再平衡排水机制保护了分区交接过程。
围绕背压、卡顿任务和优雅关闭的生产环境强化措施,将这些架构保证转化为能够经受真实运行条件考验的系统。该系统已处理了超过4000万条生产消息,且未观察到任何顺序违规情况。
这些模式适用于任何消息顺序具有语义意义的场景,包括对话系统、工作流引擎和事务处理。
作者部分主容器
关于作者
章节标题
每个作者的主容器
#### Joshua Oluikpe
显示更多
显示更少
#### 本内容属于Apache Kafka主题
##### 相关主题:
- 架构与设计
- AI、ML与数据工程
- 事件流处理
- Redis
- 事件驱动架构
- 键值存储
- NoSQL
- 架构
- 流处理
- Apache Kafka
- 数据库
- 相关编辑内容
- 相关赞助商 《实用智能体编码实践手册》
- 相关赞助商 大规模自动化代码转换。修复漏洞、迁移框架、现代化所有仓库中的遗留代码。免费试用Moderne。
InfoQ新闻通讯
每周内容精选,每周二发送。加入超过25万名高级开发者的社区。查看示例
我们保护您的隐私。