Skip to content

gRPC从零到生产级:HTTP/2、Protobuf、流控、Deadline与负载均衡

gRPC是基于接口定义语言、代码生成和HTTP/2传输的RPC框架。它把“调用远程服务”包装成强类型Stub,但本质仍是跨网络请求:连接可能失败、响应可能丢失、超时不等于未执行、流量可能因长连接而倾斜。

本页从.proto契约一直追到HTTP/2帧、服务端线程、业务提交和响应返回,并覆盖四种调用模式、流控、Deadline、取消、重试、负载均衡、TLS、健康检查和生产Runbook。

本页负责从零建立协议与生产心智模型。生成代码、ManagedChannelImpl、Resolver、Picker、DelayedTransport、Subchannel、ClientCallImplRetriableStream、Netty Transport、ServerImpl和Boot 3可运行工程,请继续学习gRPC Java内部原理与生产治理;面试复习单独进入gRPC独立面试题

一、学习目标

  1. 区分gRPC、HTTP/2和Protobuf的职责。
  2. 解释Channel、Subchannel、Call、Stream、Stub和Server Handler。
  3. 说清一次Unary调用完整链路。
  4. 区分Unary、服务端流、客户端流和双向流。
  5. 解释HTTP/2多路复用、Stream ID、帧和头部压缩。
  6. 区分HTTP/2流控与业务背压。
  7. 设计Protobuf字段兼容、oneof和演进规则。
  8. 传播Deadline与取消,并说明超时为何仍需幂等。
  9. 解释DNS/NameResolver、LoadBalancer和长连接倾斜。
  10. 排查UNAVAILABLE、DEADLINE_EXCEEDED、RESOURCE_EXHAUSTED和流卡死。

二、三层职责不要混淆

层次负责什么不负责什么
gRPCStub、调用模式、状态码、拦截器、Deadline、LB等业务事务和幂等
HTTP/2连接、Stream、多路复用、Frame、流控、HPACKProtobuf字段兼容
ProtobufSchema、字段编号、二进制编解码、代码生成网络重试和服务发现

gRPC默认常使用Protobuf,但协议理论上可替换Marshaller;HTTP/2承载的不只是“一个请求一个连接”,而是一条连接上的多个逻辑Stream。

三、核心对象

对象含义
ManagedChannel客户端逻辑通信入口,管理解析、LB、连接和状态
NameResolver把目标名称解析为地址和服务配置
LoadBalancer根据地址、连接状态和策略选择Subchannel
Subchannel指向一个或一组后端地址的连接管理对象
Stub.proto生成的类型安全调用入口
ClientCall一次逻辑RPC调用,可能包含多次尝试
HTTP/2 Stream一次尝试在连接上的双向逻辑流
ServerCall服务端框架看到的RPC调用上下文
Service Impl开发者实现的业务方法

Channel不是一条永不变化的TCP连接,也不应每次请求创建。它通常长期存在,内部可管理多个Subchannel和连接。Stub通常轻量且线程安全语义应按生成实现确认,可从Channel派生不同Deadline或凭据配置。

四、从.proto定义契约

protobuf
syntax = "proto3";

package inventory.v1;
option java_multiple_files = true;
option java_package = "com.example.inventory.v1";

service InventoryService {
  rpc Reserve(ReserveRequest) returns (ReserveReply);
  rpc WatchStock(WatchStockRequest) returns (stream StockChanged);
  rpc BatchReport(stream StockChanged) returns (BatchReply);
  rpc Sync(stream StockChanged) returns (stream StockChanged);
}

message ReserveRequest {
  string request_id = 1;
  string sku = 2;
  int32 quantity = 3;
}

message ReserveReply {
  string reservation_id = 1;
  ReserveStatus status = 2;
}

enum ReserveStatus {
  RESERVE_STATUS_UNSPECIFIED = 0;
  RESERVED = 1;
  REJECTED = 2;
}

字段编号是线上的身份,不是展示顺序。sku = 2发布后不能把编号2改给quantity。字段改名通常不影响二进制编号,但会影响JSON映射和代码API。

五、四种调用模式

模式请求响应商业场景
Unary一个一个查询库存、创建预留
Server Streaming一个多个订阅状态、持续下载
Client Streaming多个一个批量上报、分块上传
Bidirectional Streaming多个多个双向采集、实时协同

