Skip to content

08 - NATS 集群架构与 Route Pool ​

本文是 NATS 指南系列第八篇,深入讲解 NATS 集群的工作原理、Route Pool 机制、订阅信息传播,以及生产环境的配置与调优。


目录 ​

  1. 集群的作用
  2. 集群配置最低要求
  3. Route Pool 详解
  4. Subject Interest Propagation
  5. 集群消息路由完整流程
  6. Headless Service DNS 的重要性
  7. 配置示例
  8. 三种网络拓扑对比
  9. 滚动升级时集群的行为
  10. routez 端点详细字段解读
  11. 小结

1. 集群的作用 ​

1.1 为什么需要集群 ​

单节点 NATS 服务器虽然性能极高(单机每秒可处理数百万消息),但在生产环境中面临两个根本问题:

单节点痛点:

  ┌─────────────┐
  │ NATS Node   │◀── 所有客户端连接
  │ (单点)      │
  └──────┬──────┘
         │
    重启/崩溃
         │
         ▼
  所有客户端断连!
  消息处理中断!
  业务停摆!

NATS 集群解决两个核心问题:

问题 1:高可用(HA)

集群节点故障时自动转移:

  ┌─────────┐    ┌─────────┐    ┌─────────┐
  │ Node-1  │◀──▶│ Node-2  │◀──▶│ Node-3  │
  │(Primary)│    │(Standby)│    │(Standby)│
  └─────────┘    └─────────┘    └─────────┘
       │
   节点崩溃
       │
       ▼
  客户端自动重连到 Node-2 或 Node-3
  消息路由不受影响

问题 2:水平扩展

客户端分散连接到不同节点,避免单点成为瓶颈:

  Client-A ──▶ Node-1
  Client-B ──▶ Node-2  ←→ 节点间消息路由(Route)
  Client-C ──▶ Node-3
  Client-D ──▶ Node-1

  Subscriber 可以连接任意节点,Publisher 也可以连接任意节点,
  NATS 集群内部负责消息路由。

1.2 集群 vs 单节点性能对比 ​

单节点:
  所有消息经过 1 台服务器
  内存: 受限于单机
  QPS:  ~4M msg/s(参考值)
  延迟: ~0.1ms

3节点集群(客户端均匀分布):
  每台服务器处理约 1/3 的客户端流量
  需要额外的路由流量(节点间通信)
  QPS:  ~2-3M msg/s(单节点视角,集群总吞吐更高)
  延迟: ~0.1-0.3ms(增加了节点间路由延迟)

2. 集群配置最低要求 ​

2.1 奇数节点原则 ​

NATS 集群(不含 JetStream)本身不使用 Raft 共识算法,因此纯 Core NATS 集群对节点数量没有奇数要求。但当启用 JetStream 时,其元数据 Raft 群组需要奇数节点才能形成多数派:

JetStream Raft 多数派:

  节点数    Raft 多数    容忍故障数
  ─────────────────────────────────
    1          1            0
    2          2            0  ← 任一故障即失效,不推荐
    3          2            1  ← 生产最低要求
    5          3            2  ← 大规模生产
    7          4            3  ← 非常高可用场景

推荐:生产最少 3 节点

2.2 网络要求 ​

集群间通信端口(默认 6222):

  Node-1:6222 ◀──▶ Node-2:6222
  Node-1:6222 ◀──▶ Node-3:6222
  Node-2:6222 ◀──▶ Node-3:6222

客户端连接端口(默认 4222):
  Client ──▶ NodeX:4222

监控端口(默认 8222):
  HTTP ──▶ NodeX:8222/routez

2.3 集群名称一致性 ​

关键要求:所有节点的 cluster.name 必须完全相同,否则节点拒绝加入集群:

错误示例(导致集群无法形成):
  Node-1: cluster { name: "prod-cluster" }
  Node-2: cluster { name: "prod-cluster" }
  Node-3: cluster { name: "prod"         }  ← 名称不一致!

3. Route Pool 详解 ​

3.1 为什么需要多条 TCP 连接 ​

NATS 集群节点之间通过 TCP Route 连接传递消息。早期版本节点间只有 1 条 TCP 连接,这带来了严重的**行头阻塞(Head-of-Line Blocking, HOL Blocking)**问题:

单 TCP 连接的行头阻塞:

  Node-1                              Node-2
    │                                    │
    │──── 大消息(5MB)────────────────▶ │  ← 占满带宽,阻塞后续
    │──── 小消息(100B)等待排队 ────▶   │  ← 必须等大消息传完
    │──── 心跳(ping)等待排队 ────▶     │  ← 延迟增大
    │                                    │

TCP 本质上是一个有序字节流,后面的数据必须等待前面的数据传输完成。当一个大消息占据 TCP 缓冲区时,后续的小消息(包括心跳、订阅更新等控制消息)都会被阻塞。

3.2 Route Pool 的解决方案 ​

NATS 2.2 引入了 Route Pool:节点间建立多条 TCP 连接(一个"连接池"),消息通过不同连接并行传输:

Route Pool(默认 pool_size=4):

  Node-1                              Node-2
    │                                    │
    ├──── TCP Route Connection #1 ──────▶│  用于普通消息
    ├──── TCP Route Connection #2 ──────▶│  用于大消息
    ├──── TCP Route Connection #3 ──────▶│  用于控制消息
    └──── TCP Route Connection #4 ──────▶│  用于高优先级
    │                                    │

现在不同消息可以走不同连接,互不阻塞:

