跳转至

RPC 网络通信

学习目标

学完本章后,学习者应该能够:

  1. 理解 RPC、gRPC、HTTP/2 和 Protobuf 的关系。
  2. 理解连接池、超时、重试、熔断和服务发现。
  3. 能设计服务间调用的基础治理策略。
  4. 能识别重试放大、超时不一致和连接泄漏风险。

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 策略:

  1. 总超时 800ms。
  2. 只允许幂等读请求重试。
  3. 最多重试 1 次。
  4. 下游持续失败时快速失败。
  5. 记录调用耗时和错误类型。
参考答案

服务 A 应从入口请求 context 派生 800ms 的总超时,并把这个 context 传给服务 B。读请求如果错误类型是临时网络错误或明确可重试状态,可以最多重试 1 次,并使用短退避。写请求除非有幂等键,否则不重试。

下游连续失败时可以开启熔断,在短时间内快速失败,避免请求堆积。所有调用都要记录目标服务、方法、耗时、状态码、错误类型和是否重试。

面试题

1. RPC 和本地函数调用最大的区别是什么?

参考答案

RPC 跨网络,可能出现超时、连接失败、服务不可用、部分成功、重复执行和未知结果。本地函数调用通常只在同一进程内执行,错误模型简单得多。

因此 RPC 必须显式处理超时、取消、重试、幂等、序列化和连接管理。

2. 重试为什么可能带来风险?

参考答案

重试会增加下游流量。下游已经慢或故障时,大量重试可能进一步压垮它。多层调用都重试还会造成指数级流量放大。

非幂等操作重试可能造成重复创建、重复扣款、重复发送通知。重试必须限制次数、错误类型、总超时和适用场景。

3. 服务发现解决什么问题?

参考答案

服务发现解决调用方如何找到可用服务实例的问题。它可以通过 DNS、Kubernetes Service、注册中心或服务网格实现。

服务发现通常要配合健康检查、负载均衡、连接池更新和故障摘除,否则调用方可能继续访问不可用实例。