流式RPC不是MQ。它通常依赖在线连接和双方内存状态;连接断开后如何续传、去重和恢复需要应用协议设计。需要离线堆积和持久消费时应使用MQ或持久日志。

六、一次Unary调用完整过程

mermaid
flowchart TD
    A["业务线程调用Blocking或Async Stub"] --> B["客户端拦截器加入身份与追踪元数据"]
    B --> C["NameResolver与LoadBalancer选择Subchannel"]
    C --> D["取得已有HTTP/2连接或建立TLS连接"]
    D --> E["创建Stream并发送Headers与Message Frame"]
    E --> F["服务端解析Headers和Protobuf消息"]
    F --> G["服务端拦截器认证、授权和追踪"]
    G --> H["业务方法执行本地事务"]
    H --> I["序列化响应并发送Data与Trailers"]
    I --> J["客户端把Status和消息交给调用线程"]

gRPC状态通常在HTTP/2 Trailers中完成表达。HTTP/2连接成功不等于RPC成功;收到响应消息但Trailers表明非OK时,也不能只取消息忽略最终Status。

6.1 Blocking、Future与Async Stub

  • Blocking Stub让当前线程等待结果,使用简单,但不能在事件循环线程中阻塞。
  • Future Stub返回可组合Future,适合Unary异步调用。
  • Async Stub通过观察者接收消息,支持流式调用。

客户端线程、gRPC网络事件循环和业务执行器是不同资源。服务端若把慢数据库查询直接跑在网络事件循环,会阻塞同线程处理的其他Stream。

七、HTTP/2连接、Stream和Frame

mermaid
flowchart TD
    A["一条TCP加TLS连接"] --> B["HTTP/2 Connection"]
    B --> C["多个RPC使用不同Stream ID"]
    C --> D["各Stream的Frame交错发送"]
    D --> E["接收端按Stream ID分别重组"]

7.1 多路复用解决什么

多个Stream共享一条连接,避免HTTP/1.1有限并发连接和应用层队头阻塞。不同Stream的Frame可交错,接收方按Stream ID重组。

但底层仍是TCP:一个丢包可能阻塞后续TCP字节到达,影响同连接上的多个Stream,这叫传输层队头阻塞。HTTP/2没有消除TCP层问题。

7.2 常见Frame

  • HEADERS:方法、路径、Content-Type、元数据和状态相关头。
  • DATA:承载gRPC消息帧字节。
  • SETTINGS:协商并发流、初始窗口等连接参数。
  • WINDOW_UPDATE:补充连接或Stream流控窗口。
  • RST_STREAM:取消或异常终止单个Stream。
  • PING:连接活性和往返时间。
  • GOAWAY:通知不再接受更大Stream ID的新流,客户端应迁移新调用。

gRPC消息在DATA中还有自己的消息边界,通常包含压缩标记和消息长度,因此一个Protobuf消息可能跨多个HTTP/2 DATA Frame,一个DATA Frame也不能简单等同于一个业务消息。

八、流控、背压和isReady

HTTP/2有连接级和Stream级接收窗口。接收方消费数据后发送WINDOW_UPDATE,发送方才能继续。它防止一个快速发送者无限占满对方网络缓冲,但不等于业务队列永不OOM。

mermaid
flowchart TD
    A["发送方准备大量消息"] --> B["检查窗口与isReady"]
    B --> C["可写时只发送有限批次"]
    C --> D["不可写时暂停生产"]
    D --> E["接收方消费并更新窗口"]
    E --> F["onReady后继续下一批"]

异步流式Java调用中,应关注ClientCallStreamObserverServerCallStreamObserverisReady()和onReady回调,按可写状态生产消息。若业务线程无视Ready高速调用onNext,库可能排队大量待发送消息,造成堆内存增长。

应用背压还需要:

  • 有界业务队列。
  • 单连接最大在途消息数。
  • 消费速率指标和最老消息年龄。
  • 超限时暂停上游、拒绝或落盘,而不是无限缓存。
  • 取消时停止生产任务,避免RPC结束后后台仍计算。

九、Protobuf兼容演进

