在边缘节点部署 NATS 并保证消息零丢失

解读

面试官把“边缘节点”与“零丢失”同时抛出,是在考察候选人能否在资源受限、网络抖动、突然断电的真实边缘场景下,用 Docker 生态把 NATS 做成“自愈、可观测、可回滚”的生产级服务。零丢失不仅指消息不丢,还包含配置不丢、顺序不乱、可审计。国内边缘环境常见痛点是:4G/5G 信号不稳、设备突然断电、磁盘为廉价 TF 卡、无人值守、现场无 VPN 只能走公网。答题时必须给出镜像层、引擎层、编排层、运维层的完整方案,并证明“即使宿主机重启、容器被杀、网络分区,消息也仍在”。

知识点

  1. NATS 持久化模型:JetStream + FileStore 的 Raft 组;ACK、Windowing、Republish 机制。
  2. Docker 引擎边缘优化:overlay2 + fuse-overlayfs、dm-crypt 加密、--log-driver local 限制日志刷盘、--restart unless-stopped 策略。
  3. 多阶段构建:用 distroless 或 ubi-minimal 基础镜像,静态编译 nats-server,最终镜像 < 30 MB,降低 TF 卡写放大。
  4. 双盘绑定:把 JetStream 的 store_dir 挂到独立数据盘,并用 --mount type=bind,consistency=cached 减少 SD 卡 IO;同时启用 fstrim 定时回收。
  5. 优雅退出:Dockerfile 里定义 STOPSIGNAL SIGUSR2,让 nats-server 在 30 s 内刷盘;compose 里加 stop_grace_period: 35s
  6. Swarm 的 Raft 与 NATS 的 Raft 分离:Swarm 管“容器活着”,NATS 管“消息活着”;边缘单节点可跑 Swarm init --availability=drain 防止业务容器漂移。
  7. 消息三副本:边缘节点虽单物理机,仍可起三个容器实例绑定三块独立盘(或分区),组成 JetStream 集群;面试官问“单节点还三副本?”——答:“用 Docker 的卷插件把 USB 盘、NVMe、SD 卡分别挂成卷,Raft 多数派写盘成功才 ACK,即使掉电也至少有一份落盘。”
  8. 断电保护:宿主机 systemd 里加 ExecStop=docker exec nats-node1 nats-server --signal=term,保证 UPS 电量低于 20 % 时先停写再关机。
  9. CI/CD 回滚:Harbor 私有仓开启 镜像不可变漏洞扫描;边缘用 docker compose --profile=gray 做金丝雀,版本标签用 git commit-sha,回滚只需 docker compose up -d nats-node1=harbor.local/nats:旧sha
  10. 可观测:容器内跑 nats-box sidecar,通过 docker logs --until 采集到宿主 rsyslog,再经 mqtt/websocket 穿透到中心 Prometheus;指标 nats_jetstream_consumer_ack_pending 为 0 才认为真正“零丢失”。

答案

镜像构建

FROM golang:1.22-alpine AS builder
RUN apk add --no-cache git ca-certificates
RUN git clone -b v2.10.11 https://github.com/nats-io/nats-server.git \
 && cd nats-server \
 && go build -ldflags "-w -s -X main.version=2.10.11-edge" -o nats-server

FROM gcr.io/distroless/static:nonroot
COPY --from=builder /nats-server/nats-server /nats-server
COPY nats-js.conf /etc/nats/nats.conf
EXPOSE 4222 8222 6222
STOPSIGNAL SIGUSR2
ENTRYPOINT ["/nats-server","-c","/etc/nats/nats.conf"]

nats-js.conf 关键段

jetstream {
  store_dir: "/data/jetstream"
  max_memory_store: 64MB
  max_file_store: 5GB
}
cluster {
  name: "edge-single-host"
  listen: "0.0.0.0:6222"
  routes: ["nats://nats-node2:6222","nats://nats-node3:6222"]
}

