RPC 网络通信¶
学习目标¶
学完本章后,学习者应该能够:
- 理解 RPC、gRPC、HTTP/2 和 Protobuf 的关系。
- 理解连接池、超时、重试、熔断和服务发现。
- 能设计服务间调用的基础治理策略。
- 能识别重试放大、超时不一致和连接泄漏风险。
RPC 解决什么问题¶
RPC 让调用远程服务看起来像调用本地函数,但本质上仍然是网络通信。
网络调用和本地调用不同:
- 可能超时。
- 可能部分成功。
- 可能重复执行。
- 可能返回未知状态。
- 需要序列化和反序列化。
- 需要连接管理。
高级工程师不能被“像本地调用”迷惑,必须始终记得它跨网络。
gRPC、HTTP/2 与 Protobuf¶
gRPC 常见组合:
- HTTP/2:传输层上的应用协议能力,多路复用。
- Protobuf:接口定义和序列化格式。
- gRPC:RPC 框架,处理方法调用、状态码、metadata、stream 等。
gRPC 适合内部服务通信,但浏览器直接调用需要额外支持或网关。
超时、重试与熔断¶
服务间调用必须设置超时。没有超时的 RPC 可能无限等待。
重试要谨慎:
- 只对幂等操作重试。
- 设置最大次数和退避。
- 不要让调用链层层重试造成流量放大。
- 超时预算要从入口统一向下传递。
熔断用于在下游持续失败时快速失败,避免拖垮上游。
服务发现¶
服务发现解决“我要调用哪个实例”的问题。
常见方式:
- DNS。
- Kubernetes Service。
- Consul / etcd / Nacos。
- xDS / 服务网格。
服务发现要配合健康检查、负载均衡和连接更新。
Go 后端实际应用例子¶
例子一:用 context 控制 RPC 超时¶
ctx, cancel := context.WithTimeout(parentCtx, 500*time.Millisecond)
defer cancel()
resp, err := userClient.GetUser(ctx, &pb.GetUserRequest{Id: userID})
if err != nil {
return nil, err
}
不要让下游调用脱离上游请求生命周期,否则客户端已经断开,下游调用还在继续消耗资源。
例子二:重试只用于幂等请求¶
func Retry(ctx context.Context, attempts int, fn func(context.Context) error) error {
var last error
for i := 0; i < attempts; i++ {
if err := fn(ctx); err != nil {
last = err
time.Sleep(time.Duration(i+1) * 50 * time.Millisecond)
continue
}
return nil
}
return last
}
这只是示意。生产重试要考虑错误类型、退避、抖动、幂等性和总超时。
常见误区¶
- 误区一:RPC 像本地函数,所以不用处理网络错误。
RPC 必须处理超时、取消、连接断开、服务不可用和未知结果。
- 误区二:失败就重试一定更可靠。
重试可能放大流量,压垮下游。非幂等操作重试还可能造成重复扣款、重复创建。
- 误区三:每次调用都新建连接更干净。
高频调用应复用连接。频繁建连会增加 TCP、TLS、认证和端口成本。
线上问题案例¶
某链路入口服务重试 3 次,中间服务也重试 3 次,底层数据库慢时,单个请求最多放大成 9 次下游调用,导致故障进一步扩大。
修复方式是统一超时预算,限制重试层级,只在边界层或明确幂等场景重试,并加入熔断和限流。
实战任务¶
设计一个服务 A 调用服务 B 的 RPC 策略:
- 总超时 800ms。
- 只允许幂等读请求重试。
- 最多重试 1 次。
- 下游持续失败时快速失败。
- 记录调用耗时和错误类型。
参考答案
服务 A 应从入口请求 context 派生 800ms 的总超时,并把这个 context 传给服务 B。读请求如果错误类型是临时网络错误或明确可重试状态,可以最多重试 1 次,并使用短退避。写请求除非有幂等键,否则不重试。
下游连续失败时可以开启熔断,在短时间内快速失败,避免请求堆积。所有调用都要记录目标服务、方法、耗时、状态码、错误类型和是否重试。
面试题¶
1. RPC 和本地函数调用最大的区别是什么?¶
参考答案
RPC 跨网络,可能出现超时、连接失败、服务不可用、部分成功、重复执行和未知结果。本地函数调用通常只在同一进程内执行,错误模型简单得多。
因此 RPC 必须显式处理超时、取消、重试、幂等、序列化和连接管理。
2. 重试为什么可能带来风险?¶
参考答案
重试会增加下游流量。下游已经慢或故障时,大量重试可能进一步压垮它。多层调用都重试还会造成指数级流量放大。
非幂等操作重试可能造成重复创建、重复扣款、重复发送通知。重试必须限制次数、错误类型、总超时和适用场景。
3. 服务发现解决什么问题?¶
参考答案
服务发现解决调用方如何找到可用服务实例的问题。它可以通过 DNS、Kubernetes Service、注册中心或服务网格实现。
服务发现通常要配合健康检查、负载均衡、连接池更新和故障摘除,否则调用方可能继续访问不可用实例。