http通信应优先用http.client配合服务发现和轮询/随机策略;grpc通信必须使用grpc-go内置balancer(如round_robin),因其连接复用、流控、健康探测和metadata透传深度耦合底层连接池,手动代理会破坏streaming、丢失deadline与header且无法实现连接级健康检查。

Go 微服务里该用 grpc-go 还是 http.Client 做负载均衡?
直接结论:HTTP 通信优先用 http.Client 配合服务发现 + 轮询/随机策略;gRPC 通信必须用 grpc-go 内置的 balancer 接口或 round_robin 等内置策略,不能自己封装 HTTP 负载逻辑去“代理” gRPC 流量。
原因很简单:grpc-go 的连接复用、流控、健康探测、metadata 透传都深度耦合在底层连接池里。你用 http.Client 手动转发 gRPC 请求,会破坏 streaming、丢失 deadline、无法传递 Authorization 或自定义 header,而且根本没法做真正的连接级健康检查。
- HTTP 场景(如 RESTful API):用
http.Client+ 自定义RoundTripper实现基于实例列表的轮询,配合定期刷新服务列表(如从 Consul/Etcd 拉取) - gRPC 场景:必须走
grpc.WithBalancerName("round_robin"),并确保后端地址是dns:///service-name或通过resolver.Builder注入动态解析逻辑 - 别在中间层写“gRPC 反向代理”——除非你真需要协议转换,否则纯属给自己埋坑
为什么 net/http 默认 RoundTripper 不支持服务实例轮询?
http.DefaultTransport 和默认 http.Transport 是面向单 endpoint 设计的,它只认一个固定 URL.Host,不会自动拆解服务名、查注册中心、选健康实例。你直接 http.Post("http://user-service/create", ...),DNS 解析一次就缓存到底,根本没机会做负载。
要让它“活起来”,得替换 http.Client.Transport:
立即学习“go语言免费学习笔记(深入)”;
- 写一个自定义
RoundTripper,内部维护一个可更新的实例列表(比如[]string{"10.0.1.10:8080", "10.0.1.11:8080"}) - 每次
RoundTrip时,用原子计数器或sync.Pool+ 随机索引选一个实例,拼出完整 URL(如http://10.0.1.10:8080/create) - 关键点:必须禁用
Transport.MaxIdleConnsPerHost = 0或设为足够大,否则每个实例会被当成独立 host,连接池不复用 - 别忘了加超时和重试逻辑——
http.Client本身不重试非幂等请求(如 POST),得自己 wrap
grpc-go 的 round_robin 为什么有时只打到一个实例?
常见但容易被忽略的原因:你传给 grpc.Dial 的地址不是 DNS 格式,或者 resolver 没生效。例如写成 grpc.Dial("10.0.1.10:9000", ...),gRPC 就当它是单点直连,round_robin 根本不触发。
正确姿势:
- 地址必须带 scheme,且是
dns:///user-service(注意三个斜杠),不是dns://user-service或user-service:9000 - 确保系统 DNS 配置能解析
user-service到多个 A 记录(比如 CoreDNS 返回 2 条),或自己实现resolver.Builder并注册(resolver.Register(...)) - 检查日志是否输出
round_robin: NewSubConn called with [{...} {...}]—— 没这句说明 resolver 没返回任何地址 - 默认
round_robin不做健康检查,挂掉的实例仍会收请求;如需剔除,得配合health check插件或自定义 balancer
服务发现数据延迟导致负载不均,怎么缓解?
Consul/Etcd 返回的服务列表不可能实时,几秒延迟很常见。这时靠客户端“硬轮询”容易雪崩,也浪费资源。
- 用长连接监听变更:比如
consul-api的Watch接口,或 Etcd 的Watchstream,收到变更再更新本地实例缓存 - 加一层本地缓存 TTL:即使发现接口卡住,也能 fallback 到最近一次有效列表,避免全量不可用
- 对新实例做“预热”:首次加入列表时,先发几个探针请求(如
/health),确认可用再纳入负载池 - 别让所有客户端同一时刻刷新——加随机 jitter(比如 5s ± 2s),防止注册中心被打爆
最麻烦的其实是健康状态同步:一个实例网络抖动但进程没死,注册中心可能还把它标为 healthy。这时候得靠客户端主动探测 + 熔断(比如用 gobreaker 包),而不是只信服务发现。