9.1 必须遵守

  1. 已发布字段编号不能复用。
  2. 删除字段后用reserved保留编号和名称。
  3. 新增字段时旧客户端会忽略未知字段;新服务端要接受旧客户端缺省值。
  4. 数值类型互换要确认wire type和语义,不能只看Java可转换。
  5. Enum零值必须表达UNKNOWN/UNSPECIFIED,兼容未识别值。
  6. 金额不要用double;使用最小货币单位整数或明确decimal消息。
  7. 时间统一使用标准Timestamp或明确时区和精度。
protobuf
message ReserveRequest {
  reserved 4, 5;
  reserved "legacy_user_id";
  string request_id = 1;
  string sku = 2;
  int32 quantity = 3;
  string tenant_id = 6;
}

9.2 缺省值与字段存在性

Proto3标量缺省值可能让“未传数量”和“明确传0”难以区分。需要存在性语义时使用支持presence的字段方式、optional或包装/oneof,并结合目标Protobuf版本验证生成代码。

9.3 oneof

oneof表达多个字段同一时刻只应设置一个,适合支付方式等互斥结构。演进时仍要考虑旧客户端遇到新分支后的行为,不能把未知分支默认为某个危险操作。

十、Deadline、取消与结果未知

Deadline表达调用最晚允许完成的时间点;跨进程传播时框架通常按剩余时长处理,减少机器时钟偏差影响。服务端应周期检查取消状态,停止无意义计算。

mermaid
flowchart TD
    A["上游设置总Deadline 800ms"] --> B["排队与网络已消耗120ms"]
    B --> C["下游只获得约680ms剩余预算"]
    C --> D["检查是否在剩余预算内完成"]
    D --> E["成功返回OK;超时返回Deadline错误"]
    E --> F["写操作超时后按request_id查询事实"]

DEADLINE_EXCEEDED只表示调用方未按时取得成功结果,不证明服务端没有执行。数据库提交后响应丢失时,客户端仍会超时。因此写接口必须包含稳定request_id,服务端在本地事务中以唯一键记录结果,重试复用第一次结果。

取消也是协作式的:客户端取消会发送RST_STREAM等信号,但服务端业务代码、数据库驱动或外部HTTP调用未必立即停止。业务代码要检查Context取消并给下游设置更短剩余超时。

十一、状态码怎样使用

Status常见含义是否重试
INVALID_ARGUMENT参数不合法修正参数,不重试
NOT_FOUND资源不存在通常不重试
ALREADY_EXISTS唯一资源已存在查询原结果或按幂等处理
FAILED_PRECONDITION当前状态不允许状态变化后才可能重试
ABORTED并发冲突或事务中止可在幂等和退避下重试
RESOURCE_EXHAUSTED配额、消息或资源超限降速、缩小请求,避免立即风暴
UNAVAILABLE服务暂时不可用或连接失败幂等且有预算时重试
DEADLINE_EXCEEDED超过Deadline结果可能未知,先查事实
INTERNAL服务内部不变量错误通常先修复,不盲目重试
UNKNOWN未正确映射的未知错误先定位,不能默认安全重试

不要把所有异常都映射成UNKNOWN,也不要把业务“库存不足”伪装成UNAVAILABLE诱导重试。稳定业务拒绝可用明确状态加结构化错误详情,敏感堆栈不要返回客户端。

十二、服务发现与负载均衡

mermaid
flowchart TD
    A["Target例如dns:///inventory"] --> B["NameResolver生成地址快照"]
    B --> C["LoadBalancer维护Subchannel"]
    C --> D["Picker选择READY Subchannel"]
    D --> E["按pick_first或round_robin策略"]
    E --> F["RPC创建HTTP/2 Stream"]

12.1 为什么只访问ClusterIP会倾斜

若NameResolver只得到一个Kubernetes ClusterIP,gRPC可能建立少量到VIP的HTTP/2连接;每条连接由Kubernetes数据面固定到一个Pod,大量RPC多路复用后只打到少数Pod。扩容不会迁移旧连接。

解决方向:Headless Service返回Pod地址、gRPC自定义Resolver、xDS/Service Mesh提供Endpoint、增加受控Subchannel和连接生命周期。详见Kubernetes连接级负载

12.2 Wait-for-Ready

