
本文翻译自 Matteo Merli 撰写的博文,原文参见:
https://streamnative.io/blog/reliability-that-thinks-ahead-how-pulsar-helps-agents-stay-resilient
AI Agent 运行在无法预测的环境中。Agent 可能会遇到瞬时错误,例如大语言模型(LLM)调用超时、数据库短暂宕机,或传感器消息(sensor msg)验证失败;消息系统如何应对这些故障至关重要。
本文将聚焦与 Agent pipeline 的可靠性相关的特性:消息确认(ACK)、重试和死信队列(DLQ),并对比 Apache Pulsar 与 Apache Kafka 的可靠性,展示 Pulsar 如何帮助 Agent 从容应对故障。
挑战:消息处理中的故障
设想一个 AI 工作流,其中每条消息会触发一系列动作。例如,一条消息可能指示 Agent 调用一个外部 API 或运行一次 ML 推理。如果某个动作在处理某条消息时失败,我们希望对其重试或特殊处理,同时不丢失消息也不阻塞整个 pipeline。我们还希望避免消息重复或不必要地乱序处理。传统的消息队列(如 JMS brokers)已提供每消息确认和死信队列来确保无消息丢失,且有问题的消息可以被隔离。下面我们来看 Pulsar 和 Kafka 在这方面有何不同:
Kafka 的 offset 模型
在 Kafka 中,消费者的进度通过提交 offset 来追踪,消费者会周期性记录"我已在分区 Y 中处理到消息 X 为止"。然而,Kafka 并不对单条消息进行确认,一次提交总是意味着该分区中之前的所有消息都已被处理。这通常被称为高水位(high-watermark)提交模型。
其影响在于:如果你的消费者在处理第 100 条消息时失败,它无法告诉 Kafka"只有第 100 条消息失败了"------它要么不提交 offset 100(这意味着重启后会重新处理该消息以及之后的所有消息),要么通过提交 offset 101 来跳过它(从而隐式地也确认了第 100 条消息,即使它处理失败了)。
Kafka 中没有内置的 NACK(否定确认)概念,无法说"重试这条消息,且不要推进 offset"。这种全有或全无的批量确认使得细粒度的错误处理变得棘手。开发者最终需要实现变通方案:一种常见模式是每个分区逐个处理消息,并在每条消息之后立即提交,以便精确知道是哪条消息引发问题。
如果某条消息失败,消费者可以在不提交该 offset 的情况下停止,从而实际上暂停该分区。但这意味着该分区中的其他消息(即使是已经拉取下来的)直到消费者重启、并且该消息被跳过或处理之后才能被处理。
另一种方案是团队实现自定义逻辑,将 offset 存储在外部(例如数据库),从而可以标记单条消息为已处理或失败;但这种做法很复杂,且不是 Kafka 原生支持的。
Kafka 中的重试和 DLQ
由于 Kafka 不追踪单条消息的确认,它也不会自动将失败的消息重定向到 DLQ。处理一条"毒药消息"(一直处理失败的消息)完全由应用程序负责。
常见做法是:如果处理失败,将该消息生产到一个特殊的"错误 topic"(即 DLQ)供后续分析,然后提交 offset 以在主 topic 中跳过它。
这种做法是可行的,但需要你手动编写代码,并确保原子性(你不能在失败后、写入 DLQ 前丢失消息)。有一些 Kafka 库/模式可以辅助实现,但仍然不是内置的;较新的 Kafka Streams API 或 Kafka Connect 的错误处理除外(而且这些仅限于特定框架)。
简而言之,Kafka 的设计假设消费者自己管理重试。如果消费者宕机,Kafka 会允许组内另一个消费者接管该分区,但默认情况下该新消费者会从最后提交的 offset 开始重新读取,这意味着它可能会重放部分消息(包括导致崩溃的那条消息,如果它尚未被提交)。这提供了至少一次(at-least-once)交付,但处理重复和故障的责任落在你身上。
接下来,看看 Pulsar 如何处理相同的场景:
Pulsar 的单条消息 ACK 与 NACK
Pulsar 消费者在处理完成后会向 broker 显式确认每一条消息(或一批消息)。这种确认是按消息追踪的,而不仅仅是按 offset。
如果消费者未能处理某条消息,它可以针对那一条消息发送否定确认(NACK)。NACK 告诉 Pulsar broker:"我无法处理消息 X,请稍后重新投递它。" 关键的是,这不会阻塞其他消息的确认。
例如,如果 Pulsar 消费者拉取了消息 100 和 101,其中 100 处理失败而 101 成功,消费者可以 ACK 101 并 NACK 100。消息 100 将在可配置的延迟后被重新投递(给同一个消费者或其他消费者,取决于订阅模式),而消息 101 由于已被 ACK,不会被重新处理。这种细粒度控制意味着一条慢消息或有问题的消息不需要阻塞整个 pipeline,其他消息可以继续流动。
Pulsar 还有一个确认超时特性:如果消费者在配置的时间内忘记 ACK 某条消息(比如处理过程中进程崩溃),broker 会自动认为它失败并重新投递。如何检测处理实例是否已为死信?broker 的 ACK 超时机制确保未确认的消息不会消失。
重试与死信 topic
Pulsar 在消费者层面支持自动重试和死信策略。你可以配置一个订阅,使得如果某条消息失败一定次数(即被 NACK 或反复超时),Pulsar 会将其路由到与该订阅关联的死信 topic(DLQ)。
这类似于传统消息队列系统中的"死信队列"概念。该消息随后会移出主流程,你的消费者组不会被它卡住,同时它被安全存储以供后续检查或特殊处理。
Pulsar 的 DLQ 特性是内置且易于启用的,而在 Kafka 中你需要手动创建和管理死信 topic。
此外,Pulsar 还可以在 DLQ 之外使用重试 topic。其原理是:Pulsar 会将消息重新排队到重试topic,尝试一定次数(可选地在两次尝试之间添加延迟),只有在达到最大重试次数后仍然失败时,才会进入 DLQ。原始消费者可以配置为在延迟后自动从重试 topic 消费,从而实现退避策略。
所有这些都可以通过配置完成。这种内置的重试机制为你"前瞻性"地考虑问题,简化了原本需要手动编写的重试循环代码。
无阻塞处理
由于单条确认能力,Pulsar 消费者如果愿意,不必严格按顺序处理。例如,在共享(Shared)订阅(即队列场景)中,一条慢消息不会阻止其他消费者处理该 topic 中的后续消息。即使是单个消费者,也可以使用多线程并行处理消息(拉取一批消息,逐个 ACK)。
在 Kafka 中,在分区内部进行并行处理是很危险的,因为你无法乱序确认消息;而 Pulsar 没有这一限制。举例来说,如果我们的 Agent 在一个 Pulsar 队列中收到 100 个任务,它可以将这些任务分发给多个工作线程,并在每个任务完成后进行确认。而 Kafka 消费者要么增加分区数(每个分区一个线程),要么在一个分区内顺序处理,以避免 offset 问题。
因此,Pulsar 的设计能带来更好的利用率和吞吐量,尤其是在某些消息耗时较长、另一些较短的不同负载场景下。
示例:Agent 故障恢复
假设我们有一个 AI Agent 负责监控新闻文章,对每篇文章事件,Agent 必须调用 LLM 进行摘要,然后将摘要索引到数据库中。假设某一篇文章导致 LLM 卡住或产生错误(可能因为太长或内容有问题)。下面分别展示 Kafka 和 Pulsar 会如何处理:
Kafka
Agent 从"articles" topic 中消费一个事件。如果使用自动提交,它可能已经将之前的消息标记为已消费,现在卡在了这条坏消息上。如果使用手动提交,它会暂不提交。无论哪种方式,该分区的处理都会暂停,直到问题解决。有以下几个选择:
-
崩溃或停止消费者,记录错误,然后从最后提交的 offset 重新启动(这将重新读取那条坏消息,除非代码或外部状态发生变化,否则很可能会再次失败)。
-
跳过该消息,捕获异常,将事件生产到"article_errors" topic供后续处理,然后提交超过它的 offset,使主消费者继续。但你必须小心地实现生产 + 提交,以确保不丢失数据。同时,你现在引入了一个次要流程(错误 topic),需要对其进行监控。
-
将可能失败的逻辑(LLM 调用)移至带外处理:例如,快速提交消息,异步处理 LLM 调用,这样消费者不会阻塞 Kafka。但如果异步处理失败,你仍然需要将该信息发送到另一个通道,因为 Kafka 已经将其标记为已完成。
这些方案都并非不可行,但它们都将实现可靠性的责任交给了开发者。
Pulsar
Agent 的消费者接收到了文章事件。如果 LLM 调用失败,消费者只需在代码中对那条消息调用"consumer.negativeAcknowledge(message)"。Pulsar 会将其记录为一次 NACK。在此期间,消费者甚至可以继续处理后续消息(取决于配置)。Pulsar 会在默认延迟(例如 1 分钟)后重新投递该消息,给系统时间恢复或处理临时问题。
如果该消息每次都失败(例如,该文章对 LLM 来说一直过大),比如尝试 3 次后,它将自动路由到死信topic。你的主消费者永远不会被它卡住,可以继续处理其他消息。
与此同时,你的团队可以通过一个单独的进程或监控仪表板从死信 topic "articles-DLQ" 中消费,检查这些有问题的事件出了什么错。团队可能会发现那些进入 DLQ 的文章格式不受支持,然后采取相应措施,但重要的是,整个 Agent 系统在遇到小故障时依然继续平稳运行,无需手动调整 offset 或紧急干预。
可靠性的另一个方面是系统在扩缩容消费者时的行为。Kafka 用户对再均衡(rebalance)过程很熟悉:如果你向一个组添加新消费者,或者某个消费者宕机,Kafka 会短暂暂停消费以重新分配分区所有权。在再均衡期间,该消费者组不会收到任何消息。在拥有大量分区的大型 Kafka 部署中,再均衡可能需要相当长的时间,这意味着一次扩缩容事件或单个消费者故障会导致整个组的处理延迟。
Pulsar 的共享订阅在这方面体验更平滑。由于任何消费者都可以获取任何消息,添加或移除消费者不需要显式的再均衡暂停。如果某个消费者离开,其未确认的消息会立即对其他消费者可用;如果新消费者加入,它会开始接收一部分消息,而无需 broker 侧重新分区。在共享订阅中扩缩容 Pulsar 消费者实际上无需停机。因为 Agent 系统可能需要动态调整以适应负载,这种优雅的扩缩容进一步增强了 Agent 系统的可靠性。
关键要点
-
单条消息确认:Pulsar 支持对单条消息进行确认,而 Kafka 只能通过推进 offset 水位来确认。这意味着 Pulsar 消费者可以独立地成功或失败处理各条消息,避免一条坏消息阻塞其他消息。
-
内置重试和 DLQ:Pulsar 原生支持消息重试,并在达到最大重试次数后将其发送到死信 topic。Kafka 缺乏内置 DLQ;实现它需要自定义逻辑并管理单独的错误 topic。Pulsar 的做法简化了错误处理,并提高了复杂 pipeline 中的可靠性。
-
否定确认(NACK):Pulsar 的 NACK 特性让消费者能够显式地发出失败信号,触发消息重新投递。Kafka 消费者没有原生的 NACK,只能要么不提交(导致再均衡或停顿),要么手动将消息重新排队到其他地方。Pulsar 的 NACK 与 ackTimeout 共同确保崩溃或慢消费者不会导致消息丢失或卡死。
-
扩缩容时的可靠性:Pulsar 支持无停机扩缩容消费者(共享订阅无需再均衡),这意味着系统能够在消费者故障或新增时适应变化,而不会暂停处理。相比之下,Kafka 消费者组的再均衡在分区重新分配期间会暂时停止消息处理。
对于可能 7x24 小时运行、处理不可预测输入的 AI Agent 来说,让消息层自动处理重试和故障将带来革命性的改变。你的 Agent 可以专注于如何处理数据,而 Pulsar 则确保即使在出现问题的情况下,数据的交付也坚如磐石。

