答案是必须明确区分注册、选节点和健康剔除三环节:consul+go-micro/v2可快速落地,前者负责服务注册与健康检查,后者通过registry和selector实现发现与轮询/随机策略;grpc round_robin需配合dns或自定义resolver注入地址列表,且不内置健康剔除,须额外实现。

Go 微服务里做服务发现和负载均衡,不是加个 go get 就能自动生效的事——它必须明确区分「谁管注册」、「谁管选节点」、「谁管健康剔除」,三者缺一不可。硬编码地址或只配个 http.Client 会在线上扩缩容时直接失效。
Consul + go-micro/v2 是最快落地的组合
Consul 提供开箱即用的健康检查、KV 存储和 DNS 接口;go-micro/v2 的 registry 和 selector 模块刚好把服务发现和负载策略封装成可插拔组件。它不依赖 Sidecar,所有逻辑都在 Go 进程内完成,适合中小团队快速验证架构。
-
micro.NewService()初始化时传入consul.NewRegistry(),服务启动后自动注册到localhost:8500 - 客户端调用
service.Client().Call()时,selector默认走轮询(RoundRobin),你也可以显式指定selector.WithStrategy(selector.Random) - Consul Agent 必须可访问,且服务名必须全小写(
user-srv合法,UserSrv会被忽略) -
go-micro/v2的Registry接口不兼容 etcd v3 的 gRPC API;若强行换 etcd,得自己实现适配层
gRPC 原生 round_robin 要求目标是可解析的地址列表
grpc-go 的 round_robin 策略本身不拉注册中心,它只负责从已知地址中选一个。所以你不能传 "10.0.1.5:8080",而必须让目标变成可扩展的解析源。
- 在 Kubernetes 中,配合 Headless Service(
clusterIP: None)+ DNS,让"dns:///user-svc.default.svc.cluster.local"解析出全部 Pod A 记录 - 非 K8s 环境下,需手写
resolver.Builder,监听 etcd/Consul 路径变化,调用cc.UpdateState()注入新地址列表 -
round_robin是连接粒度的:每个grpc.ClientConn内部维护自己的地址池,不会跨连接共享状态 - 没有内置健康剔除——如果某个实例宕机,
round_robin仍可能选中它,需配合Keepalive或自定义拦截器做失败熔断
自己实现客户端 Balancer 要防三个坑
很多团队最终会自己封装 RoundRobinBalancer 或 WeightedRandomBalancer,因为要加权重、平滑上线、灰度路由等定制逻辑。但容易在细节上翻车。
- 计数器必须用
atomic.Int64,不能用普通int+sync.Mutex——高并发下锁竞争会导致吞吐骤降 - 实例列表更新时,必须原子替换整个切片(
b.instances.Store(newList)),不能边遍历边修改底层数组 - 权重策略若用「平滑加权轮询」,不能简单按权重累加取模,否则小权重实例长期得不到调度;得用类似 Nginx 的 virtual node 映射方式
- HTTP 场景下,别每次请求都新建
http.Client;复用带Transport的实例,并设置MaxIdleConnsPerHost防连接耗尽
真正难的从来不是“怎么选节点”,而是“怎么知道该不该选它”。健康检查的周期、超时阈值、失败连续次数,这些参数比算法本身更影响线上稳定性。别在压测后再调——它们得和注册中心心跳间隔对齐,否则会出现“注册中心说活着,但请求已超时”的裂缝。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











