想象一条街道上有许多身手矫健的骑手:你递给他们一张纸条,他们飞快地送到对方手里;如果对方不在,纸条就随手扔掉了。这套系统快得惊人、极简到几乎没有规则——但也正因为如此,纸条一旦送出,风一吹就再也找不回来。NATS 核心,就是这群只负责“送到即走”的骑手。而 JetStream,则像是给骑手们配了一座智能驿站仓库:包裹进仓后自动编号、按货架顺序排好,分拣员扫码取件、签收回执;即使骑手半路摔跤,仓库里的货物也分毫不差。本文带你参观这座驿站:货架如何设计(Stream)、分拣员如何工作(Consumer)、回执怎么签(ACK)、怎么防止重复接单(消息去重),以及它和传统巨型物流中心(Kafka)的差异。

岗位关联

理解 JetStream 的流存储与消息确认模型,能让你在微服务消息选型中作出“轻性能、低成本”的可靠决策——这是后端工程师走向架构师的关键一步。

目录
  1. 可视化演示
  2. 架构定位
  3. 流与消费者
  4. 投递语义
  5. ACK 机制
  6. 消息去重
  7. Kafka 对比
  8. 集群与容量
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示Spark RDD的惰性求值机制:转换操作构建血缘DAG图,行动操作触发执行,节点故障时通过血缘重建分区。

本节配有交互动效(进入视口后自动加载,约 3KB)。

转换模式(惰性求值) 转换操作(map/filter)仅构建血缘DAG,不立即执行 每个RDD记录父依赖关系,形成有向无环图 RDD \u2192 transform \u2192 new RDD 转换操作创建新的RDD并记录血缘关系

架构定位

回到骑手比喻。NATS 核心就是一个“即时送达、无仓储”的快递网络:生产者和消费者都在线时,消息秒达;一旦有一方不在线,消息立刻蒸发。它的默认语义是 at-most-once——至多送一次,送了就完事,没人签收也不补送。

JetStream 的诞生,本质上是在骑手网络旁边加了一座仓库。它不是一个独立安装的中间件,而是内嵌在 nats-server 程序中的持久化层:消息先落仓(磁盘),再按货架号(序列号)组织,消费者可以随时来取件,哪怕取件人迟到了十分钟,包裹也还好好地躺在架子上。

从架构上看,NATS 提供两条路径:核心路径只在内存中转发消息,追求极低延迟;JetStream 路径把消息写入流存储,并用 Raft 协议在多个节点间复制副本,保证节点崩溃后数据仍在。代价是每条消息多一次落盘与回执的开销,这就是持久化必须支付的成本。

所以选型时先问自己:这笔“快递”能不能丢?能丢,走核心路径;不能丢,就进 JetStream 仓库。架构定位的本质,就是为每一类消息选择正确的仓储方式。

T端到端=T发布+T落盘+T回执\displaystyle T_{\text{端到端}} = T_{\text{发布}} + T_{\text{落盘}} + T_{\text{回执}}
这条式子说明:开启 JetStream 后,一条消息从发布到确认,多了“落盘”和“回执”两段耗时。架构选型的本质,就是衡量这笔延迟是否可接受。
  1. 端到端总时延消息从发布方发出到消费者确认收到所经历的总时间。
    T端到端\displaystyle T_{\text{端到端}}
  2. 发布传输发布方把消息通过网络送到 JetStream 服务器的时间。
    T发布\displaystyle T_{\text{发布}}
  3. 落盘写入JetStream 将消息追加写入磁盘存储文件的耗时。
    T落盘\displaystyle T_{\text{落盘}}
  4. 回执确认消费者处理完成后把 ACK 送回服务器的一段额外往返。
    T回执\displaystyle T_{\text{回执}}

流与消费者

驿站里每个货架都只放同一类包裹,按到达顺序从 1 号位摆到 N 号位——这就是 Stream(流)。分拣员不会每次从头看完整条货架,手里只捏着一张“取件进度卡”,记录自己取到了几号位——这就是 Consumer(消费者)。