默认Fail-fast调用在Channel暂不可用时可能立即失败;Wait-for-Ready允许调用在Deadline内等待Channel恢复。它不是无限排队开关:没有Deadline会积压调用并占内存,写请求仍需幂等。

十三、重试与Hedging

gRPC重试可由服务配置指定可重试状态、最大尝试和退避。一次逻辑RPC可能包含多个Transport Stream尝试。透明重试和显式策略受库版本与提交边界限制,不能假设任何错误都会自动重试。

Hedging是在前一次尚未失败时延迟发起并行尝试,以降低尾延迟,但会增加实际负载和重复执行概率。只适合只读或严格幂等操作,并要限制并发、预算和触发状态。

客户端SDK、Service Mesh和业务代码不能各自独立重试。若每层最多3次,最坏会形成27次尝试。统一由最了解错误语义的一层负责,并记录逻辑请求数与物理Attempt数。

十四、Keepalive、GOAWAY和连接生命周期

  • HTTP/2 PING可探测空闲连接是否仍可用。
  • Keepalive过于频繁会增加服务器和网络负担,服务端可能发送GOAWAY或关闭滥用客户端。
  • GOAWAY通常表示该连接不再接受新的更大Stream ID;已接受Stream可能继续,客户端应在新连接上发起后续RPC。
  • 最大连接年龄可让负载逐步再平衡和证书轮换,但设置过短会制造握手风暴。
  • TCP Keepalive与HTTP/2 PING是不同层次。

十五、TLS、认证和授权

生产应验证服务端证书名称和信任链,不使用usePlaintext()跨不可信网络。mTLS可让服务端验证客户端工作负载证书;Token可放入Metadata,但必须走TLS且避免日志输出。

拦截器适合:

  • 注入和校验认证信息。
  • trace上下文传播。
  • 统一Deadline上限。
  • 记录方法级指标。

拦截器不应把业务事务隐藏在网络层,也不要记录完整请求中的密码、身份证和Token。

十六、Health、Reflection与探针

标准gRPC Health服务可按服务名返回SERVING状态,适合Kubernetes gRPC Probe和负载检查。它应反映是否适合接流量,不要因为一个非关键依赖抖动就让全部实例摘除。

Server Reflection允许工具运行时查询服务描述,便于grpcurl调试。生产是否开启要结合暴露面和权限;关闭Reflection不等于接口安全,TLS、认证和网络策略仍必须存在。

十七、Java最小Demo

服务端实现:

java
public final class InventoryServiceImpl
        extends InventoryServiceGrpc.InventoryServiceImplBase {

    @Override
    public void reserve(ReserveRequest request,
                        io.grpc.stub.StreamObserver<ReserveReply> observer) {
        if (request.getRequestId().isEmpty() || request.getQuantity() <= 0) {
            observer.onError(io.grpc.Status.INVALID_ARGUMENT
                    .withDescription("request_id与quantity不合法")
                    .asRuntimeException());
            return;
        }

        // 生产代码:用request_id唯一键在同一本地事务中扣减并保存结果。
        ReserveReply reply = ReserveReply.newBuilder()
                .setReservationId("R-" + request.getRequestId())
                .setStatus(ReserveStatus.RESERVED)
                .build();
        observer.onNext(reply);
        observer.onCompleted();
    }
}

客户端必须复用Channel并设置Deadline:

java
ManagedChannel channel = ManagedChannelBuilder
        .forAddress("inventory", 9090)
        .usePlaintext() // 仅本地演示;生产配置TLS或Mesh mTLS
        .build();

InventoryServiceGrpc.InventoryServiceBlockingStub stub =
        InventoryServiceGrpc.newBlockingStub(channel)
                .withDeadlineAfter(800, TimeUnit.MILLISECONDS);

ReserveReply reply = stub.reserve(ReserveRequest.newBuilder()
        .setRequestId("order-20260718-001")
        .setSku("sku-1001")
        .setQuantity(1)
        .build());

channel.shutdown();
channel.awaitTermination(5, TimeUnit.SECONDS);

