更轻 更现代 更灵活,聊聊我为什么推荐 NATS——NATS 系列(1)

Kaku Lv4

提到 pub/sub,你多半先想到 Redis

在 Redis 里做消息广播,一行命令就上手:

1
redis-cli subscribe news

另一个终端 PUBLISH news "hello",这边立刻收到。单纯不需要保证可靠的广播很爽,但只要你打算拿它当真消息系统用,Redis 官方文档就会来给你泼冷水——Pub/Sub 一节白纸黑字写着 at-most-once:消息发出去,服务器就不再管了。订阅者断线?丢了,永远不会重发。

Redis 自己也承认这一点,给出的答案是 Redis Streams:消息落进键空间,有消费者组,有 ack,支持 at-least-once。但代价也跟着来了——命令体系完全是另一套(XADD / XREADGROUP / XACK),心智上和 Pub/Sub 是两个系统。而且 Streams 说到底是一类普通的内存数据,和缓存、session 共用同一份 maxmemory 预算,消息存多了,腾出来的可能就是别的 key。

我平时折腾微服务,消息这块的真实需求其实很朴素,就三条:

  1. 一个事件广播给多个服务(谁关心谁订);
  2. 一批任务摊给几份工作进程(每人干一份,别重复干);
  3. 服务重启的时候,消息还在。

三条都不过分。拿 Redis 凑合,前两条能玩,第三条要么丢要么自己造轮子。

那上 Kafka?很早之前我写过 Kafka 4.0 的部署:一个 broker 也要 JVM、一串 KRaft 环境变量、topic 分区规划、消费组管理。幸好现在砍掉了 ZooKeeper,不然的话…… 它当然强大,无人能敌的吞吐,可我只是想给几个服务之间拉一条事件总线,这个开局实在太重。

至于其他 MQ,RabbitMQ、ActiveMQ、RocketMQ、Pulsar……它们都有各自的生态和定位,但都不是”轻量级”。RabbitMQ 是 Erlang 写的,集群是 broker 之间互相认领队列;ActiveMQ 是 Java,集群是 broker 之间互相认领 topic;Pulsar 是 Java + BookKeeper,集群是 broker + bookie 的双层架构。它们都能做消息系统,但都不是”随手起一个就能用”。

这个空档——三件套齐全,但轻得能随手起一个——就是 NATS 的地方。而且熟悉之后你会发现,它给的不只是”轻量版全家桶”,是一套用完全不同的方式组装出来的消息系统。

必备三件套都有,但拼装方式完全不同

NATS并没有把自己称之为消息队列,它的定位是消息系统:应用连上服务器,按 subject(主题)互发消息,发消息的人不需要知道接收者在哪个地址、甚至不需要知道有没有接收者。

我们先把这三个的行为说清楚:

发布订阅。 这算是所有需要传递消息的系统的核心功能了。Core NATS 的行为和 Redis Pub/Sub 一致:fire-and-forget,at-most-once,没有在线订阅者的消息直接丢弃。这个先把话说在前面,后面所有”它很轻”的讨论都建立在这条性质上。默认消息大小 1MB,可配置。

消费者组。 需求其实有两种,NATS 把它们拆给了两层:

  • 实时分摊——在线的几份进程瓜分消息,不需要回看:用 queue group,订阅时多给一个组名,同组内一条消息只投递给一个成员,不需要 offset,不需要 ack;
  • 带进度的分摊——离线了还要,处理完成才确认:用 JetStream consumer,消费进度记在服务端,ack 之前消息算未处理,超时自动重投。这个其实更类似我们熟悉的 Kafka 消费组概念,但 NATS 的实现方式和 Kafka 完全不同:Kafka 把消费者组和分区再均衡绑在一起,是一套独立的概念;NATS 这两档是同一套订阅模型上的两个开关。

Redis 需要你在 Pub/Sub 和 Streams 之间二选一,Kafka 把消费者组和分区再均衡绑在一起,是一套独立的重概念。NATS 则依靠它的模型把两档功能做到同一套订阅模型上,切换个开关的事儿。

持久化。 Core NATS 只是总线,它决定消息怎么传输,但是正如路由器并不管你消息是否落盘一样。单纯 Core-NATS 并没有任何持久化功能;此时我们就可以引入NATS的一个DLC——JetStream。JetStream 是叠在总线上的存储层。发布路径不变,还是往 subject 发;服务端建一个 stream,绑定 orders.> 这样的主题模式,匹配的事件就被抄一份进流,每条消息带序号。consumer 记录自己读到哪了——用官方文档的话说,JetStream 把发布者和订阅者从”时间”维度解耦了:两边不再需要同时在线。