在 JetStream 中,Stream 是消息的有序写入日志:每条消息获得一个单调递增的序列号,并长期保存在磁盘上。Consumer 则是独立于 Stream 的读取视角,它维护自己的游标(已确认的交付序号),决定下一次从哪个位置开始读。

Stream 用 subject 过滤器决定什么消息入流,比如只收 orders.* 主题下的订单事件;Consumer 则通过交付策略(deliver policy)选择起点:取最新、取最旧、按序列号回溯或按时间回溯。多个 Consumer 可以挂载在同一个 Stream 上,彼此互不干扰——就像多位分拣员各拿一张进度卡,共享同一排货架,但各自记录各自的进度。

游标就是分拣员手里的卡片。只要卡片没丢,货架再怎么新增包裹,他都能接着上次的位置继续取——这正是“流式消费可回溯”的来源,也是 JetStream 能做事件重放的基础。

B=H头部−C游标\displaystyle B = H_{\text{头部}} - C_{\text{游标}}
积压量等于 Stream 最新消息序号减去消费者游标序号。这个差值就是“分拣员落后货架多少件货”。
  1. 积压量 B消费者尚未处理的消息条数,数值越大积压越严重。
    B\displaystyle B
  2. 头部序号生产者最新写入 Stream 的消息序列号,代表货架最新一格的编号。
    H头部\displaystyle H_{\text{头部}}
  3. 游标位置消费者已确认推进到的消息序号,代表进度卡上划掉的最后一格。
    C游标\displaystyle C_{\text{游标}}
  4. 比较求差头部序号减去游标序号,就是待消费的积压数量。
    H头部−C游标\displaystyle H_{\text{头部}} - C_{\text{游标}}

投递语义

暴雨天骑手送包裹,你问他送不到怎么办。第一种应答:“我再补送,送到为止,但可能多送一遍”——这是至少一次。第二种应答:“我给每件包裹贴唯一快递单号,你扫码核对,重复件直接拒收”——这是精确一次。

投递语义描述系统对“丢消息”和“重复消息”两种事故的承诺。NATS 核心是至多一次;JetStream 默认提供至少一次:消息只要被写入 Stream 就不会丢,但消费者可能重复收到同一条消息。

JetStream 的至少一次,靠 ACK 加自动重发实现:消费者处理完必须回 ACK,超时未回就重新投递。要逼近精确一次,需要三件套:生产者给消息设置唯一 Nats-Msg-Id(用于去重)、消费者用 AckExplicit 逐条确认、消费业务逻辑保持幂等。注意:跨系统的分布式事务在真实世界里并不存在,业界常说的“精确一次”本质上是“有效一次”。

所以“精确一次”不是魔法,而是“去重 + 幂等 + 确认”三件套的合体。就像驿站不保证暴雨中每件包裹都不淋湿,但保证每张单子都有唯一编号、每笔签收都留底。

P送达=1−(1−p)n+1\displaystyle P_{\text{送达}} = 1 - (1-p)^{n+1}
重试次数越多,消息最终送达的概率越接近 1。这正是“至少一次”的数学底线:只要失败后不断重试,消息就不会丢。
  1. 单次成功率 p一次投递尝试成功到达消费者的概率。
    p\displaystyle p
  2. 失败概率某一次投递失败的概率为 1 减去成功率。
    (1−p)\displaystyle (1-p)
  3. 重试轮数首次投递加 n 次重试,总共尝试 n+1 次,全部失败的概率是它。
    (1−p)n+1\displaystyle (1-p)^{n+1}
  4. 至少一次保证只要有一次成功就算送达,故用 1 减去“全部失败”。
    P送达=1−(1−p)n+1\displaystyle P_{\text{送达}} = 1 - (1-p)^{n+1}

ACK 机制

