156 lines
6.4 KiB
Markdown
156 lines
6.4 KiB
Markdown
|
|
---
|
|||
|
|
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-容错模式]] — 负载均衡 + 熔断的组合效果
|