真正值得注意的区别是这三件套长在一起:同一个 server 进程,同一个 subject 命名空间,同一套协议。JetStream 的开启方式是一个启动参数的事(nats-server -js,存储目录 -sd),不需要另起一套集群、一套 API。Kafka 是一体式大礼包,Redis 是两个割裂的系统,NATS 则是靠同一个模型的不同特性组合来搭建你的消息系统。

NATS 的世界观:把消息当”网络流量”来路由

我比较喜欢那网络来类比 NATS:把消息看作是网络中的数据包,通过主题(subject)进行寻址和路由。大多数 MQ 的心智模型是以”队列”为核心:你声明队列/主题,消息进队列,消费者从队列取。这比较符合直觉,但计算机向来不是按人类直觉设计的,我们的科技树里已经有了一个极为高效的信息传递方式,计算机网络。NATS 的核心抽象不是队列,是 subject 寻址 + 路由,这套东西长得非常像计算机网络。这也是我感觉最爽的地方,整个传输链路如同网络一样,对业务完全透明。

subject 是点分层级,通配符两种:

1
2
3
orders.us.created      具体主题
orders.*.created 匹配任意一段(相当于主机位通配)
orders.> 匹配后面所有层级(相当于前缀路由)

集群模式下订阅信息在节点间传播,路由表里没有匹配的订阅,消息就不往里转发。拿我们熟悉的投递方式来对照:

网络里的概念NATS 里的对应说明
组播(订了才收)普通订阅,每条消息发给所有匹配订阅者类似 IGMP 加入组
任播(多个候选只到一台)queue group,同组内分摊一条只发一次我写过单播组播任播,那篇里的”任播味”这里就有
一问一答(DNS 查询)request/reply,发布者等一个定向回信不需要服务发现、不需要注册中心
地址 + 路由表subject + 服务端订阅路由表发布者眼里没有”队列”这个实体
VPC 隔离account,一个 server 内划分互不可见的域多租户的基础
自治系统互联leaf node / supercluster把多个集群拼成广域总线

类比到这儿为止,边界也说清楚:NATS 不实现网络协议,连接就是 TCP,”路由”是进程内存里的订阅表匹配。但这套类比决定了写代码的手感——你在”往一个地址空间发事件”,而不是”往一个存储队列塞数据”。

request/reply (这也是 NATS 的重磅特色,我们后面展开讲)尤其能体现这种手感。一个服务发 nats request hello "有人在吗",另一个服务用 nats reply 挂着应答,双方互相不知道地址,中间没有任何注册中心,request 消息通过一个隐藏的回复 subject 定向送回。把它当成”免服务发现的 RPC 底座”也不过分——官方文档管这个叫 location transparency,我认为这是它”云原生”这个词最实在的落点:服务实例随起随停,通信层不 care。

动手:一台机器,四种玩法全过一遍

以下全部是我本机实测,不是从文档抄的理想输出。环境:nats-server v2.15.0(当前最新正式版本,2026-09-17 发布,核对于 2026-09-23),nats CLI v0.5.0。实验机 4222 端口已有别的实例在跑,所以下面命令连的是 -s nats://localhost:14222,你照抄时改成自己的地址或省略 -s 用默认值即可。

起一个带持久化的 server:

1
nats-server -js -sd ./jsstore -p 14222 -m 18222

一个进程,总线加存储全有了。另外说一句:4222 是消息端口,8222 风格的 HTTP 监控端点(上面开的 18222)自带 varz/connz 等页面,不用装 agent。

玩法一:广播。 开两个订阅者,同一条消息两边都拿到(示例输出,省略了时间戳里的 -s 参数信息):