分拣员从货架取走包裹后,要在机器上扫码签收——这就是 ACK。签了,驿站认为这件货处理完毕;迟迟不签,驿站就把包裹重新放回待取区,再次投递。

ACK 是 JetStream 可靠性的核心操作。消费者拉取消息后有两种回应:ACK 表示成功处理;NAK 表示处理失败,要求服务器立即重投。如果消费者在 ack_wait 等待窗口内没有任何回应,服务器也会自动判定超时并重投。

JetStream 的 Consumer 有三种确认策略:AckNone 完全不需要确认,服务器只管推送;AckAll 一次确认到当前游标位置,效率高但粒度粗;AckExplicit 逐条确认,最精确也最稳妥。配合 max_deliver 参数可以限制最大投递次数,超过后消息进入死信队列或直接丢弃,避免一条坏消息无限重试拖垮整条流水线。

把 ack_wait 想象成“骑手等签收的最长耐心”。耐心太短,包裹反复重投浪费运力;耐心太长,一件坏消息就能堵住整条货架。优秀的运维者会按业务处理耗时来调这个参数。

T判定=2×TRTT+T处理\displaystyle T_{\text{判定}} = 2 \times T_{\text{RTT}} + T_{\text{处理}}
ack_wait 的取值就是在“耐心”和“效率”之间找平衡:太短则反复重投,太长则坏消息长期卡住队列。
  1. 往返时间客户端与服务器之间一来一回的网络耗时。
    TRTT\displaystyle T_{\text{RTT}}
  2. 往返双倍考虑 ACK 在途丢失、需要保守估计的情况,取两倍往返。
    2×TRTT\displaystyle 2 \times T_{\text{RTT}}
  3. 处理耗时消费者处理单条消息所需的最长耗时预算。
    T处理\displaystyle T_{\text{处理}}
  4. 判定超时超过该时间仍未收到 ACK,JetStream 就判定失败并触发重发。
    T判定\displaystyle T_{\text{判定}}

消息去重

驿站每天收到大量长得一模一样的包裹。怎么区分这是两单,还是同一单被重复录入?办法是看唯一快递单号——相同的单号,第二次直接拒收。

JetStream 的去重同样靠“单号”:发布者在消息头里带上 Nats-Msg-Id。Stream 会记住一段时间内见过的所有 ID,新消息的 ID 若已存在,直接丢弃并返回“重复”。

这个“一段时间”叫去重窗口,默认 2 分钟,可通过 Stream 配置调整。窗口期内,同一 ID 的重复消息会被拦截;窗口过后,同样的 ID 会被当作新消息放行。去重窗口不是免费的午餐:窗口越大,服务器内存里需要保留的 ID 集合就越大,内存占用随之上升。

给每条消息贴单号,代价是一点内存;收益是网络重试、客户端重发这些“人工补送”不再造成重复。单据查重,是这座驿站里最划算的投资。

N窗口=R速率×T窗口\displaystyle N_{\text{窗口}} = R_{\text{速率}} \times T_{\text{窗口}}
去重窗口内要记住的 ID 数量,等于消息速率乘以窗口时长。这是估算内存占用的小公式。
  1. 窗口容量去重窗口内需要保留的消息 ID 总数,直接决定内存占用。
    N窗口\displaystyle N_{\text{窗口}}
  2. 消息速率生产者在高峰期每秒发布的消息条数。
    R速率\displaystyle R_{\text{速率}}
  3. 窗口时长JetStream 保留消息 ID 的有效时间,默认 2 分钟。
    T窗口\displaystyle T_{\text{窗口}}
  4. 估算乘积速率乘时长,得到窗口内同时存活的最大 ID 数量。
    R速率×T窗口\displaystyle R_{\text{速率}} \times T_{\text{窗口}}

Kafka 对比

Kafka 是城市边缘的巨型物流中心:分拣线(分区)成千上万,承载着全网最重的吞吐,但也需要专门的电力系统(ZooKeeper/KRaft)和一支庞大的运维队伍。JetStream 则是街角的智能驿站:一个二进制文件就能起服务,分拣员按需取件,日常几乎免维护。

