某次容量评审上出现过很典型的一幕:一个团队要上新的一套设备遥测系统,评审表里照着 Kafka 的经验给消息层配了一块数 TB 的 NVMe 盘,理由是「消息系统嘛,肯定要存东西」。上线半年后回头看监控,磁盘使用率长期停在个位数的区间,倒是连接数曲线一路贴着上沿走,订阅表的条目数也在悄悄爬,某次批量发布的时候发布端到投递端的延迟从毫秒级抖到了几十毫秒。钱花在了用不上的地方,真正卡住的地方一分钱没花。
问题出在把两类系统的资源模型混为一谈。以日志/流处理为代表的那类消息系统,磁盘是天然的第一约束:消息写进来先落日志段,消费者各自维护位点,保留窗口决定了要攒多久的历史,于是「磁盘容量 × 保留天数 × 分区数」就是一条完整的资源账。分区数还会进一步决定文件句柄数、leader 选举开销和并行度,所以配盘和配额的逻辑是自洽的。
核心 NATS 走的完全是另一条路。它的服务端用 Go 编写,是云原生计算基金会(CNCF)托管的项目,设计目标是做一层极轻的、以主题(subject)为中心的消息路由。默认语义下,一条消息从发布方进来,服务端按 subject 找到此刻在线的订阅者,把它们投递出去,然后这条消息就不存在了。没有日志文件段,没有消费者位点,没有重放。既然不攒历史,磁盘就压根不在这条资源账的入口上。
这条差异会一路传导到采购清单。按 Kafka 思路你会优先问「盘多大、几块、什么介质、保留几天」;换成 NATS,正确的第一问是「峰值有多少条常驻长连接、每条连接上挂多少个订阅、subject 空间里到底有多少个不同的字符串」。前者盯着字节的存量,后者盯着连接的存量和匹配表的规模。用错模型的结果就是本文开头那一幕:盘空着,连接和订阅先撞墙。
要算对资源账,得先把语义钉死。核心 NATS 提供的是至多一次(at-most-once)投递:消息被投递给当前在线的订阅者,投完即忘(fire and forget)。这句话有三个可以直接翻译成容量结论的推论。
第一,服务端不为离线的订阅者保留任何东西。一个订阅者掉线十分钟,这十分钟里发给它的消息就永久丢了,重连上来也不会补发。所以「堆积」这个在 Kafka 里很自然的缓冲概念,在核心 NATS 里根本不存在——没有堆积,就意味着消费端的处理能力和发布端的速率之间没有任何缓冲层,背压会直接打到最前面的那一环。
第二,发布调用的返回成功,只代表服务端收下了这条消息,不代表任何订阅者收到了它。客户端 SDK 的 publish 是异步的,它没有 ack 可以等。想确认对方收到,就得走请求-响应模式自己等回复,这是下一节和后面 RPC 章节的事。
第三,没有位点,也就没有「消费者组滞后量」这种指标。你在 Kafka 里习惯看的 lag,在核心 NATS 里没有对应物。可观测体系得换一套抓手:看的不再是「落后多少条」,而是「有多少消息被丢弃了(因为没人订阅)」「有多少连接被判定为慢消费者而断开」。
把「丢弃」这个行为看清之后,容量建模的思路就变了。Kafka 的模型是「进来多少 → 存多久 → 谁慢慢消费」,瓶颈常在磁盘吞吐和顺序写;核心 NATS 的模型是「进来多少 → 瞬间匹配 → 瞬间复制投递出去」,瓶颈在 CPU 的匹配开销、内存的常驻表项、以及出站带宽的复制放大。这两句话基本决定了后面所有的估算方法。
subject 是以点号分层的字符串,比如 sensor.room1.temp。注意这里说的是「点分层级」,不是文件路径,层级数没有强约束,但每一层都会被服务端拿来当成一个 token 做比较。
订阅端支持两个通配符:* 匹配恰好一层,> 匹配剩余的全部层级。于是:
服务端内部把这些订阅组织成一棵按 token 分叉的树(订阅树)。每来一条发布消息,服务端要把 subject 拆成 token,沿着这棵树做一次遍历,找出所有命中的订阅条目,然后把消息复制投递给它们。这条路径是每条消息都要走一遍的热路径,所以它的成本会被发布速率线性放大。
成本由三个变量共同决定:订阅表里条目的总数、subject 的平均层级深度、以及通配符订阅所占的比例与所处位置。前两项好理解,第三项容易被忽略——sensor.> 这种在上层的贪婪匹配,会让遍历在很浅的位置就命中并向下扩散;而 sensor.*.temp 这种精确层数的匹配,遍历范围要可控得多。也就是说,通配符写得越靠上、越贪婪,单次发布的遍历分支就越多。
真正把匹配表撑爆的,是主题基数。所谓基数,就是 subject 字符串里那个变化最剧烈的段。把设备 ID 放进 subject(telemetry.device-8f3a2b.temp),一万个设备就是一万个不同的 subject,如果每个上面还挂着订阅,匹配表里就是一万个条目;把设备 ID 挪进消息体、subject 只保留到 telemetry.temp,匹配表可能就只剩几十条。同样的业务量,两种 subject 设计对应的服务端内存和匹配开销能差出数量级——这是 NATS 容量设计里最不该拍脑袋的一处。具体的开销系数需以实际版本文档与自有压测为准,不要在选型阶段套用任何现成的经验数值。
核心 NATS 有两种订阅形态,区别只有一句话:普通订阅是广播,队列组订阅是组内二选一。
普通订阅下,每一个匹配的订阅者都会收到一份完整的消息副本。同一份服务的三个实例用普通订阅挂在 order.created 上,一条消息会被复制三份投递三次。三个实例各自处理的都是全量,单机负载一点没降。
队列组(queue group)下,同一队列组内的多个订阅者之间,每条消息只投递给其中一个。同样三个实例,只要订阅时声明同一个队列组名,一条消息只会被服务端挑一个成员投出去。这才是负载均衡。
这个区别直接决定扩容动作:用队列组订阅的分发型链路,加实例就是加处理能力,吞吐近似线性增长;用普通订阅的广播型链路,加实例不减少任何单实例的负载,只会让出站带宽和投递复制次数线性上涨。所以 NATS 里最典型的容量事故之一,就是拿广播订阅去做任务分发,然后发现加了十台机器,每台还是一样忙,服务端出站带宽反而先打满了。
把它写成一个可计算的量:设一次发布在 subject s 上匹配到的独立订阅者数为 N(s),匹配到的队列组数为 G(s),那么这次发布产生的实际投递份数是 N(s) + G(s)——每个独立订阅者一份,每个队列组一份。这个式子后面会反复用到,它是从「发布速率」过渡到「投递速率」和「出站带宽」的唯一桥梁。
这一节是全文的核心。既然磁盘不在入口上,那入口是什么?是三个可数的量,外加一条把它们串起来的链。
要素一:连接数 C。NATS 的客户端连接是常驻长连接,不是每次发消息建一次。C 由部署拓扑决定:微服务侧 C ≈ 实例数 × 每实例连接池大小;IoT 侧 C ≈ 设备数 ÷ 每个边缘网关的汇聚比。每条连接都要占服务端内存,也要占一个文件描述符。连接数不是「发消息的时候才消耗」的瞬时量,而是 7×24 常驻的存量。所以「半夜没有业务流量」并不会让连接数掉下来,容量必须按峰值在线规模算,而不是按平均流量算。
要素二:订阅条目数 T。T ≈ C × S̄,其中 S̄ 是每连接的平均订阅条目数。这里有个特别容易漏掉的成分:临时订阅。请求-响应模式里每一个在飞的请求都会临时建一个 inbox 订阅,请求完成就销毁。所以 T 不是一个静态常数,而是静态订阅数 + 瞬时并发请求数。并发一上来,订阅表就跟着涨,而订阅表涨了,所有发布的匹配遍历都会变慢——这是一个会把局部压力扩散到全局的耦合点。
要素三:主题基数 B。即集群中出现过的、互不相同的 subject 字符串数量。B 和 T 是两回事:T 是订阅表的条目数,B 是 subject 空间的规模。高基数 subject 设计会让 B 随设备数、订单数线性膨胀,进而推高 T 和匹配表的内存占用。设计 subject 时的判断标准很朴素:需要用通配符批量订阅的字段,才可以放进 subject;只用于消费端内部处理的字段(比如设备 ID、订单号),放进消息体。
把三者串成一条链,就是本文唯一的那个核心论点:
写成估算式:D = Σ_s P(s) × (N(s) + G(s)),BW_out ≈ D × L,其中 P(s) 是 subject s 的发布速率,L 是平均消息体字节数(含 subject、header 与 payload)。内存侧则大致是常驻连接与订阅的开销 + 待发缓冲的峰值占用两块。
这套式子的价值不在于算出一个精确值,而在于逼你把每个变量摊到桌面上。选型时按这个顺序问一遍:峰值在线连接多少 → 每连接挂几个订阅、其中几个是临时的 → subject 里有没有高基数段 → 一条消息平均被复制几份 → 出站带宽是多少。五问答完,瓶颈在哪一环基本就清楚了。所有系数都需以实际版本文档与自有压测为准,不要拿别的环境的数字直接套。
请求-响应是 NATS 的内建模式,不需要额外引入 RPC 框架就能实现同步调用。机制很朴素:请求方先订阅一个临时且唯一的回复 subject(通常叫 inbox,客户端库会自动生成带随机串的名字),然后往目标 subject 发布消息,并把 inbox 写在消息的 reply-to 字段里;响应方处理完,往这个 inbox 回发结果;请求方在 inbox 上等着,收到第一个响应就返回,超时则报错。
为什么这么一个简单机制就能顶掉一整套 RPC 框架的很多职责?因为它顺手解决了三件事。服务发现:调用方只需要知道 subject,不需要知道对方有几个实例、IP 是什么,实例上下线由 NATS 的连接状态自然反映。负载均衡:响应方以队列组订阅,服务端自动在成员间分发,等价于是服务端侧的负载均衡器,而且没有额外的网络跳转。多路复用:所有请求共用一条长连接,靠 inbox 区分响应归属,不需要为每路调用建连接。
这也解释了前面那句「临时订阅是 T 的动态部分」。在飞的并发请求数 R_inflight 按 Little's Law 就是请求速率 × 平均响应时间,每一个在飞请求都对应一个 inbox 订阅条目。于是一个看似只跟「QPS」有关的指标,会通过 inbox 订阅直接变成订阅表的压力。响应时间变长一倍,在飞的 inbox 就翻一倍,订阅表跟着膨胀,然后所有发布的匹配都变慢——这就是 NATS 里典型的延迟正反馈,排查时如果只盯着 QPS 会完全看不出来。
但要清醒:内建不等于完备。超时之后,服务端的工作并不会被取消,响应方仍会继续跑完;重试策略、熔断、限流、schema 校验、链路追踪这些,协议层都不管,得自己在客户端侧补齐。所以更准确的说法是:NATS 把 RPC 的传输与寻址做进了协议,把治理留给了应用层。要不要再叠一层框架,取决于你的治理需求有多重,而不是取决于「能不能调通」。
前面所有内容都有一个前提:核心 NATS,不落盘。一旦显式启用持久化扩展(JetStream),账本就换了一页。
在持久化模式下才出现这些概念:流(stream)负责接收并保存消息,消费者(consumer)负责读取并维护自己的进度,保留策略决定消息留多久、留多少、什么条件下删,存储类型分文件与内存两种。到这一步,前面消失的那些东西全回来了:磁盘、位点、重放、ack。
磁盘成为第一约束之后,约束项至少四层。IOPS 与 fsync:持久化写不再是「投递完就扔」,每条消息都要走到存储层,同步刷盘策略下的写放大直接决定单条发布延迟的下限。保留窗口:按时间、按条数、按总字节、按订阅者确认情况(interest/limits 一类策略)保留,窗口越长,容量需求越大,这是一条简单的乘积关系。消费者确认(ack):ack 本身也要落盘,未确认的消息不能删,慢消费者会从「被断开」变成「把磁盘撑住」。位点与重放:位点要持久化,重放会瞬间产生大量顺序读,读放大和写放大同时存在。
换句话说,在持久化模式里,内存侧的账(连接、订阅、基数)并没有消失,只是不再是第一约束。它的位置被「写入速率 × 保留窗口 × 副本数」取代了。这也是为什么在选型评审里必须先问一句:这套业务到底要不要持久化?要,就按存储型系统的模型配盘、配 IOPS、算保留窗口;不要,就回到本文的主线,把钱花在连接承载能力、内存与网络出口上。两个模型混着用,是最贵的那种用法。
顺带一句:保留策略、存储类型、ack 语义的具体可选项与默认值,需以实际版本文档为准,选型时不要照抄二手资料里的参数表。
NATS 的默认最大消息体是 1 MB(服务端可配置,具体配置项名称与取值上限需以实际版本文档为准)。这个数字不是随便定的,它是一条设计边界:NATS 面向的是小而多的指令/事件,不是大块数据搬运。
把大消息塞进 NATS 会同时在三个地方付出代价。内存上,消息要在服务端的待发缓冲里停留,缓的是连接维度的队列,消息体越大,同样条数下的缓冲占用越高,慢消费者出现的概率也越高。带宽上,投递份数 N(s) + G(s) 是乘数,一条 1 MB 的消息广播给 50 个订阅者就是 50 MB 的出站,这个放大比 CPU 更容易先打满。延迟上,大消息在单条连接上串行发送,会排在小消息后面,把尾部延迟显著拉长。
真有大块数据要走,两条路。分片:发送侧切成多片,每片带序号,接收侧按序重组;代价是要自己做序号管理、超时清理与乱序处理,还要考虑一片丢失导致整条消息作废。引用传递:大块数据放对象存储,NATS 里只传一个引用标识;代价是多一次外部取数,但消息链路保持轻量,也更符合 NATS 的设计初衷。
判断标准可以这样定:消息体接近或超过默认值时,先确认它是「必须跟着事件一起走的字段」还是「可以按需再取的载荷」。大部分所谓的大消息,最后都会发现只有几十字节是路由和判定真正需要的。
以下为典型部署思路,并非特指某一真实客户。
假设一套设备遥测系统,设备规模在十万级,每台设备周期性上报若干指标,同时平台要能下发指令、掌握设备的在线状态。
连接数的两种量级。如果每台设备直连 NATS,C 就是十万级——这个量级下,常驻内存、fd 与重连风暴都是必须正面应对的问题。更常见的做法是在边缘侧放一层汇聚网关,若干个网关把一片区域的设备收敛成少量长连接,C 降到百级到千级。这一层汇聚,是 IoT 场景里对资源账影响最大的一个架构决策,它把「连接数」这个变量整体缩小了一到两个数量级。
subject 设计的两种量级。方案 A 把设备 ID 放进 subject:telemetry.<device_id>.temp,基数随设备数线性膨胀,订阅表条目数也随设备数走。方案 B 把设备 ID 挪进消息体,subject 收到 telemetry.<region>.temp,基数只随「区域 × 指标」组合增长,通常低好几个数量级。方案 B 的代价是消费端想按单设备过滤,得在 payload 里自己判断,无法用 subject 级通配符做服务端过滤。取舍点在于:你到底需不需要「订阅某一个设备」这种粒度?大部分遥测场景的消费方是按指标或按区域批量处理的,答案是不需要。
在线状态怎么算。连接本身就是状态:设备在,连接在;设备掉线,连接断。这比让设备周期性发心跳再由服务端判超时要准确得多,也省掉了一整套心跳消息的带宽。但反过来,这也意味着一次网络抖动会被直接解读成「设备离线」,如果上层对离线做了告警或自动化动作,抖动会引发误动作。
下行指令走队列组。平台下发指令的链路,消费端应当以队列组订阅,这样加实例能真正分摊处理压力;而上行遥测如果需要多个后端系统各自处理同一条数据(比如一份入实时计算、一份入告警),那才用广播订阅。这一进一出的订阅形态通常是不一样的,别默认统一。
资源画像的结论。这个场景的资源账入口依次是:汇聚后的连接数 → 每连接订阅数(主要由后端消费服务的种类决定)→ subject 基数(由是否把设备 ID 放进 subject 决定)→ 上行出站带宽(遥测是典型的「入一份、出多份」)。磁盘在这套核心 NATS 的画像里基本不出现,除非明确要保留历史遥测——那属于上一节讨论的持久化范畴。
以下为典型部署思路,并非特指某一真实客户。
另一类典型用法是把 NATS 当作微服务之间的调用通道,指令下发、状态查询、事件广播混在同一套 subject 空间里。
连接数。C ≈ 服务实例数 × 每实例连接数。这里真正的变量是每实例连接数:共用一条连接最省资源,但所有请求共享一条 TCP 的队头阻塞窗口,大消息会把小请求堵住;按读写分离或按优先级建多条连接可以缓解,代价是 C 成倍上涨。容器环境下实例数会弹性变化,所以 C 的峰值要按弹性上限算,不是按当前副本数算。
订阅数里的动态部分。静态订阅是「每个服务暴露一个 subject」的量级,通常不大;动态部分是 inbox,等于在飞请求数 请求速率 × 平均响应时间。这一项在 RPC 场景里往往比静态部分大得多,也是订阅表膨胀的主因。所以 RPC 场景的资源画像里,响应时间不是单纯的质量指标,它直接换算成订阅表的条目数——一次下游抖动导致的响应时间拉长,会立刻转成订阅表压力,再转成全局匹配变慢。
队列组承担负载均衡。服务提供方以队列组订阅自己的 subject,加副本即加分处理能力。要注意的是队列组是服务端在订阅成员间分发,它并不知道各成员的实时负载,成员之间处理快慢不一致时会出现分配偏斜。想做真正的按负载分发,得在应用层加权重或做二次调度。
超时与取消。请求方超时返回后,提供方还在跑,这会形成「幽灵负载」:调用方以为结束了,实际处理压力还在。重试会放大这个问题——一次超时重试等于把一份工作变成两份。RPC 场景的容量规划必须把重试放大系数算进去,否则压测的 QPS 和生产的实际处理量对不上。
资源画像的结论。这个场景的入口依次是:弹性峰值下的实例连接数 → 在飞 inbox 订阅数(由 QPS × 响应时间决定)→ 每连接的待发缓冲(由大消息比例决定)→ 出站带宽。磁盘同样不出现。值得注意的是,RPC 场景里最容易被低估的是第二项,因为它长得像一个延迟指标,实际是一个内存存量指标。
三种故障形态在 NATS 里的表现差别很大,排查路径也不一样。
连接抖动表现为连接数曲线呈锯齿状、认证与握手速率持续偏高。成因可能是网络质量、客户端 GC 停顿、服务端负载导致的处理超时,也可能是客户端的心跳/探活参数配得过紧。危害不只是「重连那一下」:每次重连都要重新建立订阅、重建 subject 匹配项,而重连窗口内的消息是直接丢失的。抖动频繁时,业务侧看到的是「偶发丢指令」,但服务端日志里可能只有一条普通的断开记录。判断方法:看单位时间内的新建连接速率,而不是看当前连接数——当前连接数看起来平稳,可能底下一直在疯狂重连。
重连风暴是抖动的极端形态。服务端重启、网络分区恢复、或者上游负载均衡把一批流量切过来,海量客户端在几乎同一时刻重连。瞬时压力来自三部分:认证计算、订阅表重建、以及订阅表在集群节点间的同步。这三件事都集中在很短的窗口里,于是 CPU 会打出一个尖峰,而这个尖峰又会拖慢后续的连接建立,形成正反馈。缓解手段是客户端侧的退避加随机抖动,把重连请求摊平到一段时间里;服务端侧则需要评估「冷启动后多快能把全部订阅重建完」这个指标。这个场景压测时经常被漏掉,因为它不是一个稳态指标。
慢消费者是另一种形态。服务端给每个客户端连接维护一份出站待发缓冲,客户端读取跟不上时,缓冲就会堆积,占用内存;堆积到服务端设定的上限,服务端会判定该连接为慢消费者并主动断开(具体阈值与行为需以实际版本文档为准)。它的隐蔽之处在于:这是服务端的自我保护动作,但从业务视角看就是「这个消费者又掉线了」,很容易被误判成网络问题。区分方法很直接——网络问题导致的断开通常伴随握手失败和认证重试,慢消费者导致的断开则能在断开前看到待发字节数持续上涨。广播订阅下慢消费者主要拖累自己;队列组下则更麻烦,一个慢成员会让分给它那一份的处理整体滞后,表现出来的就是队列组尾部延迟变长,而其它成员看起来闲着。
还有一类更慢性的故障:主题基数膨胀。它不会瞬间报错,而是伴着业务实体的增长,让订阅表和匹配开销持续上升,表现为发布延迟的整体抬升、内存占用的单调增长。等到明显感知时,通常已经需要动 subject 设计了,而改 subject 意味着发布方和订阅方一起改。所以基数这件事,最好在设计阶段就盯住,而不是等监控告警。
一万网络深耕 IDC 行业 19 年(成立于 2007 年),在华南、华东、华北及中国香港、海外多地整合自营节点与 BGP 多线资源,提供服务器租用、机柜托管与带宽接入的一体化交付;具体机型、带宽与报价以官网实时信息或咨询确认为准。
把全文的结论压成一份可以直接拿去评审的动作清单。
选型阶段,按顺序回答五个问题。第一,峰值常驻连接数是多少,按什么拓扑算出来的(微服务侧按弹性副本上限 × 每实例连接数,IoT 侧按设备数 ÷ 汇聚比)。第二,每条连接上挂几个订阅,其中几个是 inbox 这类临时订阅,临时订阅的量按 QPS × 响应时间估。第三,subject 里有没有高基数段,基数是多少,能不能挪进消息体。第四,一条消息平均被复制成几份投递,即 N(s) + G(s)。第五,出站带宽 D × L 是多少,出口有没有余量。这五个问题全部答完,才轮到讨论 CPU 核数和内存容量。
先确认一个前置判断:要不要持久化。要,就按存储型系统建模,核心变量变成写入速率、保留窗口、副本数与磁盘 IOPS;不要,磁盘在资源账里基本可以忽略,别为它付钱。这个判断必须在选型之前做,它决定的是两套完全不同的模型。
扩容路径按瓶颈类型分叉。连接数是瓶颈时,先考虑能不能在客户端侧做汇聚(边缘网关、连接池收敛),把 C 整体降下来,这比加机器便宜得多;汇聚不了再考虑按 subject 空间横向拆分集群,把不同业务域的 subject 分到不同集群上。订阅表是瓶颈时,优先治理基数与通配符,这是唯一能数量级下降的手段,加机器只能线性缓解。出站带宽是瓶颈时,检查有没有把广播订阅误用在了分发型链路上,把能改成队列组的改成队列组,投递份数立刻下降。
监控必须覆盖的四类指标。连接维度:当前连接数、新建/断开速率、认证耗时。订阅维度:订阅表条目总数、单 subject 订阅数分布、临时订阅的在飞数量。基数维度:不同 subject 的数量、按前缀聚合的基数 TOP 榜。投递维度:发布速率与投递速率的比值、待发字节数、被判定为慢消费者而断开的连接数、入站与出站字节比。第三类和第四类最容易漏,但它们恰恰是 NATS 特有的抓手。
压测要覆盖的两个非稳态场景。一是冷启动重连:全部客户端同时重连,看多久能恢复、CPU 峰值到多少。二是慢消费者注入:人为让一部分消费端变慢,观察内存上涨速度和服务端的断开行为。这两个场景都不会出现在稳态指标里,但它们才是生产事故的主要来源。
| 资源项 | 由什么决定 | 增长的信号 | 何时成为瓶颈 | 可观测的做法 |
|---|---|---|---|---|
| 客户端连接数 C | 实例数 × 每实例连接池大小;或设备数 ÷ 边缘网关汇聚比 | 连接数曲线长期贴着上沿,新建/断开速率持续偏高 | 常驻内存与 fd 接近单进程可承载上限,握手延迟开始抖动 | 按实例统计当前连接数、新建与断开速率、认证耗时分布 |
| 订阅条目数 T | 静态订阅 + 在飞的 inbox 临时订阅(QPS × 响应时间) | 订阅表条目单调上涨,发布到投递的延迟分位整体抬升 | 单次发布需遍历的条目过多,匹配成为热路径主要开销 | 统计总订阅数、单 subject 订阅数分布、临时订阅在飞量 |
| 主题基数 B | subject 中是否包含设备 ID、订单号之类的高基数字段 | 不同 subject 的数量随业务实体线性膨胀,内存占用单调增长 | 基数远超订阅种类数,匹配表内存与遍历分支同步膨胀 | 统计不同 subject 数量,按前缀聚合基数并输出 TOP 榜 |
| 投递放大倍数 | 广播订阅的独立订阅者数 N(s) 与队列组数 G(s) 之和 | 入站带宽没变,出站带宽持续上涨 | 出站带宽先于 CPU 打满,加广播订阅实例反而更糟 | 统计每 subject 的订阅者与队列组成员数、入站出站字节比 |
| 每连接待发缓冲 | 消息体大小 × 投递速率 ÷ 客户端消费速率 | 待发字节数持续上涨,慢消费者计数增加 | 个别慢客户端的缓冲堆积拖高整体内存占用 | 监控待发字节数、慢消费者计数、因缓冲超限断开的连接数 |
| 消息体大小 L | 业务 payload 设计、是否分片、是否改为引用传递 | 消息体大小分位整体上移,接近服务端 max_payload | 大消息同时抬高带宽、缓冲与尾部延迟三项 | 统计消息体大小分位、输出超过阈值的 subject 列表 |
| 持久化后的磁盘与 ack | 写入速率 × 保留窗口 × 副本数,以及 ack 落盘开销 | 仅在启用持久化流后出现:磁盘占用与写延迟同步上涨 | 磁盘 IOPS 与保留窗口取代连接与订阅,成为第一约束 | 监控写入 IOPS、保留窗口命中率、未确认消息堆积量 |
第一项,也是最容易算错的一项:峰值常驻连接数。它按在线规模算,不按流量算,半夜没有业务也不会掉下来。把它从部署拓扑里推出来——微服务侧按弹性副本上限乘每实例连接数,IoT 侧按设备数除以边缘网关的汇聚比。如果这一项推不出来,后面所有估算都是空中楼阁。
第二项:订阅表里到底有多少条目,以及其中多少是临时的。静态订阅数通常由服务种类决定,比较好数;真正容易失控的是 inbox,它等于在飞请求数,按 QPS 乘响应时间估。这一项把「延迟」和「内存」连在了一起,是 NATS 容量模型里最有特色的一个耦合点。
第三项:subject 的基数,以及通配符写在哪一层。在设计阶段就把它定死,因为这是唯一一个后续改动成本极高的变量——改 subject 意味着发布方和订阅方同时改。判断标准只有一条:不需要用通配符批量过滤的字段,一律不要放进 subject。
第四项:一条消息平均被复制成几份。用 N(s) + G(s) 算:每个独立订阅者一份,每个队列组一份。这个数字决定了出站带宽,也决定了「加实例到底有没有用」——用广播订阅的分发型链路,加实例只会让这个数字变大。
第五项:有没有启用持久化。这个判断必须在选型之前完成,它决定的是两套完全不同的资源模型。没启用,磁盘基本不进账本;启用了,磁盘 IOPS、保留窗口和 ack 立刻取代连接与订阅成为第一约束。
把这五项按顺序过一遍,一台机器够不够、瓶颈会在哪一环、扩容该往哪个方向走,答案基本就出来了。所有中间系数都需以实际版本文档与自有压测为准,别拿别处的经验值直接套。
本文的技术口径来自 NATS 的公开设计原则与协议语义:subject 以点分层、* 匹配单层、> 匹配剩余全部层级;核心 NATS 提供至多一次、即发即忘的投递,无持久化、无重放、无消费者位点;请求-响应通过临时 inbox 实现;队列组内每条消息只投递给一个成员;持久化能力由 JetStream 提供,并引入流、消费者、保留策略与存储类型;默认最大消息体为 1 MB 且服务端可配置;服务端以 Go 编写,是 CNCF 托管项目。这些属于原理性事实,可以直接作为推理基础。
本文没有给出任何具体的吞吐基准、单连接内存占用、版本号与发布时间、市场份额、客户案例或实测数据。文中出现的公式(D = Σ P(s) × (N(s) + G(s))、BW_out ≈ D × L)是变量关系与估算方法,用来指导你把每一项摊开测量,其中的系数必须靠你自己的压测填充。用别人的数字代入,得到的结论大概率是错的。
需要你自行验证的,至少这几类:一是各项阈值与默认值,包括最大消息体、慢消费者的判定阈值与断开行为、以及连接与订阅相关的限制项,需以实际版本文档为准;二是匹配开销随订阅条目数、层级深度、通配符比例变化的具体曲线,只能在自有 subject 设计和订阅形态下压测得到;三是持久化启用后的写入放大、保留策略命中与 ack 落盘开销,与存储介质、刷盘策略、副本数强相关,必须实测;四是重连风暴的恢复时间与 CPU 峰值,属于非稳态指标,稳态压测覆盖不到。
另外提醒两处容易被二手资料带偏的地方。其一,不同部署形态下的服务端行为会有差异,集群模式与单机模式的订阅表同步开销不是一回事,选型时不要混用参数。其二,客户端库对 inbox、重连退避、缓冲的实现各有差别,同一套服务端指标在不同语言的客户端下可能表现不同,压测时应使用生产实际使用的那一套客户端。
如果你的业务正处在「要不要上 NATS」或者「现有 NATS 集群快到瓶颈」的阶段,建议先用本文的五个问题把现状量一遍,再对照上表的可观测项补齐监控,之后再谈扩容,比直接加机器稳妥得多。
Copyright © 2013-2020 idc10000.net. All Rights Reserved. 一万网络 科技有限公司 版权所有 深圳市科技有限公司 粤ICP备07026347号
本网站的域名注册业务代理北京新网数码信息技术有限公司的产品