有了 Route Pool 后:

  Node-1                              Node-2
    │                                    │
    │──[Conn#1]── 大消息(5MB)─────────▶│
    │──[Conn#2]── 小消息(100B)─────────▶│  ← 不再等待!
    │──[Conn#3]── 心跳(ping)───────────▶│  ← 立即到达!
    │                                    │

3.3 pool_size 默认值 4 的由来 ​

NATS 官方将 pool_size 的默认值设为 4,这是基于以下考量的权衡:

pool_size 权衡分析:

  太小(1-2):
    - 行头阻塞风险依然存在
    - 高并发时消息排队延迟高

  太大(8+):
    - 每条 TCP 连接消耗文件描述符
    - 操作系统 TCP 缓冲区内存占用
    - 连接建立/维护开销增加
    - 收益递减(并发消息流通常不超过 4 个方向)

  4 = 甜蜜点:
    - 覆盖大多数并发场景(读写 × 控制数据流)
    - 内存/FD 开销可控
    - 实测延迟抖动显著降低

还有一条额外的专用连接(pinned account routes),专门用于系统账户消息,不计入 pool_size。

3.4 num_routes 计算公式 ​

对于 N 节点集群,每个节点需要与其他 (N-1) 个节点各建立 pool_size 条连接,因此每个节点的总路由连接数为:

num_routes(单节点) = (N - 1) × pool_size

另加:每个 peer 节点 1 条专用 account route(可选):
num_routes(含账户路由)= (N - 1) × (pool_size + 1)

3.5 三节点集群推导:8 条 Route 连接 ​

以 3 节点集群、pool_size=4 为例,完整推导每个节点的连接数:

集群拓扑:

         Node-1
        /       \
   Node-2 ─── Node-3

每个节点需要连接到其他 2 个节点(N-1 = 3-1 = 2)
每个 peer 建立 pool_size=4 条连接
每个节点总连接数 = 2 × 4 = 8 条 Route 连接

具体到 Node-1:

Node-1 的 Route 连接详情:

  与 Node-2 的连接(4 条):
    Route #1 (to Node-2): 用于消息路由
    Route #2 (to Node-2): 用于消息路由
    Route #3 (to Node-2): 用于消息路由
    Route #4 (to Node-2): 用于消息路由

  与 Node-3 的连接(4 条):
    Route #5 (to Node-3): 用于消息路由
    Route #6 (to Node-3): 用于消息路由
    Route #7 (to Node-3): 用于消息路由
    Route #8 (to Node-3): 用于消息路由

  总计:8 条 TCP 连接(从 Node-1 视角)

ASCII 全图:

3节点集群 Route Pool 全图(pool_size=4):

┌──────────────────────────────────────────────────────────────────┐
│                                                                  │
│   Node-1                  Node-2                  Node-3         │
│  ┌───────┐               ┌───────┐               ┌───────┐      │
│  │       │═══[R1]════════│       │═══[R5]════════│       │      │
│  │       │═══[R2]════════│       │═══[R6]════════│       │      │
│  │       │═══[R3]════════│       │═══[R7]════════│       │      │
│  │       │═══[R4]════════│       │═══[R8]════════│       │      │
│  │       │               │       │               │       │      │
│  │       │═══[R5']═══════╪═══════╪═══[R1']═══════│       │      │
│  │       │═══[R6']═══════╪═══════╪═══[R2']═══════│       │      │
│  │       │═══[R7']═══════╪═══════╪═══[R3']═══════│       │      │
│  │       │═══[R8']═══════╪═══════╪═══[R4']═══════│       │      │
│  └───────┘               └───────┘               └───────┘      │
│                                                                  │
│  Node-1 视角:8条(4到Node2 + 4到Node3)                        │
│  Node-2 视角:8条(4到Node1 + 4到Node3)                        │
│  Node-3 视角:8条(4到Node1 + 4到Node2)                        │
│                                                                  │
│  整个集群 TCP Route 连接总数:                                   │
│    节点对数 = C(3,2) = 3 对                                      │
│    每对 4 条(双向各算,实际每条 TCP 双向复用)                   │
│    总计 = 3 × 4 = 12 条 TCP 连接(集群全局)                     │
└──────────────────────────────────────────────────────────────────┘

注意:TCP 连接是全双工的,一条 TCP 连接可以双向传输数据。上图中 Node-1 看到 8 条"连接",其中有 4 条是它主动发起的(到 Node-2、Node-3),另外 4 条是 Node-2/Node-3 主动发起的(到它),但实际是同一批 TCP 连接的两端视角。

实际全局唯一 TCP 连接数 = (N-1) × N / 2 × pool_size = 3 × 4 = 12(3 节点集群)。

3.6 连接建立与关闭行为 ​

启动时的连接建立顺序:

时序:Node-1、Node-2 先启动,Node-3 后加入

T=0s: Node-1 启动,监听 :6222,无 route 连接

T=1s: Node-2 启动
  Node-2 尝试连接 Node-1:6222
  Node-2 ──[Conn#1]──▶ Node-1  (pool 第 1 条)
  Node-2 ──[Conn#2]──▶ Node-1  (pool 第 2 条)
  Node-2 ──[Conn#3]──▶ Node-1  (pool 第 3 条)
  Node-2 ──[Conn#4]──▶ Node-1  (pool 第 4 条)
  握手完成,互换集群状态(订阅信息等)

T=2s: Node-3 启动
  Node-3 尝试连接 Node-1:6222(4条)
  Node-3 尝试连接 Node-2:6222(4条)
  集群形成,订阅信息在 3 节点间同步

节点故障时的关闭行为:

Node-2 崩溃:

  Node-1 检测到 Node-2 的 4 条 TCP 连接全部断开
    → 立即将 Node-2 标记为 offline
    → 清除 Node-2 上所有订阅的路由表
    → 通知连接到 Node-1 的客户端(如有需要)

  Node-3 检测到 Node-2 的 4 条 TCP 连接全部断开
    → 同样清除路由信息

  恢复时(Node-2 重启):
    → Node-2 重新建立 4×2=8 条 route 连接
    → 重新同步订阅信息(RS+)
    → 恢复消息路由

实际日志中看到的现象(nats-server -DV):

log
[DBG] Route connect to: nats://node2:6222
[DBG] Route connect to: nats://node2:6222
[DBG] Route connect to: nats://node2:6222
[DBG] Route connect to: nats://node2:6222
[TRC] Sending CONNECT to route: {"verbose":false,"pedantic":false,...}
[TRC] Route received [PONG]
[INF] Route connection created: nats://node2:6222
...(重复 4 次,建立 pool)
[INF] Cluster Route Stats: {NumRoutes:8, ...}

# 节点下线时:
[WRN] Detected route disconnect from: nats://node2:6222
[WRN] Detected route disconnect from: nats://node2:6222
[WRN] Detected route disconnect from: nats://node2:6222
[WRN] Detected route disconnect from: nats://node2:6222
[INF] Removing route: nats://node2:6222 (cluster changed)

3.7 pool_size 调优建议 ​

场景                          建议 pool_size
─────────────────────────────────────────────
开发/测试                          1-2
生产(小规模, <1000 clients)       4(默认)
生产(中规模, 1k-10k clients)      4-6
生产(大规模, >10k clients)        6-8
消息体普遍较大(>100KB)            6-8
消息体普遍极小(<1KB)              4(默认足够)
节点间带宽受限(低延迟专线)         2-4

修改方式:

hcl
# nats-server.conf
cluster {
  name: "prod"
  port: 6222
  pool_size: 6  # 覆盖默认值 4

  routes: [
    "nats://node2:6222"
    "nats://node3:6222"
  ]
}

4. Subject Interest Propagation ​

4.1 什么是 Subject Interest Propagation ​

NATS 集群中,每个节点只将消息路由到有订阅者的节点,而不是广播给所有节点。这依赖于订阅兴趣传播机制:当一个客户端在某节点上订阅某个 subject 时,该节点会将这个"兴趣"通知给集群中的其他所有节点。

订阅兴趣传播示意:

  客户端 C1 连接 Node-1,订阅 "orders.*"

  Node-1 收到订阅后:
    → 本地记录:orders.* 有 1 个本地订阅
    → 向 Node-2 发送:RS+ $G orders.*(我有订阅者了)
    → 向 Node-3 发送:RS+ $G orders.*

  Node-2 收到 RS+ 后:
    → 记录路由表:orders.* → 有兴趣的远程节点:Node-1
    → 向 Node-3 发送:RS+ $G orders.*(如果 Node-3 还不知道的话)

  此后,任何发布到 orders.* 的消息,Node-2/Node-3 都知道要路由到 Node-1。

4.2 RS+ / RS- 消息机制 ​

NATS 集群内部使用 Route Subscribe 协议帧:

RS+(Route Subscribe):有新订阅
  格式:RS+ <account> <subject> [queue_group [weight]]
  示例:RS+ $G orders.new
        RS+ $G orders.process workers 3  ← Queue Group,weight=3(3个成员)

RS-(Route Unsubscribe):订阅取消
  格式:RS- <account> <subject> [queue_group]
  示例:RS- $G orders.new
        RS- $G orders.process workers  ← Queue Group 成员减少

$G = Global account(默认账户)

RS+ 的 weight 参数(Queue Group 专用)表示该节点上 Queue Group 的成员数,用于加权随机分发:

场景:Node-2 上有 3 个 Worker 订阅了 Queue Group "workers",
      Node-3 上有 1 个 Worker

  Node-2 → 其他节点发送:RS+ $G tasks.process workers 3
  Node-3 → 其他节点发送:RS+ $G tasks.process workers 1

  当 Node-1 上的客户端发布消息时,Node-1 知道:
    - Node-2 上有 3 个成员(权重 3)
    - Node-3 上有 1 个成员(权重 1)
  → 75% 概率路由到 Node-2(在 Node-2 内部再选一个成员)
  → 25% 概率路由到 Node-3
  总体上各 Worker 被选中概率均等(25%)

4.3 发布时如何找到有订阅者的节点 ​

发布流程:

  Publisher(连接 Node-1)发布 "orders.new" 消息

  Node-1 查询本地路由表:
  ┌─────────────────────────────────────────────┐
  │           Node-1 路由表(内存中)             │
  │                                             │
  │  Subject      │ 本地订阅 │ 远程节点          │
  │  ─────────────┼──────────┼──────────────────│
  │  orders.*     │  2       │  Node-2, Node-3  │
  │  orders.new   │  0       │  Node-2          │
  │  orders.done  │  1       │  -               │
  │  metrics.#    │  0       │  Node-3          │
  └─────────────────────────────────────────────┘

  决策:
    - orders.new 匹配到 orders.* 和 orders.new
    - 本地有 2 个订阅 → 直接投递(2 次)
    - 远程 Node-2 有兴趣 → 通过路由连接发送给 Node-2
    - Node-3 通过 orders.* 也有兴趣 → 发送给 Node-3

  如果是 Queue Group,还需要 weight 加权路由:
    - 本地 Queue 成员 2 个
    - Node-2 Queue 成员 3 个
    - 本地概率 = 2/(2+3) = 40%,Node-2 概率 = 60%

4.4 Subject Interest 流程图(ASCII) ​

               订阅传播阶段(客户端连接时)
               ─────────────────────────────

  Client-A      Node-1       Node-2       Node-3
    │              │              │              │
    │─SUB orders.*─▶│              │              │
    │              │              │              │
    │              │──RS+ $G orders.*────────────▶│
    │              │──RS+ $G orders.*──────────▶│ │
    │              │              │              │
    │              │              │◀─RS+ $G orders.* (传播)
    │              │              │──RS+ $G orders.*──▶│ (如需要)
    │              │              │              │

               消息路由阶段(Publisher 发布时)
               ─────────────────────────────

  Client-B      Node-2       Node-1       Client-A
    │              │              │              │
    │─PUB orders.new─▶│           │              │
    │              │              │              │
    │              │ (查路由表:    │              │
    │              │  orders.* →  │              │
    │              │  有 Node-1)  │              │
    │              │──RMSG orders.new──────────▶│
    │              │              │              │
    │              │              │──MSG orders.new──▶│
    │              │              │              │

4.5 订阅信息的生命周期 ​

订阅信息传播的 4 个阶段:

  阶段 1:客户端订阅
    Client → Node: SUB <subject> <sid>

  阶段 2:本地节点通知集群
    Node-1 → Node-2: RS+ $G <subject>
    Node-1 → Node-3: RS+ $G <subject>

  阶段 3:运行时保持
    所有节点在内存路由表中维护完整的"兴趣图"
    新节点加入时,双方交换完整的订阅状态

  阶段 4:订阅取消
    Client → Node: UNSUB <sid>
    Node-1 → Node-2: RS- $G <subject>(如果本地无其他相同订阅)
    Node-1 → Node-3: RS- $G <subject>

5. 集群消息路由完整流程 ​

5.1 跨节点消息路由序列图 ​

以下是一个完整的跨节点消息路由流程,涵盖订阅建立、消息发布、路由到达全过程:

完整序列:

  Service(Sub)  Node-3       Node-2       Node-1    Client(Pub)
      │            │            │            │           │
      │            │            │            │           │
   【阶段 1:订阅建立】
      │            │            │            │           │
      ├─SUB────────▶│            │            │           │
      │  "svc.calc" │            │            │           │
      │            │            │            │           │
      │            ├──RS+ $G svc.calc────────▶│           │
      │            │            │            │           │
      │            ├──RS+ $G svc.calc──▶│    │           │
      │            │            │       (Node-2 记录:     │
      │            │            │        svc.calc→Node-3) │
      │            │            │            │           │
      │            │            │            │(Node-1 记录:
      │            │            │            │ svc.calc→Node-3)
      │            │            │            │           │
   【阶段 2:客户端发布请求】
      │            │            │            │           │
      │            │            │            │◀─PUB──────┤
      │            │            │            │  "svc.calc"
      │            │            │            │  Reply: _INBOX.1
      │            │            │            │           │
   【阶段 3:Node-1 路由决策】
      │            │            │            │           │
      │            │            │            │(查路由表: │
      │            │            │            │ svc.calc  │
      │            │            │            │ → Node-3) │
      │            │            │            │           │
      │            │            │◀─RMSG──────┤           │
      │            │            │  "svc.calc" │           │
      │            │            │  Reply: _INBOX.1        │
      │            │            │            │           │
   【阶段 4:Node-2 转发给 Node-3(如有必要)或直接有连接】
      │            │◀─RMSG──────┤            │           │
      │            │  "svc.calc" │            │           │
      │            │  Reply: _INBOX.1         │           │
      │            │            │            │           │
   【阶段 5:Node-3 投递给订阅者】
      │◀─MSG───────┤            │            │           │
      │  "svc.calc" │            │            │           │
      │  Reply: _INBOX.1         │            │           │
      │            │            │            │           │
   【阶段 6:Service 处理并回复】
      │            │            │            │           │
      ├─PUB────────▶│            │            │           │
      │  "_INBOX.1" │            │            │           │
      │  "result:42" │           │            │           │
      │            │            │            │           │
      │            │(查路由表:   │            │           │
      │            │ _INBOX.1   │            │           │
      │            │ → Node-1)  │            │           │
      │            │            │            │           │
      │            ├──RMSG──────────────────▶│           │
      │            │  "_INBOX.1" │            │           │
      │            │  "result:42" │           │           │
      │            │            │            │           │
      │            │            │            ├─MSG───────▶│
      │            │            │            │  "result:42"
      │            │            │            │           │

5.2 路由跳数优化 ​

NATS 集群中,消息最多经过 1 次路由跳转(从发布者节点 → 订阅者节点)。集群内不存在多跳路由:

NATS 的 Full-Mesh 拓扑:

  每个节点与其他所有节点直接相连
  → 任何两个节点间消息只需 1 跳
  → 时延可预测(不会因路径长度变化)

      Node-1
      /    \
  Node-2 - Node-3

  Node-1 → Node-3: 1 跳(直连)
  Node-2 → Node-3: 1 跳(直连)
  Node-1 → Node-2: 1 跳(直连)

这与某些采用环形或星形拓扑的消息系统不同(它们可能需要 2-3 跳)。Full-Mesh 是 NATS 集群的拓扑要求,所有节点都必须互相连接。


6. Headless Service DNS 的重要性 ​

6.1 Kubernetes 中的 NATS 集群发现 ​

在 Kubernetes 上部署 NATS 集群时,节点需要发现彼此的 DNS 地址。普通 ClusterIP Service(有虚拟 IP)不适合 NATS 集群内部通信,因为:

普通 ClusterIP Service 的问题:

  nats-cluster-svc: 10.96.0.100(虚拟 IP)
                     │
               kube-proxy 负载均衡
                /      |      \
           Pod-0    Pod-1    Pod-2

  Node-1(Pod-0)配置 routes: ["nats://nats-cluster-svc:6222"]
  → 实际连接到哪个 Pod?随机!
  → 可能连接到自己(Pod-0 → Pod-0),造成路由循环!
  → 无法建立 Full-Mesh 连接(无法固定连到特定节点)

Headless Service(clusterIP: None)的 DNS 解析返回的是所有 Pod 的真实 IP,配合 StatefulSet 的稳定 DNS 名称,每个节点可以精确连接到特定对端:

Headless Service DNS 解析:

  nats-0.nats.default.svc.cluster.local → 10.244.0.2(Pod-0 真实 IP)
  nats-1.nats.default.svc.cluster.local → 10.244.1.3(Pod-1 真实 IP)
  nats-2.nats.default.svc.cluster.local → 10.244.2.4(Pod-2 真实 IP)

  Node-1(Pod-0)配置:
  routes: [
    "nats://nats-1.nats.default.svc.cluster.local:6222"  → 固定到 Pod-1
    "nats://nats-2.nats.default.svc.cluster.local:6222"  → 固定到 Pod-2
  ]

  完美建立 Full-Mesh!

6.2 Headless Service 配置 ​

yaml
# headless-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nats                      # StatefulSet Pod DNS 前缀
  namespace: default
  labels:
    app: nats
spec:
  clusterIP: None                 # ← 关键:Headless
  selector:
    app: nats
  ports:
  - name: client
    port: 4222
    targetPort: 4222
  - name: cluster
    port: 6222
    targetPort: 6222
  - name: monitor
    port: 8222
    targetPort: 8222

6.3 DNS 问题排查 ​

bash
# 验证 Headless Service DNS 解析
kubectl run -it --rm dns-test --image=busybox --restart=Never -- \
  nslookup nats-0.nats.default.svc.cluster.local

# 预期输出(返回真实 Pod IP,不是 ClusterIP):
# Server:    10.96.0.10
# Name:      nats-0.nats.default.svc.cluster.local
# Address 1: 10.244.0.2 nats-0.nats.default.svc.cluster.local

# 如果返回的是 ClusterIP(虚拟 IP),说明使用的是普通 Service,需要改为 Headless

7. 配置示例 ​

7.1 完整集群配置(nats-server.conf) ​

hcl
# nats-server.conf(3 节点集群,每个节点一份,修改 server_name 和 routes)

# === 基础配置 ===
server_name: "nats-0"          # 每个节点唯一名称
listen: "0.0.0.0:4222"         # 客户端连接端口
http: "0.0.0.0:8222"           # 监控 HTTP 端口

# === 日志 ===
log_file: "/var/log/nats/nats.log"
log_time: true
logfile_size_limit: 100MB       # 单个日志文件大小上限
max_traced_msg_len: 1024        # 追踪消息时的最大输出长度

# === 客户端连接配置 ===
max_connections: 65536
max_payload: 8MB                # 最大消息体大小(根据业务调整)
max_pending:  32MB              # 单个客户端待处理最大缓冲
write_deadline: "10s"           # 写超时(客户端太慢时断开)

# === 集群配置(核心)===
cluster {
  name: "prod-cluster"          # ← 所有节点必须完全相同!

  # 本节点监听路由连接的地址
  listen: "0.0.0.0:6222"

  # pool_size:每个 peer 之间建立的 TCP 连接数(默认 4)
  pool_size: 4                  # 总连接数 = (N-1) × pool_size

  # 连接到其他节点(Node-0 的配置,连接到 Node-1 和 Node-2)
  routes: [
    "nats://nats-1.nats.default.svc.cluster.local:6222"
    "nats://nats-2.nats.default.svc.cluster.local:6222"
  ]

  # 连接超时和重连
  connect_retries: 120          # 启动时重试连接次数
  connect_error_reports: 5      # 连接错误上报频率
}

# === JetStream 配置(如需要)===
jetstream {
  store_dir: "/data/nats"
  max_memory_store: 4GB
  max_file_store: 100GB
}

# === 账户配置(多租户场景)===
# accounts {
#   MyAccount: {
#     users: [{user: "client", password: "s3cr3t"}]
#   }
# }

7.2 Node-1 配置(nats-1 的 routes 配置) ​

hcl
# Node-1 的配置(连接到 Node-0 和 Node-2)
server_name: "nats-1"

cluster {
  name: "prod-cluster"          # ← 与 Node-0 完全相同
  listen: "0.0.0.0:6222"
  pool_size: 4

  routes: [
    "nats://nats-0.nats.default.svc.cluster.local:6222"  # ← 连接到 Node-0
    "nats://nats-2.nats.default.svc.cluster.local:6222"  # ← 连接到 Node-2
  ]
}

7.3 Kubernetes StatefulSet 完整部署 ​

yaml
# nats-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nats
  namespace: default
spec:
  serviceName: nats              # 必须与 Headless Service 名称一致
  replicas: 3
  selector:
    matchLabels:
      app: nats
  template:
    metadata:
      labels:
        app: nats
    spec:
      # 优雅终止时间(确保 NATS 完成消息处理)
      terminationGracePeriodSeconds: 60
      containers:
      - name: nats
        image: nats:2.10-alpine
        ports:
        - containerPort: 4222   # client
        - containerPort: 6222   # cluster routes
        - containerPort: 8222   # monitoring
        command:
        - nats-server
        - -c
        - /etc/nats/nats.conf
        - --server_name           # 使用 Pod 名称作为 server_name
        - $(POD_NAME)
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        volumeMounts:
        - name: config
          mountPath: /etc/nats
        - name: data
          mountPath: /data/nats
        resources:
          requests:
            memory: "512Mi"
            cpu: "250m"
          limits:
            memory: "2Gi"
            cpu: "2000m"
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8222
          initialDelaySeconds: 10
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /healthz?js-enabled-only=true
            port: 8222
          initialDelaySeconds: 15
          periodSeconds: 5
      volumes:
      - name: config
        configMap:
          name: nats-config
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: "fast-ssd"
      resources:
        requests:
          storage: 20Gi
---
# ConfigMap 使用模板配置(通过环境变量注入 server_name)
apiVersion: v1
kind: ConfigMap
metadata:
  name: nats-config
data:
  nats.conf: |
    listen: "0.0.0.0:4222"
    http: "0.0.0.0:8222"
    max_payload: 8MB

    cluster {
      name: "prod-cluster"
      listen: "0.0.0.0:6222"
      pool_size: 4
      routes: [
        "nats://nats-0.nats.default.svc.cluster.local:6222"
        "nats://nats-1.nats.default.svc.cluster.local:6222"
        "nats://nats-2.nats.default.svc.cluster.local:6222"
      ]
    }

    jetstream {
      store_dir: "/data/nats"
      max_memory_store: 1GB
      max_file_store: 10GB
    }

注意:routes 列表可以包含本节点自身的 DNS 名称,NATS 会自动跳过(不会连接自己)。这样所有节点可以共用同一份 ConfigMap,无需为每个节点单独配置。


8. 三种网络拓扑对比 ​

8.1 拓扑总览 ​

NATS 支持三种网络拓扑,适用于不同场景:

  1. Cluster(集群)
     ─────────────
     同一数据中心 / 同一地域
     所有节点全互联(Full-Mesh)
     路由连接走内网,延迟极低

  2. Super Cluster via Gateway(超级集群/网关)
     ─────────────────────────────────────────
     跨数据中心 / 跨地域
     集群之间通过 Gateway 连接(非 Full-Mesh)
     适合高延迟广域网链路

  3. Leaf Nodes(叶节点)
     ─────────────────────
     边缘计算 / IoT 场景
     单个节点连接到核心集群
     安全隔离,权限可控

8.2 Cluster(同区集群) ​

部署位置:同一可用区 / 同一机房

  ┌──────────────────────────────────────────────────┐
  │                  Data Center A                   │
  │                                                  │
  │   Client-A    Client-B    Client-C    Client-D   │
  │      │           │           │           │       │
  │   Node-1 ◀──▶ Node-2 ◀──▶ Node-3                │
  │   (Full Mesh, 低延迟 <1ms)                       │
  │                                                  │
  └──────────────────────────────────────────────────┘

特点:
  ✓ 延迟极低(节点间 <1ms)
  ✓ 消息路由透明(客户端无感知)
  ✓ Full-Mesh:消息最多 1 跳
  ✗ 不适合跨地域(WAN 延迟会影响 route 稳定性)
  ✗ 无法跨地域独立运行(一个集群只能在一个地域)

适用场景:
  - 单机房高可用
  - 同地域多可用区(AZ 内 RTT <5ms)
  - K8s 单集群部署

8.3 Super Cluster via Gateway(跨区超级集群) ​

部署位置:多个地域 / 数据中心

  ┌──────────────────────┐         ┌──────────────────────┐
  │     Asia Cluster     │         │      EU Cluster       │
  │                      │         │                       │
  │  N1 ◀──▶ N2 ◀──▶ N3 │         │  N4 ◀──▶ N5 ◀──▶ N6 │
  │          │           │         │          │            │
  │     Gateway-Asia     │◀───────▶│     Gateway-EU        │
  │                      │  WAN 链路│                      │
  └──────────────────────┘  ~50ms  └──────────────────────┘
                                            ◀──▶
                                   ┌──────────────────────┐
                                   │      US Cluster       │
                                   │  N7 ◀──▶ N8 ◀──▶ N9 │
                                   │     Gateway-US        │
                                   └──────────────────────┘

Gateway 特性:
  - 采用"按兴趣路由"(只转发有订阅的消息)
  - 一条 Gateway 连接(非 pool 模式)
  - 自动发现:subject 兴趣通过 A+/A- 协议传播

配置示例(Asia 节点的 gateway 块):
  gateway {
    name: "asia"
    listen: "0.0.0.0:7222"
    gateways: [
      { name: "eu",  urls: ["nats://eu-gw1:7222", "nats://eu-gw2:7222"] }
      { name: "us",  urls: ["nats://us-gw1:7222"] }
    ]
  }

特点:
  ✓ 跨地域消息路由(任意节点 pub,任意节点 sub)
  ✓ 地域故障时其他地域正常运行
  ✓ 网络分区容忍(Gateway 断开不影响本地集群)
  ✗ WAN 链路增加延迟(跨区消息需要 50-200ms)
  ✗ 配置相对复杂

适用场景:
  - 全球多地域部署
  - 容灾架构(Active-Active 多地域)
  - 数据主权要求(各地域独立运行)

8.4 Leaf Nodes(叶节点) ​

部署位置:边缘设备 / IoT 网关 / 独立环境

  ┌─────────────────────────────────────┐
  │         Core NATS Cluster           │
  │                                     │
  │    N1 ◀──▶ N2 ◀──▶ N3              │
  └─────────────────────────────────────┘
        ▲              ▲
        │              │  WAN / 专线
        ▼              ▼
  ┌──────────┐    ┌──────────┐
  │ Leaf     │    │ Leaf     │
  │ Node     │    │ Node     │
  │ (工厂A)  │    │ (工厂B)  │
  │          │    │          │
  │ 本地设备  │    │ 本地设备  │
  └──────────┘    └──────────┘

Leaf Node 特性:
  - Leaf 连接到 Core Cluster,作为"延伸"
  - 本地可独立运行(WAN 断开时 Leaf 内部通信正常)
  - Subject 映射:Leaf 本地 subject 可映射到 Core subject
  - 权限隔离:Leaf 只能访问配置允许的 subject

配置示例(Leaf Node 的配置):
  leafnodes {
    remotes: [
      {
        url: "nats://core-cluster-lb:7422"
        credentials: "/etc/nats/leaf.creds"
      }
    ]
  }

特点:
  ✓ 网络断开时本地仍可运行
  ✓ 安全隔离(Leaf 无法访问 Core 未授权的 subject)
  ✓ 轻量:Leaf 只需单节点,无需集群配置
  ✓ 适合资源受限的边缘设备
  ✗ 仅单向连接到 Core(非对等)
  ✗ 跨 Leaf 通信必须经过 Core

适用场景:
  - 工厂/仓库边缘节点
  - IoT 设备汇聚网关
  - 开发环境连接生产 NATS
  - 多租户安全隔离

8.5 三种拓扑对比表 ​

┌────────────────┬──────────────┬────────────────┬──────────────────┐
│   维度         │   Cluster    │ Super Cluster  │   Leaf Nodes     │
├────────────────┼──────────────┼────────────────┼──────────────────┤
│ 适用范围       │ 同区/同机房  │ 跨区/跨地域    │ 边缘/IoT/扩展   │
│ 节点间协议     │ Route        │ Gateway        │ Leaf             │
│ 延迟           │ 极低 <1ms    │ WAN 延迟       │ 取决于链路       │
│ 故障隔离       │ 无           │ 地域级隔离     │ 边缘独立运行     │
│ 配置复杂度     │ 低           │ 中             │ 低               │
│ 消息路由跳数   │ 最多 1 跳    │ Cluster内1跳   │ 经过 Core 转发   │
│                │              │ + 1 Gateway跳  │                  │
│ 订阅传播       │ RS+/RS-      │ A+/A-          │ LS+/LS-          │
│ 安全隔离       │ 无           │ 账户级         │ Leaf 级强隔离    │
│ 网络分区容忍   │ 低(需 Raft) │ 高             │ 极高(本地运行) │
└────────────────┴──────────────┴────────────────┴──────────────────┘

9. 滚动升级时集群的行为 ​

9.1 滚动升级流程 ​

Kubernetes 的 StatefulSet 滚动升级(RollingUpdate)按 Pod 序号倒序依次重启:

3 节点集群滚动升级过程:

  初始状态(所有节点运行旧版本 v2.9):
  Node-0(v2.9) ◀──▶ Node-1(v2.9) ◀──▶ Node-2(v2.9)

  Step 1:升级 Node-2(序号最大的先升级)
  Node-0(v2.9) ◀──▶ Node-1(v2.9)    Node-2(重启中...)
                                           │
                                     ← 约 10-30 秒 →
                                           │
  Node-0(v2.9) ◀──▶ Node-1(v2.9) ◀──▶ Node-2(v2.10)

  Step 2:升级 Node-1
  Node-0(v2.9) ◀──▶ Node-1(重启中...) ◀──▶ Node-2(v2.10)

  Step 3:升级 Node-0
  Node-0(重启中...) ◀──▶ Node-1(v2.10) ◀──▶ Node-2(v2.10)

  完成:
  Node-0(v2.10) ◀──▶ Node-1(v2.10) ◀──▶ Node-2(v2.10)

9.2 节点离线期间的影响 ​

Core NATS(无 JetStream):

Node-2 重启期间(约 15 秒):

  影响:
  - 连接到 Node-2 的客户端:连接断开,需要重连
    → 客户端配置了 nats.MaxReconnects(-1) 时自动重连到其他节点
    → 重连期间消息可能丢失(Core NATS 无持久化)

  不影响:
  - 连接到 Node-0、Node-1 的客户端正常工作
  - Node-2 上没有本地订阅者的 subject 消息正常路由
  - Node-0 ◀──▶ Node-1 之间的消息路由完全正常

  恢复:
  - Node-2 重启后重新建立 Route 连接(8条)
  - 重新同步订阅信息(RS+)
  - 客户端自动重连

JetStream 集群(Raft):

JetStream Raft 在 Node-2 下线期间:

  3 节点 Raft:多数派 = 2
  Node-2 下线后剩余 2 个节点(Node-0 + Node-1)
  2 ≥ 2(多数派),Raft 仍可工作!

  影响:
  - JetStream Stream/Consumer Leader 可能迁移
    → 如果 Node-2 是某个 Stream 的 Raft Leader,会触发重新选举
    → 选举期间(通常 <1 秒)该 Stream 的写操作暂停

  不影响:
  - 其他 Stream(Node-2 不是其 Leader)正常工作
  - 已持久化的消息不丢失
  - Consumer 继续消费

  2节点场景的危险(升级中途故障):
  如果 Node-2 正在重启,此时 Node-1 也崩溃:
  → 只剩 Node-0 一个节点,无法形成多数派
  → JetStream 元数据操作(创建 Stream/Consumer 等)暂停
  → 但 Publish 消息到已存在的 Stream 可能仍可工作(等待 quorum)

9.3 优雅关闭的重要性 ​

NATS 服务器支持 --signal drain 信号,触发优雅关闭流程:

优雅关闭流程(推荐配置 preStop hook):

  K8s 发送 SIGTERM
      │
      ▼
  NATS 收到信号,开始 drain:
  1. 停止接受新客户端连接
  2. 等待现有客户端完成消息处理(最多 terminationGracePeriodSeconds)
  3. 向路由节点发送下线通知
  4. 路由节点更新路由表,不再向此节点发消息
  5. 关闭所有连接
  6. 退出

  K8s StatefulSet 配置建议:
    spec:
      terminationGracePeriodSeconds: 60   # 给足够时间 drain
      containers:
      - lifecycle:
          preStop:
            exec:
              command:
              - nats-server
              - --signal
              - drain

9.4 版本兼容性 ​

NATS 服务端版本兼容规则:

升级过程中,新旧版本节点共存:

  Node-0(v2.9) ◀──▶ Node-2(v2.10)

  NATS 保证:
  ✓ 同一大版本(如 2.x)内的小版本可以混跑
  ✓ 路由协议向后兼容
  ✗ 跨大版本(如 1.x → 2.x)需要所有节点同时升级或有专门迁移文档

  建议:
  - 升级前查阅 NATS Release Notes
  - 先在测试环境验证滚动升级
  - 保持各节点版本差异不超过一个 minor 版本

10. routez 端点详细字段解读 ​

10.1 访问 routez ​

bash
# 查询节点的路由状态
curl http://localhost:8222/routez

# 格式化输出
curl -s http://localhost:8222/routez | python3 -m json.tool

# 只看简要信息
curl -s http://localhost:8222/routez?subs=0

10.2 routez 返回字段全解析 ​

json
{
  "server_id": "NAQGJXPBDCFLWRP7GKTSLJFOO3AG3HFG4K4T2SLXNQNZFOBDZ6DGSM",
  "now": "2024-03-15T10:30:00.123456789Z",
  "num_routes": 8,
  "routes": [
    {
      "rid": 1,
      "remote_id": "NBRV5GQHKXGTLDNJYQUCR2NZXVXHRVR4MLBRPXHVGPV7HWRPKRZ7N",
      "remote_name": "nats-1",
      "did_solicit": true,
      "is_configured": true,
      "ip": "10.244.1.3",
      "port": 6222,
      "start": "2024-03-15T10:00:00.000000000Z",
      "last_activity": "2024-03-15T10:30:00.120000000Z",
      "rtt": "0.312ms",
      "uptime": "30m0s",
      "idle": "80ms",
      "in_msgs": 1234567,
      "out_msgs": 987654,
      "in_bytes": 123456789,
      "out_bytes": 98765432,
      "subscriptions": 145,
      "account": "",
      "compression": "s2_best",
      "auth_timeout": false,
      "tls_version": "",
      "tls_cipher_suite": "",
      "pool_idx": 0
    },
    {
      "rid": 2,
      "remote_id": "NBRV5GQHKXGTLDNJYQUCR2NZXVXHRVR4MLBRPXHVGPV7HWRPKRZ7N",
      "remote_name": "nats-1",
      "did_solicit": true,
      "is_configured": true,
      "ip": "10.244.1.3",
      "port": 6222,
      "rtt": "0.298ms",
      "pool_idx": 1
    }
    // ... 共 8 条(4 到 nats-1,4 到 nats-2)
  ]
}

10.3 各字段详解 ​

num_routes
  当前节点的路由连接总数。
  健康集群:(N-1) × pool_size
  如果 num_routes < 期望值,说明有节点未连接或连接断开。
  示例:3节点集群期望值 = (3-1) × 4 = 8

routes[].rid
  路由连接的本地内部 ID(递增整数)。
  每次连接建立时分配新 ID,重连后 ID 会变化。
  用于区分同一 peer 的多条连接(pool)。

routes[].remote_id
  对端节点的 Server ID(56字节 NKEY 格式)。
  同一 peer 的 pool 中所有连接的 remote_id 相同。
  可用来确认实际连接到了哪个节点。

routes[].remote_name
  对端节点的 server_name 配置值(如 "nats-1")。
  便于人工识别,是 remote_id 的可读形式。

routes[].did_solicit
  true  = 本节点主动发起的连接
  false = 对端节点发起的连接(本节点被动接受)
  在 pool 中,通常一半主动一半被动(双方各发起一部分)。

routes[].is_configured
  true  = 此 route 对应 routes[] 配置中的某一项(静态配置)
  false = 动态发现的路由(通常不出现在基础集群中)

routes[].ip / .port
  对端节点的实际 IP 和端口。
  如果与 DNS 名称不一致,可能说明 DNS 解析有问题。

routes[].rtt
  到对端节点的往返延迟(Round-Trip Time)。
  正常值(同机房):< 1ms
  跨可用区:1-5ms
  异常值(>10ms):可能存在网络拥塞或配置问题。

routes[].uptime
  此连接已建立的时长。
  短暂 uptime(如 <5s)可能说明连接频繁重建(网络不稳定)。

routes[].idle
  此连接上次传输数据到现在的时间。
  长时间 idle(>30s)但节点正常是正常的(无消息时 PING 维持连接)。

routes[].in_msgs / .out_msgs
  通过此路由连接收/发的消息总数(累计,从连接建立开始)。
  可用于分析消息流方向和负载分布。

routes[].in_bytes / .out_bytes
  通过此路由连接收/发的字节总数。
  用于评估带宽使用情况。

routes[].subscriptions
  通过此路由连接传播的订阅数量(对端节点告知本节点的兴趣总数)。
  异常低值(=0)可能说明 RS+ 传播失败(客户端收不到跨节点消息)。

routes[].compression
  路由连接是否启用压缩及压缩算法。
  可选值:""(无压缩)、"s2_best"、"s2_fast"、"s2_auto"
  启用压缩可降低带宽,但增加 CPU 开销。

routes[].pool_idx
  此连接在 pool 中的索引(0 ~ pool_size-1)。
  pool_idx=0 表示该连接是 pool 中的第一条。
  用于确认 pool 是否正常建立(应该有 0,1,2,3 四个索引值)。

10.4 routez 诊断案例 ​

案例 1:集群未完全形成

bash
# 期望 8 条路由(3节点,pool_size=4),实际只有 4 条
curl -s http://localhost:8222/routez | jq '.num_routes'
# 输出:4

# 诊断:只连接到了 1 个 peer,另 1 个 peer 未连接
# 排查:
# 1. 检查另一个节点是否启动
# 2. 检查 DNS 解析
kubectl exec nats-0 -- nslookup nats-2.nats.default.svc.cluster.local
# 3. 检查网络策略(NetworkPolicy)
# 4. 查看 NATS 日志中的连接错误
kubectl logs nats-0 | grep -E "route|error|connect"

案例 2:subscriptions 为 0

bash
curl -s http://localhost:8222/routez | jq '.routes[0].subscriptions'
# 输出:0

# 诊断:对端节点没有任何订阅,或 RS+ 传播失败
# 后果:发布到对端订阅者的消息无法路由
# 排查:
# 1. 确认对端节点有客户端订阅
# 2. 检查 NATS 服务端版本(RS+ 在旧版本有 bug)
# 3. 查看路由连接的 uptime(是否刚刚重建)

案例 3:rtt 异常高

bash
curl -s http://localhost:8222/routez | jq '.routes[] | {name: .remote_name, rtt: .rtt}'
# 输出:
# {"name": "nats-1", "rtt": "52.3ms"}  ← 异常!同机房应 <1ms

# 诊断:网络拥塞或跨区路由
# 排查:
# 1. ping 对端节点 IP
# 2. 检查是否发生了 Pod 跨节点调度
kubectl get pods -o wide | grep nats
# 3. 检查是否有大量消息导致拥塞
# 4. 考虑增加 pool_size 减少行头阻塞

10.5 自动化监控脚本 ​

bash
#!/bin/bash
# check_nats_routes.sh:检查 NATS 集群路由健康状态

EXPECTED_ROUTES=8  # (3-1) × 4
MONITOR_URL="http://localhost:8222"

check_node() {
    local node=$1
    local url="${MONITOR_URL}/routez"
    
    result=$(curl -s "$url" 2>/dev/null)
    if [ $? -ne 0 ]; then
        echo "ERROR: 无法连接到 $node 的监控端点"
        return 1
    fi
    
    num_routes=$(echo "$result" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['num_routes'])")
    
    if [ "$num_routes" -lt "$EXPECTED_ROUTES" ]; then
        echo "WARNING: $node 路由连接数不足!期望 $EXPECTED_ROUTES,实际 $num_routes"
    else
        echo "OK: $node 路由连接数 $num_routes(正常)"
    fi
    
    # 检查高 RTT
    echo "$result" | python3 -c "
import json, sys
d = json.load(sys.stdin)
for r in d.get('routes', []):
    rtt_str = r.get('rtt', '0ms').replace('ms', '')
    try:
        rtt = float(rtt_str)
        if rtt > 5.0:
            print(f'WARNING: 到 {r[\"remote_name\"]} 的 RTT 过高: {r[\"rtt\"]}')
    except:
        pass
"
}

for i in 0 1 2; do
    MONITOR_URL="http://nats-$i.nats.default.svc.cluster.local:8222"
    check_node "nats-$i"
done

11. 小结 ​

核心要点回顾 ​

Route Pool:
  - 解决行头阻塞(HOL Blocking)
  - 默认 pool_size=4,生产可调整到 6-8
  - 3节点集群每节点 8 条 Route TCP 连接
  - 公式:num_routes = (N-1) × pool_size

Subject Interest Propagation:
  - RS+ / RS- 协议在节点间同步订阅信息
  - 路由表全内存,发布时 O(1) 查找目标节点
  - Queue Group 通过 weight 实现加权路由
  - 新节点加入时全量同步订阅状态

集群配置关键点:
  - cluster.name 必须完全一致(所有节点)
  - Headless Service DNS 是 K8s 部署必要条件
  - StatefulSet 稳定 DNS 名称支持精确路由配置
  - routes 列表可包含自身,NATS 自动跳过

网络拓扑选择:
  - 同区:Cluster(Full-Mesh,1跳,极低延迟)
  - 跨区:Super Cluster + Gateway(地域隔离)
  - 边缘:Leaf Nodes(独立运行,安全隔离)

滚动升级:
  - Core NATS 升级期间消息可能丢失(无持久化)
  - JetStream 升级期间 Leader 迁移,短暂写暂停
  - 配置优雅关闭(terminationGracePeriodSeconds)
  - 同大版本内小版本可混跑

routez 监控:
  - num_routes 应等于 (N-1) × pool_size
  - subscriptions 为 0 表示无跨节点订阅传播
  - rtt > 5ms(同机房)表示网络异常
  - pool_idx 验证 pool 是否完整建立(0,1,2,3)

参考配置速查 ​

3 节点集群(pool_size=4,JetStream 启用):
  - 每节点 Route 连接数:8
  - 全集群 TCP Route 总数:12(3对 × 4条)
  - JetStream Raft 容忍故障:1 个节点
  - 监控端口:8222/routez
  - 集群通信端口:6222

上一篇:07 - JetStream 持久化消费者详解 下一篇:09 - NATS 认证与授权(TLS + NKeys + JWT)