一句话总结
服务发现解决"我要调用的服务在哪里"的问题,负载均衡解决"同一个服务有多个实例时选哪个"的问题.
在 K8s 时代,这两件事大部分由平台自动完成--但理解底层机制能帮你做出正确的架构决策.
从一个问题出发
在单体时代,所有逻辑在一个进程内,调用另一个模块就是函数调用.拆成微服务后:
- 订单服务要调用用户服务--用户服务跑在哪台机器,哪个端口?
- 用户服务有 3 个副本--调哪一个?
- 其中一个副本刚发布新版本正在重启--怎么避免把请求发过去?
这就是服务发现和负载均衡要解决的核心问题.
服务发现的两层模型
在 Kubernetes 04 篇中我们讨论过--服务发现有两个层次:
| 层次 | 职责 | K8s 实现 | 应用层实现 |
|---|---|---|---|
| 名称解析 | 服务名 → IP 地址 | CoreDNS(svc.namespace.svc.cluster.local) |
Consul / Nacos / etcd |
| 负载均衡 | 一个 IP → 多个后端 Pod | kube-proxy(iptables/IPVS) | 客户端 LB(gRPC resolver) |
K8s 原生 vs 应用层注册中心
如果你的服务全部跑在 K8s 里,大多数场景不需要额外的注册中心--K8s Service + CoreDNS 已经提供了服务发现.
只有当你需要跨集群,跨云,非 K8s 环境混合部署,或需要更细粒度的流量控制时,才考虑 Consul / Nacos 等应用层方案.
K8s 原生服务发现
ClusterIP Service 回顾
K8s 04 篇的核心知识--这里只做关联总结:
- 创建 Service 得到一个虚拟 IP(ClusterIP)
- CoreDNS 将
order-service.default.svc.cluster.local解析为该 ClusterIP - kube-proxy 在每个节点维护 iptables/IPVS 规则,将 ClusterIP 流量转发到后端 Pod
- 当 Pod 挂了 / 未就绪时,自动从 Endpoints 移除
对于大多数 HTTP/REST 服务,这就够了--ClusterIP 做 L4 负载均衡,每个连接随机分配到一个 Pod.
gRPC 长连接的负载均衡问题
K8s 04 篇和 02 篇都提到过--gRPC 使用 HTTP/2 长连接,ClusterIP 的 L4 负载均衡失效:
|
Headless Service + 客户端负载均衡
|
Headless Service 的 DNS 查询直接返回所有 Pod IP(A 记录),而非 ClusterIP.客户端可以自己决定连接哪个 Pod.
|
DNS 缓存陷阱
DNS 有 TTL 缓存.Pod 扩缩容后,客户端可能还在用旧的 IP 列表.gRPC 的 dns resolver 默认 30s 刷新一次.
如果你的服务频繁扩缩容(如 HPA),考虑缩短 TTL 或使用更实时的发现机制(如 xDS / Consul watch).
应用层注册中心:Consul
当 K8s Service 不够用时(跨集群,非 K8s 环境,细粒度健康检查),需要应用层注册中心.Consul 是最流行的选择之一.
注册/发现模型
|
Consul vs K8s Service
| 维度 | K8s Service | Consul |
|---|---|---|
| 范围 | 单集群内 | 跨集群,跨数据中心 |
| 健康检查 | kubelet 探针(pod 级别) | 多种方式(HTTP/gRPC/TCP/脚本) |
| 元数据 | Labels/Annotations | Tags + KV + Service Meta |
| 额外能力 | 无 | KV 存储,ACL,Service Mesh(Connect) |
| 运维成本 | K8s 自带,零成本 | 需要独立部署 Consul 集群 |
Nacos:注册中心 + 配置中心一体
国内团队常用 Nacos(阿里开源),同时提供服务发现和配置管理:
| 能力 | Consul | Nacos |
|---|---|---|
| 服务注册发现 | 支持 | 支持 |
| 配置管理 | KV 存储(基础) | 完整配置中心(推送,灰度,回滚) |
| 一致性模型 | Raft(CP) | AP 模式(临时实例)/ CP 模式(持久实例) |
| 生态 | HashiCorp 全家桶 | Spring Cloud Alibaba / Dubbo |
Go 生态中的选择
如果你用 Go + K8s,通常不需要 Nacos(它更偏 Java 生态).Go 微服务框架(go-zero / Kratos)多数直接用 K8s Service 或 etcd/Consul 做发现.
选择注册中心时优先看你的基础设施:K8s 里就用 K8s 原生,非 K8s 环境看团队技术栈.
负载均衡策略
服务端 vs 客户端负载均衡
| 服务端 LB | 客户端 LB | |
|---|---|---|
| 代表 | K8s Service / Nginx / Envoy | gRPC 内置 / go-zero balancer |
| 工作位置 | 中间代理节点 | 调用方进程内 |
| 优势 | 客户端无感知,集中管理 | 无额外网络跳数,可定制策略 |
| 劣势 | 额外延迟(多一跳),单点风险 | 客户端复杂度高,需要发现机制 |
| 适用 | HTTP / 短连接 | gRPC / 长连接 |
常见策略
| 策略 | 原理 | 适用场景 |
|---|---|---|
| Round Robin | 轮询 | 实例规格一致时的默认选择 |
| Weighted Round Robin | 按权重轮询 | 实例规格不同(新旧机器混合) |
| Least Connections | 选择当前连接数最少的实例 | 请求处理时间差异大 |
| Random | 随机选择 | 简单场景,大量实例时效果趋近均匀 |
| Consistent Hash | 按 key hash 到固定节点 | 需要会话亲和 / 缓存亲和 |
| P2C(Power of 2 Choices) | 随机选 2 个,选负载低的那个 | go-zero 默认策略,兼顾均匀和性能 |
P2C 是最优的通用策略
P2C(两次随机选择取较优)在数学上被证明接近最优:比纯随机均匀得多,比最小连接数开销小得多(不需要全局状态).go-zero 和 gRPC 的 grpclb 都支持这个策略.
gRPC Name Resolver 与 Balancer
gRPC 的服务发现和负载均衡是插件化架构:
|
Name Resolver 负责"发现地址列表",Balancer 负责"选择哪个地址".两者解耦,可以自由组合:
|
健康检查
负载均衡的前提是知道哪些实例是健康的.健康检查有两种模式:
| 模式 | 工作方式 | 代表 |
|---|---|---|
| 主动探测 | 注册中心/平台定期探测实例 | K8s 探针,Consul 健康检查 |
| 被动观察 | 根据实际请求结果判断实例健康 | Envoy outlier detection,gRPC 健康检查 |
生产环境最佳实践:两者结合.
- 主动探测做粗粒度剔除(实例彻底挂了 → 从列表移除)
- 被动观察做细粒度降权(实例偶尔超时 → 降低权重/临时隔离)
区分 Liveness 和 Readiness
K8s 探针的核心区别:
Liveness 失败 → 重启容器(进程死锁等致命问题).
Readiness 失败 → 从 Endpoints 移除但不重启(启动中/过载/依赖不可用).
搞反了会导致:
- 启动慢的服务不停重启
- 过载的服务被杀而非减流量
与 Service Mesh 的关系(预告)
本篇讨论的服务发现和负载均衡,在 Service Mesh 架构中会被 Sidecar(Envoy)接管:
- 无 Mesh:应用内嵌 resolver + balancer(如 gRPC 内置,go-zero)
- 有 Mesh:Envoy sidecar 统一处理发现和 LB,应用只需连接 localhost
第 08 篇会详细讲解 Service Mesh 如何将这些能力从应用代码中完全剥离.
快速回顾
- K8s 原生发现:CoreDNS + ClusterIP 覆盖大部分场景
- gRPC 长连接需要 Headless Service + 客户端 LB 或 L7 代理
- 应用层注册中心(Consul/Nacos)用于跨集群,非 K8s,细粒度控制
- 客户端 LB适合 gRPC 长连接,服务端 LB适合 HTTP 短连接
- P2C 是最优通用策略(两次随机选择取较优)
- 健康检查:主动探测 + 被动观察结合使用
- Service Mesh 会将发现和 LB 下沉到 Sidecar,应用无需关心
动手练习
- 验证 ClusterIP DNS:在 K8s 集群中创建一个 3 副本的 Deployment + ClusterIP Service,用
nslookup验证 DNS 解析 - 对比 Headless DNS:将上一步改为 Headless Service(clusterIP: None),对比 DNS 返回结果的区别
- gRPC 客户端负载均衡:写一个 gRPC 客户端,使用
dns:///+round_robin策略调用 Headless Service,观察请求是否均匀分布到各 Pod - 验证故障自动感知:杀掉一个 Pod,观察 gRPC 客户端是否自动感知并停止向该 Pod 发送请求