自架服務 NATS JetStream Kafka 訊息佇列

NATS JetStream 2.12 把 Kafka 剩下的空缺補齊:一支 20 MB 的 Go binary 撐起自架訊息骨幹

Kafka 為了訊息骨幹要架三節點 KRaft、每臺 8 GB 記憶體起跳。NATS 2.12 補上 atomic batch publish、delayed message、distributed counter,把過去「功能有缺」這條退路也堵掉,自架訊息中間件的門檻整個下修。

Kafka 作為「大規模訊息骨幹」的預設答案已經超過十年,但這個地位放在自架 VPS 情境下越來越尷尬。一個能實際上生產的三節點 KRaft 叢集,起碼要 24 GB 以上的 RAM、加起來十幾顆 CPU 核心,還得處理 JVM tuning、topic partition 規劃、跨機房同步。對只有幾個服務要串接、每秒訊息量在中低水位的團隊來說,這個成本並不合理。

NATS 這幾年一直是 Kafka 之外的另一條路,但在功能面上有幾個明顯空缺——沒有原生的 atomic batch、缺 delayed message、也缺乏 counter 語意。NATS Server 2.12 在 2025 年 9 月釋出,一次把這三件事補上,2.14 的 release candidate 又把 cron 排程跟 batch ingest 排進來。到這個時點,NATS JetStream 已經不再是「輕量但功能少」的替代品,而是能認真對打 Kafka 的自架選項。

自架 Kafka 為什麼一直不舒服

Kafka 的門檻不在於它慢或不穩,而在它的營運成本被設計得很高。過去 Kafka 依賴 ZooKeeper 管 metadata,光是 ZooKeeper 本身就是一組三節點的 JVM 應用。3.3 之後 KRaft 模式進入 GA、3.5 之後預設啟用,但這只是把 ZooKeeper 內化,並沒有把資源需求變小——生產環境的 KRaft controller quorum 每臺仍然建議 4 GB 以上 heap,加上 broker 本身的 heap 跟 page cache 需求,記憶體用量隨便就破 8 GB。

第二個痛點是 topic 設計。Kafka 的 partition 是為了平行消費而存在,設計時就得預估 consumer 的最大平行度。partition 開太少擴不出去、開太多 metadata 又炸開;跨機房或跨區複製還得配 MirrorMaker 2 這樣的獨立元件。這些取捨在 GB 等級以上的日常流量才划得來,對只有幾個 microservice 需要事件通知的情境,複雜度遠超收益。

第三是升級路徑。Kafka 的 KRaft 遷移在正式釋出後仍然充滿驚喜,光是把運行中的 ZooKeeper 模式叢集切到 KRaft 就是一套完整的 migration 流程;4.x 之後 ZooKeeper 完全移除,還在跑舊模式的團隊變成必須先升到過渡版本再升過去。這種一週級別的維運工作,對兩三人的小團隊難以承受。

NATS 的定位一直不同

NATS 的架構出發點跟 Kafka 相反。它把「訊息路由」設計成應用之間的低延遲連接組織,核心行為只有 pub/sub、request/reply、queue group 三種模式。整臺 server 是一支 Go binary,壓縮後大約 20 MB,設定檔通常十幾行就寫完。叢集用 full-mesh 自組,加一個節點只要在設定裡多寫一行 route。

過去這個設計的取捨很明顯:NATS 不做持久化。訊息如果沒被訂閱者當下接到就消失,這個模型能撐住每秒百萬級的 fanout 吞吐量,代價是無法拿來做事件溯源、audit trail、或需要 replay 的工作。

JetStream 就是為了補這個缺口做出來的。它把 stream 的持久化能力疊在 core NATS 之上,同一支 binary、同一個 protocol、同一組客戶端,只要在設定開 jetstream: { store_dir: /data } 就能用。stream 支援 file 或 memory 儲存、可以設 replicas 做 quorum 複製、consumer 有 push 跟 pull 兩種模式,行為上跟 Kafka 的 consumer group 對得起來。

2.12 補了哪些過去被質疑的空缺

過去對 NATS 最常見的技術質疑集中在三件事:一次寫多筆訊息無法原子化、沒有排程延遲送出的機制、缺乏 counter 語意。這幾件事在 Kafka 這邊都有現成方案,於是變成「NATS 適合輕量情境、上規模還是要 Kafka」的分水嶺。2.12 這個版本把這條線抹掉。

Atomic batch publish 讓 client 可以把 N 筆訊息包在一個原子操作裡送出。訊息會先在 server 端 staging 驗證,全部通過才 commit,不會出現半途失敗、部分寫入的情況。這對 outbox pattern 這類需要跟資料庫 transaction 對齊的場景直接可用,過去要靠 idempotent 設計或去重 window 撐過去的工作,現在有原生語意可以依賴。

Delayed message scheduling 讓訊息可以指定 X 秒後才被投遞。這個功能長期以來是 SQS 相對 Kafka 的優勢之一,也是很多團隊為了做延遲任務去多架一組 Redis + BullMQ 的原因。2.12 把它塞進 stream 語意,工作排程、重試 backoff、預約通知這類需求不再需要額外元件。2.14 排入了 cron 表達式,接下來連週期性排程都會在 stream 內部完成。

Distributed counters 把 stream 當作可增減的計數器用。每個 subject 對應一個 big.Int、沒有上限,可以透過 stream mirroring 或 sourcing 做 CRDT 聚合。這個語意過去在 NATS 上得手動實作,原生支援之後,計數類的 metric、限流、庫存扣減都變得直接。

除了這三個功能,2.12 也加入 mirror 可以晉升成 primary 的能力,等於補上一條原地容災切換路徑。舊版本的 mirror 只能 read-only,出事時要切主寫入還得手動搬 stream 定義。

