grpc-go 整体架构
grpc-go 的核心不是“远程函数调用”这一层语法糖,而是把服务描述、连接治理和 HTTP/2 流状态机分开。生成代码只负责类型安全入口,运行时负责把一次调用送到可用连接并维护跨进程语义。
先给答案:grpc-go 的核心不是“远程函数调用”这一层语法糖,而是把服务描述、连接治理和 HTTP/2 流状态机…
Section titled “先给答案:grpc-go 的核心不是“远程函数调用”这一层语法糖,而是把服务描述、连接治理和 HTTP/2 流状态机…”grpc-go 的核心不是“远程函数调用”这一层语法糖,而是把服务描述、连接治理和 HTTP/2 流状态机分开。生成代码只负责类型安全入口,运行时负责把一次调用送到可用连接并维护跨进程语义。 正文沿“模块地图 -> 分层边界 -> 关键取舍”展开:先确认入口和状态归属,再跟踪控制流或数据流的推进,最后落到对外可观察的结果。
主要失效边界集中在“生成代码、ClientConn、balancer”这些场景。它们破坏的是容量、顺序、并发或生命周期前提;排查时应先确认状态是否仍由正确对象持有,再核对推进条件和清理路径。
generated *.pb.go | +-> ServiceDesc / MethodDesc -> grpc.Server | +-> ClientConnInterface -> grpc.ClientConn | +-----------------+------------------+ | | | resolver balancer interceptor | | State SubConn + Picker | internal/transport | HTTP/2服务端以 Server 保存 listener、transport 连接、服务描述和拦截器:server.go:126-177;注册入口是 RegisterService:server.go:756-800。客户端的长生命周期 channel 是 ClientConn:clientconn.go:668-735,底层连接由 addrConn 和 ClientTransport 承担。
| 层 | 负责 | 不负责 |
|---|---|---|
| 生成代码 | 方法签名、序列化类型、ServiceDesc | 连接管理 |
ClientConn |
resolver/balancer、状态和调用配置 | HTTP/2 帧细节 |
| balancer | 管理 SubConn、发布 Picker | 直接发送 RPC |
| transport | stream、帧、流控、keepalive | 业务路由 |
| Server | 服务查找、拦截器、handler 调用 | 服务发现 |
ClientTransport 与 ServerTransport 的接口位于 internal/transport/transport.go:610-694;resolver 和 balancer 的核心接口分别位于 resolver/resolver.go:232-329、balancer/balancer.go:139-365。
替代方案:每次 RPC 独立解析地址、拨号并关闭连接。
问题:无法复用 HTTP/2 连接,解析和握手成本进入请求热路径,也无法维护持续的连接状态与负载反馈。
设计:ClientConn 作为长生命周期 channel,控制面持续更新 Picker,数据面只在调用时选连接并创建 stream。
| 场景 | 风险 | 处理 |
|---|---|---|
| 把 ClientConn 当单连接 | 误判连接数和状态 | 它可能管理多个 SubConn/transport |
| 在 handler 中处理协议帧 | 业务与 HTTP/2 耦合 | 使用 stream/status/metadata API |
| 生成代码版本漂移 | ServiceDesc 约定不匹配 | protobuf 与 grpc 插件版本配套 |
| 只观察 TCP 成功 | RPC 仍可能无可用 Picker | 同时观察 channel、SubConn 和 RPC 状态 |
高性能 RPC 运行时通常要把控制面与数据面拆开:解析和连接状态异步收敛,调用热路径只做配置匹配、节点选择和流创建。
面试锚点
ClientConn为什么不是一条物理连接?- 生成代码、Server 和 transport 的职责如何分离?
- resolver 与 balancer 为什么不直接参与帧收发?