选型不是比谁更“高级”,而是比谁更匹配。功能上,Kafka 用分区加消费组实现并行扩展,JetStream 用 Stream 加 Consumer 实现顺序与回溯;Kafka 的生态(连接器、流处理框架)更成熟,JetStream 胜在简单直接。

性能与运维上,JetStream 在每秒百万条消息以下的场景完全够用,端到端延迟更低、部署成本更小;Kafka 在超大规模、跨团队数据平台场景下优势明显。运维上,JetStream 的单二进制开箱即用,Kafka 则需要管理 broker、分区副本和元数据集群,复杂度不在一个量级。

一句话:你的业务需要“全球分仓”就选 Kafka,需要“街角驿站”就选 JetStream。绝大多数微服务团队,其实只需要一座驿站。

N分区=⌈Q目标q单分区⌉\displaystyle N_{\text{分区}} = \left\lceil \frac{Q_{\text{目标}}}{q_{\text{单分区}}} \right\rceil
想要多大的吞吐,就要规划多少分区。这条式子既适用于 Kafka 的容量规划,也提醒你:JetStream 不需要这种精细的分区管理。
  1. 目标吞吐业务高峰需要的总写入吞吐量,单位通常是条/秒。
    Q目标\displaystyle Q_{\text{目标}}
  2. 单分区能力单个分区在所在节点上能达到的写入峰值。
    q单分区\displaystyle q_{\text{单分区}}
  3. 除法估算目标吞吐除以单分区能力,得到需要的最少分区数。
    Q目标q单分区\displaystyle \frac{Q_{\text{目标}}}{q_{\text{单分区}}}
  4. 向上取整分区数必须是整数,且要留出余量应对峰值抖动。
    ⌈⋅⌉\displaystyle \left\lceil \cdot \right\rceil

集群与容量

一座驿站可能会失火,所以聪明的做法是开三家分店,同一批货在每家库房各存一份——这就是 JetStream 集群的 Raft 复制:三个节点共同保管同一份流数据,任一节点宕机,其余节点照常营业。

JetStream 集群建议至少 3 个节点,Stream 的副本数(replicas)决定冗余度。写入时采用 Raft 多数派确认,过半节点落盘才算成功,因此数据不会因单点故障丢失。容量规划要同时算清存储、内存和网络三本账,避免“货架塞满才发现仓库不够大”。

存储量由消息速率、保留时长和副本数共同决定。主题设计上,推荐用 subject 层级(如 orders.created、orders.paid)代替大量独立 Stream;每条 Stream 可以用最大消息数(MaxMsgs)、最大字节(MaxBytes)或最长保留时间(MaxAge)组合控制生存期限。

容量规划的公式,就是驿站进货的账本:每天进多少货(速率)、留多久(保留)、存几份(副本)。算不清这三项的物流公司,仓库迟早爆仓。

S存储=R速率×T保留×N副本\displaystyle S_{\text{存储}} = R_{\text{速率}} \times T_{\text{保留}} \times N_{\text{副本}}
存储空间的下限 = 写入速率 × 保留时长 × 副本数。这是容量规划最核心的估算公式。
  1. 写入速率生产者的平均消息写入速率,需要换算成字节每秒。
    R速率\displaystyle R_{\text{速率}}
  2. 保留时长Stream 中消息需要保留的天数或小时数。
    T保留\displaystyle T_{\text{保留}}
  3. 副本因子每个 Stream 在集群中保存的副本数量,通常为 3。
    N副本\displaystyle N_{\text{副本}}
  4. 容量下限三者相乘得到所需存储空间的最小估算值。
    S存储\displaystyle S_{\text{存储}}

AI 应用场景

场景 1:AI 推理任务的可靠分发队列

阶段:推理服务接入与任务调度

