实现一个“消息系统”,方式有很多,每种方式都有它自己的适用边界。就像我们在设计项目时,从最初的事件 Hook,到三层架构、六边形架构,再到 DDD,每一步选择的背后都是边界和责任的考量。
在做项目时,选择哪种消息系统,不是为了给项目增加负担,而是要在正确的边界上,用对的方式,既保证功能,又保持系统轻量和可维护性。
很多朋友,只要一提到“消息系统”,Kafka 几乎成了默认答案。它高吞吐、生态成熟、社区活跃,看起来无可替代。
但当你真正把 Kafka 引入到一个 Golang 微服务项目 中,用上一段时间之后,往往会产生一个不太好意思说出口的感受:
Kafka 很强,但我们真的用到它的“强”了吗?
为了一个服务解耦、一次异步通知、几个内部事件,引入一整套 Kafka 集群、Topic 规划、Consumer Group 和 Offset 管理,成本并不低,而收益却并不总是匹配。很多团队还会考虑 RabbitMQ,但它在 Go 微服务内部通信场景下,同样会带来额外复杂度。
也正是在这样的背景下,我开始在多个 Go 项目中尝试使用 NATS。这篇文章,不是站队,也不是拉踩,而是一次 真实使用体验的复盘。
一、问题不在 Kafka,而在使用场景
先说结论:Kafka 没有问题,用错地方才是问题。
在很多 Golang 项目中,Kafka 常被用在下面这些场景:
- 微服务之间解耦
- 订单创建后的异步处理
- 内部事件广播
- 控制面通知
但这些场景,本质需求只有三个:
- 解耦
- 异步
- 稳定
而 Kafka 的核心优势是什么?
你会发现一个明显的不匹配:
我们想要的是“通信能力”,却背上了“数据管道系统”的重量。
在 Go 微服务中,这种“重”体现在:
- 部署和运维成本高
- 开发阶段心智负担大
- 本地调试和测试成本高
- 对延迟并不友好
Kafka 很强,但并不是每一次异步都值得它出场。
顺带提一句,RabbitMQ 在企业级系统中也很常见,它擅长解决 业务异步和消息可靠投递 ,拥有 Exchange、Queue、Binding 等概念。但在 Golang 微服务内部通信场景下,这些能力并不会真正用上,反而增加了开发和运维复杂度。
二、我是为什么开始尝试 NATS 的
第一次接触 NATS,并不是因为它“新”,而是因为它 刚好补上了 Kafka 没覆盖好的那一段空档。
当时我的诉求非常简单:
- 服务之间能不能不用 HTTP 强耦合?
- 能不能有一种“像 RPC,但更松散”的通信方式?
- 能不能不用为此维护一套复杂的消息基础设施?
NATS 给我的第一感受只有一句话:
它不像一个“消息中间件”,更像是分布式系统里的通信底座。
没有复杂的概念,没有厚重的配置,Pub/Sub、Request/Reply 几乎开箱即用。对 Go 开发者来说,这种体验非常自然。
三、一个真实场景:订单系统的解耦实践
这是一个非常典型、也非常现实的业务场景。
1. 改造前:同步调用的“链式反应”
订单创建后,需要做这些事情:
最初的实现方式通常是:
Order Service
├── Inventory Service
├── Notify Service
├── Risk Service
└── Recommend Service
问题很快出现:
- 任一服务变慢,订单接口就变慢
- 任一服务失败,主流程就失败
- 新增一个下游改一堆代码
2. Kafka 方案:解耦了,但有点“重”
后来我们用 Kafka 做异步解耦,逻辑上没问题,但很快暴露出新的现实问题:
- Topic 设计成本高
- 消费者组管理复杂
- 运维成本不低
- 对于内部事件,显得有些“大材小用”
3. NATS 方案:事件驱动真正落地
最终的方案非常简单:
订单服务只做一件事:发布事件。
nc.Publish("order.created", data)
下游服务各自订阅:
nc.Subscribe("order.created", handler)
改造后的效果非常直接:
- 订单接口响应时间明显下降
- 下游服务完全解耦
- 新业务接入只需新增订阅者
这一次,我们真正感受到了“事件驱动架构”的轻量感。
四、NATS 真正好用的地方,不在概念里
很多文章会花大量篇幅介绍 NATS 的架构,但在实战中,真正频繁用到的只有三点:
1. Pub / Sub:天然适合内部事件
Subject 天然支持通配符,非常贴合业务语义:
order.created
order.paid
order.canceled
2. Request / Reply:被严重低估的能力
这是 NATS 在 Go 项目中 极具杀伤力 的一点。
msg, _ := nc.Request("user.query", data, time.Second)
你可以:
- 像 RPC 一样调用
- 又不绑定具体服务实例
- 保留异步系统的弹性
在内部系统中,这种模式比 HTTP + JSON 舒服得多。
3. JetStream:只在“该重的时候重”
当你真的需要:
- 消息持久化
- 至少一次投递
- 消费确认
- 消息回放
再引入 JetStream,而不是一开始就上重武器。
五、说清楚边界:NATS 不是 Kafka 的替代品
这是必须讲清楚的一点。
如果你的需求是:
- 日志收集
- 数据分析
- 海量数据流处理
- 离线计算
那么 Kafka 依然是正确选择。
一句话总结边界:
NATS 解决的是“服务之间怎么说话”,Kafka 解决的是“数据怎么流动”。
如果你用 NATS 去做 Kafka 的事情,迟早会踩坑;反过来,用 Kafka 去做服务通信,也会感到笨重。
六、Golang 项目里的消息系统选型建议
如果你只记住这一节,就够了。
- 微服务内部通信 → NATS
- 业务事件驱动 → NATS
- 关键链路、强可靠 → NATS + JetStream
- 数据管道、日志系统 → Kafka
- 企业消息异步 / 复杂路由 → RabbitMQ
不要问“哪个更高级”,只问 哪个更合适。
七、写在最后:真正的成熟,是不过度设计
回头看这些年的系统演进,我越来越确信一件事:
好的架构,不是堆出来的,而是克制出来的。
Kafka 依然很强,RabbitMQ 依然稳妥,也依然重要;但在 Golang 微服务的世界里,NATS 往往更贴合工程直觉。
工具不是信仰,选型才是能力。
希望这篇基于真实项目体验的分享,能为你下一个 Go 微服务的技术选型带来一些启发。更多关于架构设计和 Go 实践的深入讨论,欢迎在云栈社区交流。