資源占用差距在哪

一個實務可跑的三節點 NATS JetStream 叢集,官方建議每臺至少 4 vCPU、8 GB RAM。這個數字看起來跟 Kafka 差不多,但要注意兩個結構性差異。第一,NATS 的記憶體使用主要來自 file store cache 跟 metadata,本質上是 Go runtime 的行為,設 GOMEMLIMIT 就能穩定壓在指定值以內;不像 JVM 的 heap 需要慢慢摸索 G1、ZGC 跟各種 flag 的組合。第二,NATS 沒有 ZooKeeper 或 KRaft controller 這種獨立元件——三個節點跑同一支 binary、彼此對等,故障恢復不會涉及 metadata quorum 的額外協調。

對訊息量在每秒幾千到幾萬筆的 workload,NATS 單節點就能勝任,三節點叢集完全是為了 HA 才存在。同樣的流量拿 Kafka 跑,光是 broker 本身就吃掉更多資源,還要處理 partition rebalance 的 GC 壓力。臺灣 VPS 常見的 4 vCPU / 8 GB 規格,跑 NATS 三節點叢集是舒服的,跑 Kafka 則在勉強線上。

什麼情況下仍該選 Kafka

有幾種情境 Kafka 仍然是更好的選擇,不必為了輕量硬去用 NATS。

多組獨立的下游團隊要以不同方式消費同一份資料流時,Kafka 的 partition 模型跟 consumer group offset 管理設計得成熟。想像一份訂單事件同時餵給 data warehouse、fraud detection、通知服務、報表引擎,四組都需要各自的 offset、各自的重播能力、各自的 lag 監控——這個場景 Kafka 生態的 Connect、Streams、Schema Registry 一路串起來,NATS 生態要拼湊得多。

要跟 Kafka 生態圈的既有工具打通時,Kafka 的相容性難以替代。Debezium 對接 CDC、Flink 或 Materialize 做 stream processing、Iceberg 的 sink connector,這些都是 Kafka wire protocol 為主的世界。NATS 有 Kafka bridge 但終究是橋接。

單一 topic 每秒突破十萬筆訊息、單筆訊息 MB 級的極端吞吐情境,Kafka 的順序寫入 log segment 架構在硬體上仍然是最有效率的。這種規模的自架情境通常已經進入專職 SRE 團隊管理的階段,成本考量的優先度早已被穩定性壓過。

反過來說,服務數量在十個以下、事件流量在每秒幾千筆以內、團隊想把訊息基礎設施的營運成本壓到最低,NATS 現在幾乎沒有拒絕的理由。

部署起來大概長什麼樣

單節點 JetStream 在 VPS 上開起來大約是這樣:

1
2
3
4
5
jetstream {
store_dir: /var/lib/nats/jetstream
max_memory_store: 2GB
max_file_store: 50GB
}

多節點就多寫 cluster 區塊,把三個 IP 互相寫進 routes 就完成。stream 的建立可以用 nats CLI 或 client library 動態做,不像 Kafka 需要事先開一輪 topic 設計會議。

要塞進 Docker、Kubernetes 也很直接。官方 Helm chart 把三節點 StatefulSet、PVC、跟 metrics exporter 都預設好,helm install 完就是能跑的叢集。這也是 NATS 相對 Kafka 一直維持的一個優勢——同一份設定可以從單機 VPS 一路擴展到多節點 K8s,不用改架構。

儲存端要注意的是 file store 走 OS page cache,NVMe SSD 的效能會直接反映在 stream 寫入延遲上。臺灣 VPS 的 NVMe 環境下,單節點 stream 寫入 latency 落在 sub-millisecond 是常態;跑在 EBS 或雲端 block storage 上,這個數字會拉高兩到五倍,對延遲敏感的應用要納入考量。

從 Kafka 遷過去的路徑

實務上很少會有「一次全部遷過去」的情境,比較常見的是逐個 topic 評估。事件流量小、consumer 少、沒有跟 Debezium 或 Flink 串接的 topic 可以優先搬。NATS 有官方的 Kafka bridge,讓兩邊在遷移期間並行——來源 Kafka 的訊息會被 bridge 轉發到 NATS stream,下游服務可以逐個切換 client,不必一刀兩斷。

真正需要注意的是資料模型的重新設計。Kafka 的 topic + partition 對應到 NATS 的 stream + subject,subject 支援分層命名(例如 orders.tw.web.created)以及 wildcard 訂閱,這個模型比 Kafka 的 partition key 靈活得多,但也意味著遷移不是 1:1 對應,而是重新規劃 subject 拓撲。這一步花掉的時間往往比技術遷移本身多。

訊息骨幹放回自己控制的機器

自架訊息系統的難度過去被 Kafka 拉高過一次。NATS 這幾年慢慢把功能面補齊到能對打 Kafka,2.12 之後幾乎不存在「因為缺功能不能用」的情境。剩下的取捨變回單純的規模跟生態問題,選擇邏輯終於變得健康。

跑訊息骨幹最直接影響體驗的兩個變數是網路穩定性與磁碟延遲。NCSE Network 的 VPS 主機位於臺灣是方電訊機房,搭載 Intel Gold CPU 與 NVMe SSD,同機房跨機節點延遲可壓在毫秒等級,適合架 NATS 三節點叢集或 Kafka broker cluster。想把訊息中間件從雲端託管拉回可控環境,臺灣本地的 VPS 是可以直接接的下一步。

需要穩定的雲端主機?

NCSE Network 提供企業級 VPS,7 天免費試用,臺灣是方電訊機房,99% SLA 保證。

查看 VPS 方案 →