This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
2026-05-17 22:00:24 +08:00

156 lines
6.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [grpc, connection-management, keepalive, load-balancing, dns, service-discovery]
create time: 2026-05-07 16:00
---
# 连接管理与负载均衡
## 概述
在生产环境中,gRPC 连接的管理质量直接影响**稳定性**和**可用性**。本文将 Cover 连接保活策略、负载均衡 Picker、服务发现 Name Resolver 三大主题——这些都是日常开发容易忽略但出问题时就是一大片故障的领域。
> [!question] 先看一个问题
>
> 两个微服务之间建立了 gRPC 连接,之后 30 分钟没有任何请求。此时第一个新的 RPC 请求发起时会发生什么?
如果中间经过了 Nginx、云厂商 LB 或 AWS ALB,很可能连接已经被idle timeout 切断。但两端都还认为连接是活的,于是第一次 `Send()` 报 `use of closed network connection`。这就是**没有配置 Keepalive 的典型故障**。
## Keepalive 策略
gRPC 连接默认不发送 keepalive ping,长时间空闲的连接会被中间代理(如 Nginx、云厂商 LB)切断:
```go
server := grpc.NewServer(
grpc.KeepaliveParams(keepalive.ServerParameters{
Time: 10 * time.Second, // ping 间隔
Timeout: 5 * time.Second, // 超时检测
MaxConnectionAge: 5 * time.Minute, // 最大生命周期(平滑退役)
}),
grpc.KeepaliveEnvelope(keepalive.EnforcementPolicy{
MinTime: 5 * time.Second, // 客户端最小 ping 间隔
PermitWithoutStream: true, // 允许空闲连接保活
}),
)
```
### Keepalive 参数解读
| 参数 | 方向 | 含义 |
|------|------|------|
| `Time` | 服务端 | 多久没收到 ping 就发一个 ping |
| `Timeout` | 服务端 | 发了 ping 后等多久没回复就算对方挂了 |
| `MaxConnectionAge` | 服务端 | 连接最多存活多久,强制关闭让客户端重建 |
| `MinTime` | 服务端 | 限制客户端 ping 频率,防 DoS |
| `PermitWithoutStream` | 服务端 | 即使没有活跃 Stream 也允许发送 ping |
### 连接生命周期
```mermaid
timeline
title 连接生命周期管理
0 min : 建立连接
5 min : 达到 MaxConnectionAge<br/>服务端主动关闭<br/>(旧连接不再收新请求)
5min+ : 客户端创建新连接<br/>完成平滑迁移
```
> [!keypoint] MaxConnectionAge 的作用
>
> 它不是为了保活,而是为了**平滑退役**。当后端实例缩容或升级时,通过 MaxConnectionAge 让旧连接自然到期失效,客户端自动切换到新连接,避免突然断连导致的请求失败。
## 负载均衡 Picker
gRPC 内建了几种负载均衡策略,客户端侧自动分发请求到不同后端实例:
```mermaid
graph LR
LR_WRR["Weighted Round Robin<br/>权重轮询"] --> Picking["Picker.pick()<br/>→ 选择一个 SubConn"]
LR_HRR["Hash-based<br/>粘性会话"] --> Picking
LR_RRS["Random Selection<br/>随机挑选"] --> Picking
LR_PR["Pick First<br/>单一连接"] --> Picking
Picking --> SubConn["SubConn<br/>单个后端实例"]
```
### 策略对比
| 策略 | 行为 | 适用场景 |
|------|------|---------|
| **pick_first**(默认) | 每次只用一个 SubConn,直到 health check 失败才切换 | 只有一个后端、简单场景 |
| **round_robin** | 轮询分发到新连接 | 无状态服务、均匀负载 |
| **weighted_round_robin** | 按权重轮询(Go 1.25+) | 后端规格不一致时使用 |
| **hash-based** | 按 key hash 选定后端 | 需要会话粘性的场景 |
```go
// 客户端指定负载均衡策略
conn, err := grpc.Dial(
"dns:///orderservice:9000",
grpc.WithDefaultServiceConfig(`{"loadBalancingConfig":[{"round_robin":{}}]}`),
)
```
> [!warning] pick_first 的隐患
>
> 默认的 pick_first 策略只使用一个连接。如果这个连接对应的后端实例挂了,gRPC 要等到 health check 失败才会切换。在高可用要求高的场景下,务必切换到 round_robin。
## Name Resolver —— 服务发现桥梁
Name Resolver 是 gRPC 将逻辑服务名解析为物理 IP:Port 的桥梁:
```mermaid
graph LR
Target["目标地址:<br/>dns:///svc:9000"] --> NR["Name Resolver"]
NR --> SD["服务发现后端<br/>consul / kubernetes / etcd"]
SD --> AddrList["[]Resolver.Addresses"]
AddrList --> CC["ClientConn<br/>新建/复用 SubConn"]
style NR fill:#fff3e0
style SD fill:#e3f2fd
```
### Name Resolver 方案
| 方案 | URI 前缀 | 适用场景 |
|------|---------|---------|
| DNS | `dns:///host:port` | 最简单,依赖 DNS 记录 |
| Kubernetes | `k8s://` | K8s Service Discovery |
| Eureka | `eureka:///service-name` | Spring Cloud 生态 |
| File | `file:///path/to/config` | 静态配置文件开发调试 |
### Kubernetes 环境下的零配置方案
```go
// 结合 CoreDNS SRV 记录,无需硬编码任何地址
conn, err := grpc.Dial(
"dns:///my-service.default.svc.cluster.local:9000",
grpc.WithDefaultServiceConfig(`{"loadBalancingConfig":[{"round_robin":{}}]}`),
)
```
Kubernetes 的 DNS 控制器会自动将 Service 的 Endpoints 更新到 DNS 记录,gRPC 客户端只需要监听 DNS 变化即可。
> [!tip] 云原生首选
>
> 在 Kubernetes 环境中,结合 ExternalName 或 CoreDNS SRV 记录可以实现零配置的服务发现。配合 `round_robin` 负载均衡策略,后端扩容缩容时客户端自动感知,无需手动干预。
## 连接管理与可观测性联动
| 维度 | 与可观测性的配合 |
|------|----------------|
| **连接断开** | 通过 Metrics 监控连接创建/销毁频率,异常突增说明后端频繁重启 |
| **负载均衡不均** | 通过 Histogram 看每个后端实例的 QPS 分布,失衡则调整 picker |
| **Keepalive 超时** | 通过日志告警 Detect connection reset,定位中间代理 idle timeout 配置 |
> [!keypoint] 监控建议
>
> 生产环境的 gRPC 客户端和服务端都应该暴露以下指标:
> - `grpc_connection_state_changes_total`:连接状态变更次数
> - `grpc_call_duration_seconds`:单次 RPC 耗时
> - `grpc_server_handled_total`:按 status code 分组的服务端调用计数
## 关联笔记
- [[01-协议与架构]] — Name Resolver 是架构图中连接管理层的第一环
- [[07-最佳实践]] — Keepalive、负载均衡在生产环境的配合配置
- [[02-服务治理/04-服务发现]] — 更深度的服务发现机制对比(Nacos / Consul / K8s)
- [[02-服务治理/06-容错模式]] — 负载均衡 + 熔断的组合效果