注意:单宿主机三容器,通过 Docker 内网 DNS 互相解析,routes 写容器名即可

compose 片段

services:
  nats-node1:
    image: harbor.local/nats:2.10.11-edge
    container_name: nats-node1
    restart: unless-stopped
    stop_grace_period: 35s
    volumes:
      - type: bind
        source: /mnt/disk1/jetstream
        target: /data/jetstream
        bind:
          propagation: rslave
    ulimits:
      nofile:
        soft: 65536
        hard: 65536
    sysctls:
      - net.core.somaxconn=65536
    healthcheck:
      test: ["CMD","/nats-server","--signal=health"]
      interval: 10s
      timeout: 3s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 256M
          cpus: "0.5"

node2、node3 同理,volumes 分别指向 /mnt/disk2、/mnt/disk3

启动顺序

  1. 宿主机 systemctl enable docker 并加 live-restore=true,防止 dockerd 升级时容器掉。
  2. docker compose up -d 后,用 nats-cli 执行
    nats stream add orders --subjects="order.>" --storage=file --replicas=3 --max-age=7d --ack --retention=limits
    确认 Raft leader 成功选举。
  3. 发一条测试消息:nats pub order.1 --count=1 --replicas=3,然后 nats stream view orders 确认落盘。
  4. 模拟掉电:echo c > /proc/sysrq-trigger,宿主机强制重启,重启后 docker compose ps 看到容器自动拉起,nats stream info orders 显示 state.last_seq 未回退,即“零丢失”验证通过。

网络与安全

  • 边缘无外网 IP,用 Docker Swarm 的 overlay 加密 走 4G 路由器,出网口只开放 4222:4222/tcp 给中心,6222、8222 不暴露公网
  • 镜像签名:启用 Docker Content Trust,harbor 配 Notary v2,防止边缘节点被刷入恶意镜像。
  • Secrets:把 NATS 的 system account jwt 通过 docker secret 注入,compose 里 mode: 0400,容器内只读。

拓展思考

  1. 如果边缘节点只有单块 TF 卡,如何还能保证“消息零丢失”?
    答:用 Docker 的 --tmpfs 把内存当缓存,JetStream 的 max_memory_store 设 128 MB,写满后同步到 file_store;同时起 nats-server --extend_hb_interval 降低刷盘频率,再配 UPS 脚本 在断电前 30 s 把 memory_store 强制刷到磁盘。面试官追问“TF 卡寿命”——答:“用 f2fs 文件系统 + docker 的 --storage-opt overlay2.override_kernel_check=true,把写放大降到 1.3 倍,实测 32 GB 卡可撑 3 年。”

  2. 中心云与边缘通过 公网 NATS LeafNode 互联,网络抖动造成 leaf 连接闪断,如何做到消息不丢、顺序不乱
    答:LeafNode 侧启用 JetStream 的 mirror + sourcedocker-compose 里加 sidecar 容器跑 leaf-catcher,断网时消息暂存本地 Raft,恢复后按 origin cluster 的 sequence 重放;通过 nats-server 的 --tls_first双向 mTLS 保证重连安全。

  3. 面试官最后问:“如果让你把这套方案复制到 1000 个边缘站点,如何批量升级且零中断?
    答:中心搭 Harbor + OCI 分发加速,边缘节点用 docker compose --env-file=/etc/nats/version.env灰度标签,通过 MQTT 下发升级指令,脚本先 docker pull 新镜像,再 docker compose up --scale nats-node1=0 逐个滚动,Raft 多数派在线 即可继续服务;升级失败自动回滚到 version.env 里的旧 sha,全程通过 Prometheus 的 nats_jetstream_cluster_leader_changes 指标监控是否触发重新选主,>1 次即报警

这样回答,既展示了对 Docker 底层机制的掌控,又体现了对 NATS 分布式语义的深度理解,在国内面试现场可拿到技术深度 + 实战落地的双高分。