2676 字
约 8 分钟
20
S17-X07 · Nacos 的 NameServer:服务发现与推送链路

Nacos 名字里带 "Config",但它的一半职责是服务发现。很多人把两件事混着理解:配置中心靠 ConfigService 拉配置,服务发现靠 NamingService 注册实例。后者走的是一条完全不同的链路——注册表在内存、一致性用 Distro、推送在 2.x 换成了 gRPC 长连接。弄不清这条链路,"服务列表为空但实例在跑"这类问题就只能靠重启碰运气。

1. 角色分工:NameServer 与 Config

模块 职责 客户端 存储
命名服务(naming) 服务注册、发现、健康检查 NamingService 内存注册表 + data/ 下持久化
配置服务(config) 配置发布、订阅、灰度 ConfigService MySQL(外置)或 Derby(内嵌)
一致性协议 naming 用 Distro(AP),config 用 Raft(CP) — —

一个 Nacos 进程里两个模块共用一个端口(默认 8848),但内部是两套数据、两套一致性。配错 namespace 时,通常是 naming 那边注册到了默认 public,而 config 那边读的是另一个空间,两边各错一半。

2. 1.x 与 2.x:推送链路的换代

维度 Nacos 1.x Nacos 2.x
客户端长连接 UDP 推送 + HTTP 心跳 gRPC 双向流(端口 = 主端口 + 1000,即 9848)
心跳 客户端每 5 秒 HTTP PUT 连接级健康检查,无显式心跳包
推送 UDP 包,服务端主动推 服务端沿长连接下发
连接数 每实例每 5 秒一个 HTTP 请求 每客户端一条 gRPC 连接
服务端负载 大量短连接 + UDP 重试 长连接 + 连接数上限
兼容性 — 2.x 服务端需同时开 8848 与 9848(+9849 集群间)

2.x 最大的收益是把"每 5 秒一次 HTTP 心跳"变成"一条长连接",实例上到几万时差别是数量级的。代价是端口必须多开两个:只开 8848,客户端能连上却一直报"注册失败 / 获取不到实例",原因是 gRPC 端口不通。

# 2.x 端口自查:8848 HTTP / 9848 客户端 gRPC / 9849 集群间 gRPC
$ ss -ltnp | grep -E ':8848|:984[89]'
LISTEN 0  4096  *:8848  *:*  users:(("java",pid=1201,fd=98))
LISTEN 0  4096  *:9848  *:*  users:(("java",pid=1201,fd=112))
LISTEN 0  4096  *:9849  *:*  users:(("java",pid=1201,fd=113))

三条都在才算端口齐;只有 8848 时,客户端日志会停在 Client not connected 反复重连。

3. 注册流程:内存 + 磁盘 + 异步通知

NamingService naming = NacosFactory.createNamingService(properties);
naming.registerInstance("order-service",
    new Instance("10.2.3.4", 20880, "prod"));
naming.subscribe("order-service", event -> {
    // event.getInstances() 是本次变化后的全量列表
});

服务端收到注册后按顺序做三件事:

步骤 动作 失败后果
1 写入内存注册表(Service → Cluster → Instance) 内存失败极少,OOM 除外
2 写磁盘(持久实例写文件,临时实例只写内存 + 异步落盘) 服务端重启丢临时实例(预期行为)
3 异步通知订阅者 客户端延迟感知,靠定时拉取兜底

"异步"是关键。通知不是同步调用,而是投递到推送队列,由专门的线程批量推送。所以注册成功的响应不代表订阅者已经收到——这一点决定了客户端的"双保险"设计。

4. 临时实例与持久实例

类型 ephemeral 健康检查 服务端重启后
临时实例 true(默认) 客户端心跳 / 连接存活 消失,需重新注册
持久实例 false 服务端主动探测(TCP/HTTP/MySQL) 保留,磁盘恢复
# 注册一个持久实例
curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance' \
  -d 'serviceName=order-service&ip=10.2.3.4&port=20880&ephemeral=false'

临时实例的语义是"我活着我就心跳",天然适合 K8s 这种随时重建的环境;持久实例适合"物理机上的中间件,我要它即使挂了也留在列表里方便排查"。选错的典型后果:把数据库主机的实例注册成临时实例,DB 重启期间实例被彻底移除,监控里看不到这台目标。

5. 健康检查的两种模式

模式 谁发起 判定 适用
客户端心跳 实例自己 超过 heartbeatTimeout 未收到 → 不健康 临时实例(Java 客户端)
服务端探测 Nacos 服务端 主动连 TCP/HTTP,失败 N 次 → 不健康 持久实例
连接存活(2.x) 服务端 gRPC 连接断开即视为下线 临时实例(2.x 客户端)

2.x 里 Java 客户端不再发心跳,改用连接状态。这意味着:容器网络异常但进程还在时,连接先断、实例先被摘,比心跳模式快得多;反过来,如果客户端与 Nacos 之间的 gRPC 连接被中间设备静默断开而客户端未察觉,注册状态可能与实际不符,直到连接保活失败。

健康≠可用。NamingService 返回的实例可能仍是 healthy=true,但业务侧数据库连不上——健康检查只覆盖到 Nacos 与实例之间这一跳。

6. 推送与拉取的双保险