解决的问题:在线推理服务在高峰期有大量请求涌入,直接同步调用 GPU 服务容易排队超时;用普通内存队列又会因节点重启丢失任务。JetStream 把推理请求先持久化到 Stream,推理 worker 以 Consumer 拉取,处理成功回 ACK,失败回 NAK 触发重投;请求带 Nats-Msg-Id 去重,防止网络重试造成同一张图片被计费两次。

具体案例:一个 AI 图像审核服务,前端把图片 URL 发布到 infer.review 主题,三个推理 worker 各自创建 Consumer。某个 worker 处理第 7 张图片时进程崩溃,JetStream 等待 ack_wait 超时后把该任务重投给其他 worker,图片最终被审核且只扣费一次。

场景 2:AI 训练日志与特征事件流

阶段:数据采集与特征工程

解决的问题:训练样本采集脚本分布在多台机器上,直接写数据库压力大且难以回溯。利用 JetStream 的 subject 层级(如 feature.user.*)统一收集事件,分析作业按需从 Stream 回溯消费,按游标断点续传。

具体案例:推荐系统团队把用户点击流发布到 JetStream,离线特征作业每天按时间回溯重算特征,在线特征服务则用 AckNone 策略低延迟订阅最新事件——一条 Stream 同时服务离线和在线两个场景。

Python 演示

python68 行
import time  class SimStream:    """模拟 JetStream Stream:有序消息日志 + 去重窗口"""     def __init__(self, dedup_window=5):        self.log = []        self.dedup_window = dedup_window        self.seen_ids = {}     def publish(self, msg_id, payload, now):        if msg_id in self.seen_ids:            return "duplicate"        self.seen_ids[msg_id] = now        seq = len(self.log)        self.log.append({"seq": seq, "msg_id": msg_id, "payload": payload})        self._purge(now)        return seq     def _purge(self, now):        for msg_id in list(self.seen_ids):            if now - self.seen_ids[msg_id] > self.dedup_window:                del self.seen_ids[msg_id]  class SimConsumer:    """模拟 JetStream Consumer:游标推进 + ACK + 至少一次重投"""     def __init__(self, stream):        self.stream = stream        self.cursor = 0        self.unacked = {}     def pull(self):        for msg in self.stream.log:            if msg["seq"] >= self.cursor:                if msg["seq"] not in self.unacked:                    self.unacked[msg["seq"]] = 1                    return msg                self.unacked[msg["seq"]] += 1                return msg        return None     def ack(self, seq):        if seq in self.unacked:            del self.unacked[seq]        if seq >= self.cursor:            self.cursor = seq + 1  def run():    stream = SimStream(dedup_window=5)    consumer = SimConsumer(stream)    stream.publish("m1", "hello", 0)    print(stream.publish("m1", "hello-again", 0))    stream.publish("m2", "jetstream", 1)    msg = consumer.pull()    print("投递:", msg["seq"], msg["payload"])    msg = consumer.pull()    print("未ACK重投:", msg["seq"], msg["payload"])    consumer.ack(0)    msg = consumer.pull()    print("ACK后推进:", msg["seq"], msg["payload"])  if __name__ == "__main__":    run()
目的:用一百多行代码模拟 JetStream 最核心的三个行为——Stream 有序存储、Nats-Msg-Id 去重、Consumer 游标推进与至少一次重投。
  1. if msg_id in self.seen_ids: return "duplicate"去重窗口内发现相同 ID,直接拒绝。
  2. seq = len(self.log)序列号取自当前日志长度,保证单调递增。
  3. self._purge(now)每次发布后清理过期 ID,模拟去重窗口滑动。
  4. if msg["seq"] >= self.cursor:只读取游标之后的消息。
  5. self.unacked[msg["seq"]] += 1未确认消息再次被拉取,重投次数加一。
  6. if seq >= self.cursor: self.cursor = seq + 1ACK 后推进游标,对应消费进度卡划格。