Apache Pulsar 作为一个高性能、分布式的发布-订阅消息系统,正在全球范围内获得越来越多的关注和应用。如果你对分布式系统、消息队列或流处理感兴趣,欢迎加入我们!
Github:
https://github.com/apache/pulsar

扫码加入 Pulsar 社区交流群
最佳实践
互联网
腾讯BiFang | 腾讯云 | 微信 | 腾讯 | BIGO | 360 | 滴滴 | 腾讯互娱 | 腾讯游戏 | vivo| 科大讯飞 | 新浪微博 | 金山云 | STICORP | 雅虎日本 | Nutanix Beam | 智联招聘 | 达达 | 小红书|华为终端
金融/计费
腾讯计费|中原银行 | 平安证券 | 拉卡拉 | Qraft | 甜橙金融
电商
Flipkart | 谊品生鲜 | Narvar | Iterable
机器学习
物联网/芯片制造
应用材料|霖云芯创|云兴科技智慧城市 | 科拓停车 | 华为云 | 清华大学能源互联网创新研究院 | 涂鸦智能
通信
教育
推荐阅读
免费可视化集群管控 | 资料合集 | 实现原理 | BookKeeper储存架构解析 | Pulsar运维 | MQ设计精要 | Pulsar vs Kafka | 从RabbitMQ 到 Pulsar | 内存使用原理 | 从Kafka到Pulsar | 跨地域复制 | Spring + Pulsar | Doris + Pulsar | SpringBoot + Pulsar