1
2
3
4
5
# 订阅者 1 / 订阅者 2,各自都收到:
[#1] Received on "news.today"
headline-1
[#2] Received on "news.today"
headline-2

玩法二:queue group。 两个成员挂同一个组,发四条任务,实测它们一条不重不漏地一人一半:

1
2
3
4
5
6
# 成员 1:
task-1
task-3
# 成员 2:
task-2
task-4

对比上一条输出:广播是”人人有份”,queue group 是”组内分赃”。一个 --queue 参数的区别,没有 rebalance,没有 offset。

玩法三:request/reply。

1
nats request hello "Anyone there?" --timeout=2s -s nats://localhost:14222
1
2
Received with rtt 277.748µs
Hi there!

277µs——本机 localhost,不代表你的网络,但能说明这条路有多短:一次 TCP 长连接,一次往返,没有中间的队列存储。

玩法四:JetStream。 服务重启不丢消息的部分,从创建 stream 开始。--defaults 表示其余选项全部取默认值:

1
2
3
4
5
6
nats stream add ORDERS --subjects "orders.>" --storage file --retention limits --defaults
nats pub orders.new "Order #1001"
nats pub orders.new "Order #1002"
nats pub orders.shipped "Order #1001 shipped"
nats consumer add ORDERS order-processor --pull --deliver all --ack explicit --defaults
nats consumer next ORDERS order-processor --count 3 --ack

consumer 创建时的状态就有意思了——我还没去取消息,服务端的进度视图已经记下了存货:

1
Unprocessed Messages: 3

取出来,三条各自确认:

1
2
3
4
subj: orders.new / tries: 1 / cons seq: 1 / str seq: 1 / pending: 2
Order #1001
Acknowledged message
...(另外两条同构,略)

cons seq 是消费者进度,str seq 是流内序号,--ack 之前这三条会一直等着重投。流本身的状态:

1
2
3
4
Messages: 3
Bytes: 165 B
First Sequence: 1 @ 2026-09-23 17:13:54
Number of Subjects: 2

顺带两个提醒:nats stream rm 会把流连同里面存的消息一起删掉,实验环境之外先三思;从 2.15 起新建 stream 默认最多允许 1000 个 consumer,大规模 consumer 场景记得显式配置 max_consumers

代码侧。 官方 Go 客户端约二十来行就能跑通订阅加通配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"fmt"
"log"
"time"

"github.com/nats-io/nats.go"
)

func main() {
nc, err := nats.Connect("nats://localhost:4222") // 实测时连的是 14222 实验实例
if err != nil {
log.Fatal(err)
}
defer nc.Close()

nc.Subscribe("demo.>", func(m *nats.Msg) {
fmt.Printf("[%s] %s\n", m.Subject, m.Data)
})

nc.Publish("demo.go.sub1", []byte("hello from go"))
nc.Publish("demo.go.sub2.deep", []byte("wildcard works"))
nc.Flush()
time.Sleep(2 * time.Second)
}

实测输出:

1
2
[demo.go.sub1] hello from go
[demo.go.sub2.deep] wildcard works

一个订着 demo.> 的回调,收到两条不同深度、都匹配通配的消息。API 的表面积小到可以当场背下来。

和 Redis、Kafka、RabbitMQ 摆在一起看

先一张表,维度都取自各家官方文档能核对到的行为描述,不放跑分——工具选型不是跑分大赛,跑分永远是别人的环境好看。

Redis Pub/SubRedis StreamsKafkaRabbitMQNATS
官方定位缓存系统带的频道广播键空间里的流式数据结构事件流平台AMQP 消息代理云原生消息系统
投递语义at-most-once,官方明说at-most-once 至 at-least-once取决于消费位点管理默认至少一次,有 ackcore:at-most-once;JetStream:at-least-once
消息存储不存内存键空间磁盘日志,按保留策略删除队列(可持久化)core 不存;JetStream 文件/内存流
消费进度消费组记 PEL消费组 offset 落内部 topicbroker 记未 ack 消息JetStream consumer 记在服务端
回放历史不能能,但吃内存、会驱逐强项,重放是核心用法不能,ack 完就删能,流内按序号/时间回放
部署形态已有 Redis 即送同左JVM/原生镜像,集群 + 控制器Erlang broker,队列镜像单二进制,集群内嵌 Raft
寻址模型channel + glob 匹配keytopic + partition(key 哈希)exchange + bindingsubject + * > 通配

几个关键的展开:

Redis 的问题不在能力,在身份:消息系统是它的副业。Pub/Sub 官方定性 at-most-once,Streams 是另一套 API 并且消息和缓存抢同一份内存。它继续当”我已经有一个 Redis 了,顺手发个通知”的选择,别让它转正。

Kafka 的定位就是”event streaming platform”,官方文档自己列的是发布订阅 + 持久存储 + 流处理 + Connect 生态三合一。重放能力是它的王牌,几百个官方 connector 的生态别家比不了。代价是概念和运维的面:分区、再均衡、ISR、位点提交,加上我部署时那串环境变量。如果你的事情是日志管道、CDC、流计算,Kafka 该上就上;如果你的事情只是”服务之间把事件传开”,我认为专门维护一个 Kafka 集群没必要。

RabbitMQ 是经典 broker 模型:exchange 按规则把消息路由进 queue,per-message 的 ack/reject/nack 语义很细,企业集成场景久经考验。代价是寻址链条长(declare exchange → declare queue → bind),消息一旦 ack 删除就没有回放这回事。成熟稳健这个词它担得起,”云原生”和”轻”就都不沾边了。

NATS 的差异化在上文已经铺垫:寻址像网络,持久化是可开关的第二层,部署形态是单进程。它不做流处理,官方给的是去 Kafka/Flink/Spark 的桥接工具——这条生态差距要认。

为什么我推荐它

铺垫完了,我的三条理由,各自附上依据也各自说清边界。

轻,轻到改变决策。 官方对 server 的描述是”单个二进制、亚毫秒延迟、典型约 15MB 内存”——15MB 是官方口径,我在自己机器上没有认真数过它的 RSS,但这个数量级和 Kafka 那种 JVM 起跳完全不是一个物种。更有参照意义的是 Kafka:它花了整整一个 4.0 才和 ZooKeeper 告别;NATS 从第一天就没有外部依赖,集群是 server 内嵌的,Raft 选举不需要谁来当裁判。一个二进制加一个 6.9MB 的 deb 包,就是全部的安装过程。轻不只是省内存,是起一个测试实例的成本低到你愿意随手起——运维习惯会跟着变的。

现代,云原生不是标语。 它是 CNCF 项目(官方站点目前标注 incubating,以 nats.io 为准);官方维护 Helm chart 和 NACK 控制器,Kubernetes 里按 CRD 声明 stream;监控端点内建(前面 varz 我直接 curl 了),SIGHUP 热加载配置;TLS、JWT、多租户 account 全是 server 自带。而”现代”更实质的一层是前面那个 request/reply:2010 年代之前的 MQ 假设服务是长命的、位置是固定的;微服务时代实例朝生夕死,NATS 的 location transparency 是按这个场景长出来的。

灵活,一套原语拼出所有模式。 pub/sub、队列分摊、RPC 式的 request/reply、流式持久化、KV 存储、大对象存储,全部是 subject 这一个概念往上叠的,MQTT 和 WebSocket 协议也是同一个 server 开配置就能听。更妙的是拓扑能长大:单机 → route 集群 → leaf node 边缘接入 → supercluster 跨云,换形状的时候 API 一行不改。传统 MQ 里”换个部署形态”约等于”迁一次集群”,在 NATS 里只是加几行配置。

推荐是有条件的,所以把三条对应的代价放这儿:中文资料少,英文文档和 Slack 社区算活跃,但你得啃英文;没有流处理生态,重管道场景该 Kafka 还是 Kafka;JetStream 相比几十岁的各家算年轻,2.x 版本迭代不慢(2026 年里的 2.15 刚把 stream 默认 consumer 上限、存储同步行为这些细节动过),追新功能时留意升级说明。

该用谁:一句一个

我的选择习惯大致是这样,仅供参考:

  • 已经有 Redis,只需要实时通知的玩具和小工具:Pub/Sub 就够,丢了就丢了;
  • 事件要被反复重放、当数据管道用、几十路 connector 进出:Kafka;
  • 复杂的入队路由规则、老派企业集成、per-message 精细控制:RabbitMQ;
  • 服务之间的实时事件总线——要广播、要分摊、要 RPC 手感、偶尔要持久化、还希望一个二进制讲完所有故事:NATS。

话虽然这么说,但是其实很多业务并没有Kafka的吞吐量需求,也没有RabbitMQ的复杂路由需求。NATS的特性完全可以支持你一把梭的微服务事件总线需求,而且部署和运维成本低得多。甚至你可以在设计的时候直接干掉注册中心,直接用 request/reply 做服务间通信,NATS 的 location transparency 就是为这种场景设计的。配合 KV 存储和 JetStream,完全足以胜任微服务的事件总线和轻量级 RPC。梭哈!

这个系列

后续随缘写几篇有关实战和我暂时没有提到的特性,比如KV、对象存储、leaf node、supercluster、NACK 控制器、JetStream 的高级特性等。好久没有见过让我眼前一亮的中间件了,NATS 让我有了这种感觉。瘾上来了,想写点东西。

参考资料

  • 标题: 更轻 更现代 更灵活,聊聊我为什么推荐 NATS——NATS 系列(1)
  • 作者: Kaku
  • 创建于 : 2026-09-23 17:11:00
  • 更新于 : 2026-09-23 17:44:22
  • 链接: https://www.kakunet.top/2026/09/23/更轻-更现代-更灵活,聊聊我为什么推荐-NATS——NATS-系列(1)/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论