常见误区

误区“我只要在 nats-server 里开启 JetStream,发布的所有消息就都自动落盘了,不用做任何额外配置。”

正解JetStream 只是提供了仓库,不等于你用了仓库。消息要真正持久化,必须先把消息发布到配置了 Stream 的主题上;没有 Stream 接收的主题消息依旧随发随弃,就像把包裹丢在驿站门口而不是放进货架。

为何常见:“开启 JetStream”和“创建 Stream”看起来是同一个动作,官方配置又都藏在 jetstream 命名空间下,初学者容易混为一谈。

误区“既然文档说有 exactly once,那我消费逻辑就不用管重复,反正系统保证不会重复。”

正解JetStream 的“精确一次”是“有效精确一次”:靠消息 ID 去重、ACK 确认和幂等消费三者叠加而成。跨网络的分布式事务并不存在,消费者业务逻辑依然要幂等,否则去重窗口外的重复消息照样会造成重复扣款。

为何常见:文档里"Exactly Once"字样太醒目,读者容易忽略它的前置条件——生产端去重 ID 加消费端幂等。

误区“我 ACK 了一条消息,它应该马上从磁盘上消失,释放空间。”

正解ACK 只是告诉服务器“这个消费者处理完了”,相当于在消费进度卡上划掉一格。消息本身仍留在 Stream 里,直到保留策略(消息数、字节数、时间)把它淘汰。其他消费者还能从 Stream 重新消费它,这正是事件回溯能力的基础。

为何常见:把“消息队列”类比成“取走即删的收件箱”,忽略了流存储的本质是日志追加文件。

误区“为了防止重复,把去重窗口从 2 分钟调到 2 小时,不是更保险吗?”

正解去重窗口越大,服务器内存中保留的消息 ID 集合越大,内存占用随速率乘窗口线性增长。窗口应覆盖“生产者重试的最坏间隔”即可,默认 2 分钟对绝大多数场景够用;更稳的保障可以靠消费端幂等兜底。

为何常见:直觉上“范围越大越安全”,但忽略了去重是有内存成本的,典型的只见收益不见成本。

误区“Kafka 是百万级吞吐的大厂标配,JetStream 这种轻量级选手在大流量下肯定撑不住。”

正解性能取决于场景:JetStream 在中低吞吐、低延迟、小集群场景下往往更快且更省资源;Kafka 的优势在超大规模分区和成熟生态。选型应先量化自身速率,而不是假设“大就是好”。

为何常见:拿 Kafka 的峰值宣传与 JetStream 的默认配置比较,忽略了部署形态与硬件环境的差异。

面试题

Q1. NATS 核心(不含 JetStream)默认提供哪种投递语义?

单选题 · easy

  • A.至少一次
  • B.至多一次← 正确答案
  • C.精确一次
  • D.幂等一次

答案:B。NATS 核心是内存转发、送完即忘的模型,消费者离线或连接中断时消息直接丢弃,因此最多只会投递一次,即 at-most-once。JetStream 才引入持久化和至少一次语义。类比:核心 NATS 是“喊一嗓子”的骑手,没听见就算了。

Q2. JetStream 中负责保存有序消息的存储结构是什么?

单选题 · easy

  • A.主题存储
  • B.消费者对象
  • C.流 Stream← 正确答案
  • D.桶 Bucket

答案:C。JetStream 中 Stream 是有序消息日志,负责落盘与保留;Consumer 只是读取视角,维护游标。类比驿站里 Stream 是货架,Consumer 是分拣员的进度卡,两者不能混为一谈。

Q3. 消费者处理完消息后,向 JetStream 发送什么表示成功?

单选题 · easy

  • A.NAK 否定确认
  • B.ACK 成功确认← 正确答案
  • C.SYN 同步握手
  • D.FIN 结束连接

答案:B。消费者处理完消息后回复 ACK 表示成功;NAK 表示失败并要求立即重投。SYN 和 FIN 是 TCP 传输层的握手概念,与消息确认无关。类比:ACK 就是分拣员的扫码签收动作。

