服务注册与发现:从 Nacos 到 Go 落地

微服务 A 要调用微服务 B,怎么知道 B 在哪个 IP 哪个端口?——服务发现是微服务的第一块地基。我们用 Nacos 落地了注册发现,这篇文章是完整实践。

一、为什么需要服务发现

没有服务发现的世界:

服 → → 务 服 手 B 务 动 改 扩 A 配 容 置 到 里 写 → 5 死 的 重 个 启 实 I 例 P → , I 全 下 P 失 一 效 个 变 实 了 例 又 挂 了

有服务发现的架构:

服 服 服 务 务 务 B A B 启 调 实 动 用 例 挂 → B 了 向 → → 注 册 问 心 中 注 跳 心 册 超 注 中 时 册 心 ( " → I B P 注 : 在 册 P 哪 中 o " 心 r 摘 t → 除 + 拿 → 到 元 实 A 数 例 据 列 不 ) 表 再 调 → 到 它 负 载 均 衡 选 一 个

二、核心概念

概念 作用
注册 服务启动时上报自己(IP、端口、权重、元数据)
心跳续约 每 5s 发心跳,证明"我还活着"
摘除 心跳超时(默认 15s)→ 从可用列表移除
订阅 调用方监听服务变更(新增/摘除实例)
负载均衡 拿到实例列表后轮询/随机/权重选择

三、Nacos 注册(Go 客户端)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import "github.com/nacos-group/nacos-sdk-go/v2/clients"

// 1. 启动时注册
client, _ := clients.NewNamingClient(vo.NacosClientParam{...})
client.RegisterInstance(vo.RegisterInstanceParam{
    Ip:          localIP,       // 本机 IP
    Port:        8080,          // 服务端口
    ServiceName: "delivery-service",
    Weight:      1,             // 权重(灰度用)
    Metadata:    map[string]string{"version": "v1", "env": "prod"},
    Enable:      true,
    Healthy:     true,
})

// 2. 优雅退出时注销
client.DeregisterInstance(...)

// 3. 调用方:获取实例 + 负载均衡
instances, _ := client.SelectInstances(vo.SelectInstancesParam{
    ServiceName: "delivery-service",
    HealthyOnly: true,
})
target := lb.Pick(instances)   // 轮询/随机/一致性哈希

四、负载均衡策略选择

策略 适用 特点
轮询 同权重实例 最简单
加权轮询 机器规格不同 大规格多权重
一致性哈希 需要粘性会话 同 key 永远同一实例
最小连接 长连接服务 实时性最好

简历服务用加权轮询 + 一致性哈希兜底:普通请求轮询(均匀),带 user_id 的请求哈希(同一用户命中同一实例,本地缓存命中率高)。

五、健康检查:注册 ≠ 可用

注册成功 ≠ 能接流量。常见问题:

服 → 务 注 这 册 时 了 候 , 接 但 流 还 量 没 加 = 载 完 一 配 批 置 / 5 预 0 热 0 缓 存

解法:就绪钩子。服务启动流程改为:

启 动 → N a 初 c 始 o 化 s ( 配 里 置 : / 先 连 接 R 池 e / g 缓 i 存 s 预 t 热 e ) r → I n 标 s 记 t 就 a 绪 n c → e ( 才 E 注 n 册 a b l e = f a l s e ) , 就 绪 后 置 E n a b l e = t r u e
1
2
3
4
// 就绪前注册为 disabled
client.RegisterInstance(paramWithEnable(false))
// 就绪后
client.UpdateInstance(paramWithEnable(true))

六、踩坑记录

坑 1:本地 IP 注册错

多网卡机器注册了错误的 IP(docker0/虚拟网卡),调用方连不上。

解法:显式指定注册 IP(环境变量 SERVICE_IP),别自动探测。

坑 2:实例抖动(注册-注销循环)

优雅退出超时短(比如 3s),流量还在,实例被摘 → 大量 503。

解法:优雅下线流程——先 Enable=false(不再接新流量)→ 等 10s(存量请求处理完)→ 再注销。先摘流量,再杀进程。

坑 3:集群脑裂

Nacos 集群网络分区,两边各自为政,实例列表不一致。

解法:Nacos 集群节点数 ≥ 3,选主机制(Raft),别用单节点。

坑 4:缓存了实例列表不更新

调用方拿到实例列表后本地缓存,服务缩容后还在调已下线实例。

解法:订阅变更事件(Subscribe),或缓存短 TTL(5s),别缓存 1 分钟。

七、故障演练:注册中心挂了会怎样

这是最重要的认知:注册中心挂了,服务不能挂!

正 注 B 恢 常 册 复 : 中 扩 : 服 心 容 注 务 挂 / 册 : 缩 中 A A 容 心 : 重 启 用 变 启 动 本 更 , 时 地 期 重 拉 缓 间 新 一 存 同 次 的 A 步 实 实 例 例 感 列 列 知 表 表 不 继 到 + 续 ( 调 短 订 暂 阅 B 负 变 ( 载 更 B 不 均 还 , 在 但 ) 不 会 断 )

设计原则:注册中心是"索引"不是"依赖"——调用链路不能每请求都打注册中心,本地缓存 + 变更通知才是正确姿势。

总结

服务发现的完整认知:

  1. 注册 + 心跳 + 摘除:实例生命周期的三个环节
  2. 就绪才注册:避免"注册了但没准备好"的假健康
  3. 优雅下线:先摘流量再杀进程,顺序不能反
  4. 本地缓存兜底:注册中心挂了服务不能挂

服务发现解决的是"动态"问题:实例在变、IP 在变、故障在发生。设计目标是"任何变动都不影响调用",而不是"永远不变"。