JDK 8/Boot 2.7与Java 17+/Boot 3都可使用grpc-java,但必须选择兼容的grpc-java、Protobuf插件和Netty版本。Boot 3的Jakarta迁移不改变HTTP/2与Protobuf协议;Java 17+应使用当前受支持依赖和更完整的Micrometer/OTel观测,不能把旧依赖坐标原样复制。

十八、商业场景:医疗采集双向流

采集Agent与平台建立双向流:Agent上传批次进度,平台下发暂停、恢复和重试指令。生产协议需要:

  1. 每条消息含agent_idsession_id、单调sequenceevent_id
  2. 平台保存已确认序号,断线后Agent从最后确认位置续传。
  3. 重复event_id由唯一约束吸收,不能把连接内Exactly Once当持久保证。
  4. 使用isReady和有界磁盘队列控制背压。
  5. 心跳只证明连接活性,业务健康还要看采集进度时间。
  6. 证书绑定Agent身份,敏感医疗数据传输启用TLS并脱敏日志。
  7. 服务端发布时发送GOAWAY并让客户端抖动重连,避免同时风暴。

十九、生产Runbook

19.1 UNAVAILABLE突然升高

  1. 按客户端、服务端、Endpoint、Node和Zone拆分。
  2. 查看NameResolver地址、Subchannel状态和Channel状态转换。
  3. 区分连接拒绝、TLS失败、GOAWAY、RST和网络超时。
  4. 检查服务发现是否陈旧、Pod是否终止、Mesh EDS是否NACK。
  5. 查看重试Attempt,防止错误率下降但实际流量暴增。

19.2 DEADLINE_EXCEEDED升高

  1. 查看客户端Deadline分布,不只看服务端耗时。
  2. 拆分排队、连接、网络、服务端队列、数据库和响应序列化。
  3. 检查Deadline是否逐层传播,是否某层重新给满额时间。
  4. 对写请求按request_id查询事实,不能直接重试。
  5. 检查客户端事件循环是否被阻塞。

19.3 RESOURCE_EXHAUSTED

  1. 查看错误描述是消息过大、并发流、配额还是内存限制。
  2. 对照客户端和服务端最大入站消息配置。
  3. 不要只把上限调大;大消息应分块、对象存储或流式处理。
  4. 检查未尊重isReady造成的发送队列堆积。

19.4 流式RPC卡住

  1. 查看双方是否仍有连接和Stream,是否收到半关闭。
  2. 检查HTTP/2连接/Stream窗口和onReady回调。
  3. 检查业务消费者是否阻塞、队列是否满。
  4. 检查是否漏调onCompleted或取消后生产线程仍运行。
  5. 使用消息序号确认最后发送、接收和持久确认位置。

19.5 扩容后流量仍集中

  1. 查看Resolver是否只有ClusterIP还是全部Pod Endpoint。
  2. 查看Channel、Subchannel和HTTP/2连接数量。
  3. 检查pick_first、round_robin和Mesh LB实际配置。
  4. 区分RPC数和连接数,新Pod只会获得新连接。
  5. 有界调整连接寿命,避免同时重连。

二十、常见误区

误区后果
每次RPC创建Channel握手、线程和连接资源浪费
gRPC像本地方法一样可靠忽略网络失败和结果未知
Deadline等于服务端回滚下游可能已提交
流式调用等于MQ断线后消息和进度丢失
无视isReady连续onNext待发送队列导致OOM
Protobuf删除字段后复用编号新旧版本错读字段
只连ClusterIP却期待请求均衡HTTP/2长连接固定少量Pod
所有层都开启重试重试乘法压垮服务
Keepalive越短越可靠PING风暴和服务端GOAWAY

二十一、面试主线

回答时沿“Proto契约与代码生成 → Channel/Resolver/LB → HTTP/2连接与Stream → 服务端调用 → Trailers状态 → Deadline与结果未知 → 流控背压 → 长连接负载 → 重试和Runbook”展开,并明确Protobuf、HTTP/2和gRPC各自职责。

标准回答见gRPC面试题

二十二、关联知识点

本章小结

gRPC降低了接口定义和编解码成本,却没有消除分布式不确定性。真正掌握它,需要把一次Stub调用还原成名称解析、Subchannel选择、HTTP/2 Stream、Protobuf消息、服务端事务和Trailers,并能处理Deadline、流控、长连接倾斜、协议演进和失败恢复。