Q4. JetStream 消息去重依赖消息头中的哪个属性?

单选题 · mid

  • A.目标主题名
  • B.客户端回信
  • C.消息唯一ID← 正确答案
  • D.接收时间点

答案:C。JetStream 用消息头 Nats-Msg-Id 作为唯一 ID,在去重窗口内拦截重复发布。Subject 决定消息路由,Reply-To 用于请求应答,时间戳只标记时间,都不能做全局去重。类比:Nats-Msg-Id 就是快递单号。

Q5. 一个 Stream 上的消费者游标主要用于记录什么?

单选题 · mid

  • A.消息内容
  • B.消费位置← 正确答案
  • C.消息容量
  • D.确认次数

答案:B。消费者游标记录已确认推进到的消息序号,决定下次拉取从哪条开始;积压量等于头部序号减去游标。类比分拣员手里的取件进度卡,卡上记的是“取到几号位”,不是包裹内容。

Q6. 关于 JetStream 的 at-least-once 语义,下列说法正确的是?

单选题 · hard

  • A.消息一定不会重复投递
  • B.不丢失但可能重复投递← 正确答案
  • C.可能丢失且不重复投递
  • D.必定丢失并无限重试

答案:B。at-least-once 承诺消息不丢失,但可能重复投递;去重和幂等可以缓解重复的影响。选项 A 错在把至少一次与“不重复”划等号,C 是 at-most-once 的描述,D 与事实相反。

Q7. 与 Kafka 相比,JetStream 一个比较突出的优势是什么?

单选题 · hard

  • A.分区数量可以无限横向扩展
  • B.单二进制无外部依赖部署简单← 正确答案
  • C.内置流式 SQL 查询处理引擎
  • D.提供全链路分布式事务协议

答案:B。JetStream 是 nats-server 内置能力,单二进制即可部署;Kafka 历史上依赖 ZooKeeper,现在还要维护 KRaft,集群组件多。分区无限扩展、SQL 引擎、全链路事务都不是 JetStream 的主张。

Q8. 下列哪些属于 JetStream Consumer 的内置 ACK 策略?

多选题 · mid

  • A.AckNone 不自动确认← 正确答案
  • B.AckAll 确认到游标← 正确答案
  • C.AckExplicit 逐条确认← 正确答案
  • D.AckSleep 延时确认

答案:ABC。JetStream Consumer 的内置 ACK 策略是 AckNone、AckAll、AckExplicit,不存在 AckSleep。AckExplicit 最精确但开销最大,AckNone 开销最小但可靠性需要外部机制保证。

Q9. 要获得“有效精确一次”效果,JetStream 使用者通常需要哪些配合?

多选题 · mid

  • A.为每条消息生成唯一消息标识← 正确答案
  • B.消费者业务处理逻辑做到幂等← 正确答案
  • C.配置 AckExplicit 逐条确认← 正确答案
  • D.完全关闭所有消息重发机制

答案:ABC。生产端为每条消息生成唯一 Nats-Msg-Id 做去重,消费端逻辑幂等,配合 AckExplicit 逐条确认,三者叠加才能实现“有效精确一次”。完全关闭重发会让消息丢失,反而破坏了可靠性。

Q10. 关于容量规划与主题设计,JetStream 推荐哪些做法?

多选题 · hard

  • A.控制 Stream 数量避免过多← 正确答案
  • B.用 subject 层级做过滤← 正确答案
  • C.设置消息保留策略上限← 正确答案
  • D.每个消费者绑定独立节点

答案:ABC。控制 Stream 数量、用 subject 层级做过滤、设置 MaxMsgs/MaxBytes/MaxAge 保留策略都是官方推荐做法。“每个消费者绑定独立节点”既浪费资源也无必要——Consumer 是逻辑对象,与物理节点没有绑定关系。