方向 机制 周期 作用
服务端 → 客户端 长连接推送(2.x)/ UDP(1.x) 事件触发 秒级感知变化
客户端 → 服务端 定时拉取 10 秒(namingLoadCacheAtStart 之外) 补偿丢失的推送
客户端 本地缓存文件 启动时加载 Nacos 不可用时仍能启动

推送丢失在 UDP 时代很常见(包被丢、客户端不在线),所以必须有定时拉取。把推送当成"加速",把拉取当成"正确性保证",设计思考才对。本地缓存是第三层兜底:Nacos 全挂时,客户端至少能用上一次的列表,而不是空列表启动。

$ ls ~/nacos/naming/prod/
DEFAULT_GROUP@@order-service   DEFAULT_GROUP@@user-service
$ head -c 200 '~/nacos/naming/prod/DEFAULT_GROUP@@order-service'
[{"clusterName":"DEFAULT","ip":"10.2.3.4","port":20880,"healthy":true}]

缓存文件名是 group@@serviceName,内容就是上次拿到的实例 JSON。排查"服务列表为空"时,先看这个目录:文件存在但为空,说明问题在服务端;文件都没有,说明客户端压根没连上过。

7. 常用参数

spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.cloud.nacos.discovery.namespace=prod
spring.cloud.nacos.discovery.group=DEFAULT_GROUP
spring.cloud.nacos.discovery.naming-load-cache-at-start=true
spring.cloud.nacos.discovery.failure-tolerance-enabled=true
参数 默认 说明
nacos.naming.clean.interval 30000ms 清理过期实例的周期
nacos.naming.clean.expired.seconds 15000ms(临时实例不健康判定) 超过即标记不健康
nacos.naming.push.batchSize 1000 单批推送数量
nacos.naming.push.udpPushThreadCount 1.x 相关 2.x 已不适用,按版本核对
namingLoadCacheAtStart false 启动时先读本地缓存
failure-tolerance-enabled 视版本 Nacos 不可用时用缓存列表降级

clean.expired.seconds 与 K8s 的 terminationGracePeriodSeconds 不匹配时,会出现"实例已经死了但列表里还有"的窗口。把它压到 15 秒以内、同时保证规则合理,比事后加监控实在。

8. 集群一致性:Distro 是 AP

特性 Distro Raft(config 用)
一致性 最终一致(AP) 强一致(CP)
数据分布 每个节点全量 + 按责任节点分片 全量复制
写路径 写到责任节点,异步同步到其他节点 多数派确认后返回
网络分区 分片内可写,可能短暂不一致 少数派不可写
适用 频繁变化的实例列表 配置数据

Distro 的"责任节点"按实例 IP 的哈希分配,服务端只负责自己的分片,修改后推给其他节点。所以刚注册完立刻在另一个节点上查,可能查不到——这是设计选择,不是 bug。跨节点查询出现短暂不一致时,客户端定时拉取会在一轮内修正。

这也是 Nacos 常被吐槽"服务列表偶尔少几个"的根因:AP 系统默认接受短暂不一致,用最终一致换可用性。

9. 三个高频坑

现象 原因 处理
服务列表为空但实例在跑 namespace 不一致(注册在 public,查的是 prod) 两边都打日志打印 namespace,逐字比对
列表有实例但 group 不对 spring.cloud.nacos.discovery.group 与查询的 group 不一致 group 参与服务唯一键,必须精确匹配
Nacos 连不上时启动失败 未开 namingLoadCacheAtStart 打开缓存加载 + 失败容忍
2.x 客户端连 1.x 服务端 gRPC 端口不存在 升级服务端,或客户端显式降级
只开了 8848 端口 9848/9849 未放通 防火墙与 K8s Service 都要放行

namespace 那条最隐蔽:namespace 在 Nacos 里其实是 namespace ID,不是显示名。控制台新建命名空间时自动生成的 UUID 才是 ID,把"prod"这种名字填进配置,客户端就会新建一个同名空间并注册进去——看起来一切正常,只是查的人不知道要去哪找。

# 查实例,namespace 用 ID
curl -s "http://127.0.0.1:8848/nacos/v1/ns/instance/list?serviceName=order-service&namespaceId=3f1c2d9a-...-e7&groupName=DEFAULT_GROUP" | jq '.hosts | length'

10. 边界:什么情况别指望 Nacos 服务发现

需求 更合适的方案 理由
跨集群、跨云的服务发现 DNS + Service Mesh Nacos 单集群能力有限
需要强一致的服务列表 ZooKeeper / etcd Distro 是最终一致
极高频变动的端点 客户端负载均衡 + 长连接 推送风暴会拖垮服务端
只做配置 ConfigService naming 与 config 是两条链路
需要流量权重与灰度 Nacos 2.x 权重 + 元数据 权重只在选择时生效,不做流量治理

11. 自测

  1. Nacos 1.x 与 2.x 的推送链路各是什么,端口差异在哪?
  2. 临时实例在服务端重启后会怎样,为什么这是预期行为?
  3. 2.x Java 客户端为什么不再发心跳,靠什么判定下线?
  4. 推送和定时拉取分别承担什么职责,缺一个会怎样?
  5. Distro 属于 AP 还是 CP,为什么会"刚注册完在别的节点查不到"?
  6. namespace 配置里该填显示名还是 ID,填错会发生什么?
S17-X07 · Nacos 的 NameServer:服务发现与推送链路
https://www.clxhxhhr.top/posts/5670/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。