6.2 gRPC 合约、流式 RPC 与失败边界
信标塔准备让不同语言编写的服务协作,阿花却先追问:接口契约能否消除远程调用的不确定性?
gRPC 是 RPC framework,常使用 Protocol Buffers 定义合约,并将每次 RPC 映射到 HTTP/2 stream。它屏蔽了部分 wire detail,但 remote call 仍然不是 local function call:它会 timeout、可能只执行了一部分,也可能 server 已经成功而 client 没收到 result。
1. .proto 同时定义 data 与 service
syntax = "proto3";
package atlas.weather.v1;
service Weather {
rpc Get(GetRequest) returns (Reading);
rpc Watch(WatchRequest) returns (stream Reading);
rpc Upload(stream Reading) returns (UploadSummary);
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
message GetRequest {
string city = 1;
}
message WatchRequest {
string city = 1;
uint32 count = 2;
}
message Reading {
string city = 1;
double celsius = 2;
int64 observed_unix_seconds = 3;
}
message UploadSummary {
uint32 accepted = 1;
}
message ChatMessage {
string room = 1;
string sender = 2;
string text = 3;
}四种 method shape 是:
| shape | request | response | 常见用途 |
|---|---|---|---|
| Unary | one | one | query / command |
| Server streaming | one | stream | watch、大 result set |
| Client streaming | stream | one | batch upload / aggregation |
| Bidirectional streaming | stream | stream | 独立双向 event flow |
Field number 是 wire contract 的一部分。删除 field 后不要重用它的 number;使用 reserved 保留 number/name。新 field 要考虑 old client 看不见时的 default behavior。不要仅以“Protobuf 能 decode”就判定 semantic compatibility。
2. gRPC 如何使用 HTTP/2
一次 RPC 对应一个 HTTP/2 stream。多个 RPC stream 可在同一 connection 上 multiplex,各自有 HTTP/2 flow-control state。Typical request 使用 POST,path 类似:
/atlas.weather.v1.Weather/GetMessage 在 gRPC length-prefixed framing 中传输,HTTP/2 DATA frame boundary 不等于 Protobuf message boundary。Final gRPC status 通常通过 trailers 传递:
grpc-status: 0
grpc-message: ...因此,只看 HTTP status 200 不足以判定 RPC 成功。Client library 会合并 HTTP/2 transport state、gRPC status 和 application response。Proxy 必须支持 trailers、HTTP/2 和 gRPC content type,否则可能出现“HTTP 连通,RPC 失败”。
Multiplexing 消除了 HTTP/1.1 connection-level response ordering,但多个 stream 仍共享一条 TCP connection。Packet loss 会在 TCP transport 层影响该 connection 上的 data delivery,不应将 HTTP/2 multiplexing 描述成“完全没有 head-of-line blocking”。
3. Deadline 是 API contract,不是 client 的附加选项
Client 应为 RPC 设置 deadline。没有 deadline 的 call 可能一直等待,占住 thread、connection capacity 和上游 resource。
Server 需要:
- 查看 remaining deadline;
- 在 client cancellation 或 deadline exceeded 后停止无意义工作;
- 调用 downstream 时传递或收紧 deadline,为 cleanup 保留 budget;
- 不要把 deadline exceeded 当作 server 一定没有执行 operation 的证明。
Cancellation 是 signal,不是 transaction rollback。如果 RPC 可能产生重要 side effect,API 要设计 idempotency key、operation status query 或 compensating action。
4. Status code 要保留语义
常见 code 边界:
INVALID_ARGUMENT:request 本身无效,不会因 system state 改变而变对;FAILED_PRECONDITION:request 可能有效,但当前 system state 不允许;NOT_FOUND:指定 entity 不存在;ALREADY_EXISTS:create 目标已存在;PERMISSION_DENIED:caller identity 已知但无权;UNAUTHENTICATED:没有可用的 authentication credential;RESOURCE_EXHAUSTED:quota 或 resource limit;UNAVAILABLE:transient service availability problem,可能适合受控 retry;DEADLINE_EXCEEDED:caller 的 deadline 耗尽;INTERNAL:invariant broken 或不应暴露的 internal failure。
不要把所有 exception 都映射到 UNKNOWN 或 INTERNAL,也不要将 database exception text 直接放进 grpc-message。对 client 公开稳定 code 和 structured error detail,把 sensitive diagnostic 留在 server log/trace。
5. Retry 必须和 idempotency 一起设计
Retry 可能来自 client library、service mesh、proxy 或 application,多层叠加会放大 load。在启用 retry 前先回答:
- Method 重复执行是否安全?
- 哪些 status code 是 transient?
- Per-attempt timeout 和 overall deadline 多少?
- Max attempts、backoff 和 jitter 多少?
- Server 如何用 idempotency key 去重?
Client 没收到 response 时,不能推断 server 没有 commit。对创建 order 之类 RPC,要用 caller-generated stable request ID 建立 deduplication contract,而不是看到 UNAVAILABLE 就无条件重发。
6. Streaming 不等于无限 buffer
HTTP/2 和 gRPC library 提供 flow control,但 application 仍需要 bounded memory policy。Streaming handler 要考虑:
- 单条 message 大小上限;
- inbound / outbound queue limit;
- slow reader 对 producer 的 backpressure;
- cancellation 时停止 producer;
- partial progress 是否可 resume;
- stream 最终 status 到达前,已收到的 message 是否可 commit。
Bidirectional streaming 只表示双方的 message stream 可以独立推进,不自动提供 business ordering、exactly-once delivery 或 durable replay。
7. 可运行的 Unary + Server Streaming 最小项目
安装工具:
python3 -m pip install grpcio grpcio-tools将前面 .proto 缩减为实验所需的 weather.proto:
syntax = "proto3";
package atlas.weather.v1;
service Weather {
rpc Get(GetRequest) returns (Reading);
rpc Watch(WatchRequest) returns (stream Reading);
}
message GetRequest { string city = 1; }
message WatchRequest { string city = 1; uint32 count = 2; }
message Reading { string city = 1; double celsius = 2; }生成 Python code:
python3 -m grpc_tools.protoc \
-I. \
--python_out=. \
--grpc_python_out=. \
weather.protoserver.py:
from concurrent import futures
import time
import grpc
import weather_pb2
import weather_pb2_grpc
class Weather(weather_pb2_grpc.WeatherServicer):
def Get(self, request, context):
if not request.city:
context.abort(grpc.StatusCode.INVALID_ARGUMENT, "city is required")
return weather_pb2.Reading(city=request.city, celsius=21.5)
def Watch(self, request, context):
if not request.city:
context.abort(grpc.StatusCode.INVALID_ARGUMENT, "city is required")
count = min(request.count or 3, 10)
for index in range(count):
if not context.is_active():
return
yield weather_pb2.Reading(
city=request.city,
celsius=21.5 + index * 0.1,
)
time.sleep(0.1)
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=8))
weather_pb2_grpc.add_WeatherServicer_to_server(Weather(), server)
if server.add_insecure_port("127.0.0.1:50051") == 0:
raise RuntimeError("failed to bind 127.0.0.1:50051")
server.start()
print("listening on 127.0.0.1:50051")
server.wait_for_termination()
if __name__ == "__main__":
serve()client.py:
import grpc
import weather_pb2
import weather_pb2_grpc
with grpc.insecure_channel("127.0.0.1:50051") as channel:
stub = weather_pb2_grpc.WeatherStub(channel)
current = stub.Get(weather_pb2.GetRequest(city="Nanjing"), timeout=2)
print(current)
stream = stub.Watch(
weather_pb2.WatchRequest(city="Nanjing", count=3),
timeout=2,
)
for reading in stream:
print(reading)这里的 insecure channel 只绑定 loopback,用于实验。Production 要根据 trust model 使用 TLS server credential / channel credential,并在 application 层设计 authentication 与 authorization。“开了 TLS”不等于 caller 已拥有 method permission。
8. Load balancing 与 browser boundary
gRPC channel 往往长时间复用 connection。如果 load balancer 只在 TCP connection 建立时选 backend,一条长连接上的多个 RPC 不会被逐请求重新分配。可选策略包括 gRPC-aware L7 proxy、client-side load balancing 和 xDS,并要与 health checking、connection age 和 outlier detection 一起设计。
Browser 不直接暴露通用 raw HTTP/2 framing API,gRPC-Web 通常通过 compatible client 和 proxy 与 gRPC backend 交互。它的 streaming support、content type 和 transport behavior 与 native gRPC 不完全相同,不应将 native bidirectional streaming 能力直接假设到 browser client。
9. 验收问题
- 为什么 HTTP status 200 不足以证明 gRPC 成功?
- 区分 deadline exceeded、cancellation 和 transaction rollback。
- 设计一个可安全 retry 的 create operation,说明 idempotency key 存在哪里。
- 为 server-streaming RPC 定义 message limit、queue limit 和 slow-reader policy。
- 为什么一条 HTTP/2 connection 的 multiplexing 不能消除 TCP packet loss 的 connection-level influence?