状态:草稿 (2026-10-17),代码阅读。 基准版本 grpc/grpc-go v1.83.2(提交 030ee8becb20,从本地 Go 模块缓存中读取)。最新发布版为 v1.84.0,行号可能略有不同。正文中的
파일:줄是 v1.83.2 的函数声明行。本篇没有新的实测,仅与第 6 篇的数据进行对比。
目录
1. 一句话总结:公司组织架构
Channel 是 团队长。当接收到工作(调用)时,它不会亲自去做,而是将通讯录负责人(解析器)给出的名单交给分配负责人(负载均衡器),分配负责人会挑选好员工(子通道)并在每次有工作到来时,通过当天的分配表(Picker)选定一人。员工拥有自己的一条电话线(HTTP/2 连接)。分配表是一张整体替换的纸,所以在选择工作的瞬间,不会与任何人争夺锁。
2. 架构图
3. 代码导读
| 路径 | 说明 | 首先阅读的位置 |
|---|---|---|
clientconn.go |
通道、子通道(addrConn)、空闲模式、重试控制 |
NewClient(183), addrConn(1240) |
call.go, stream.go |
一元调用、流式调用、重试尝试(csAttempt) |
invoke(65), newClientStream(213) |
resolver_wrapper.go, resolver/, internal/resolver/ |
解析器包装与 dns、passthrough、unix 实现 | ccResolverWrapper.UpdateState(129) |
balancer_wrapper.go, balancer/ |
负载均衡器包装,pick_first、round_robin、ringhash、weightedroundrobin 等 | ccBalancerWrapper.NewSubConn(180) |
picker_wrapper.go |
每次调用选择子通道,等待 | pickerWrapper.pick(105) |
internal/transport/ |
HTTP/2 客户端与服务器,流量控制,keepalive,控制缓冲区 | http2_client.go, http2_server.go |
rpc_util.go |
5 字节前缀,压缩,接收上限,调用选项 | msgHeader(879), parser.recvMsg(785) |
server.go |
服务器,服务注册,分发 | Serve(888), handleStream(1800) |
codes/, status/ |
状态码与状态对象 | codes.go |
health/, reflection/, channelz/, admin/ |
附加服务(第 5、8 篇) | health/server.go |
xds/, internal/xds/ |
xDS 解析器与负载均衡器(第 5 篇) | 规模较大,留待以后 |
4. 跟踪核心路径
4.1 创建通道:暂不建立任何连接
NewClient 应用选项并解析目标字符串以选择解析器构建器,随后
(initParsedTargetAndResolverBuilder, 默认 scheme dns), 准备好空闲状态并结束。
准备空闲状态(initIdleStateLocked) 会创建解析器包装
(newCCResolverWrapper) 和负载均衡器包装(newCCBalancerWrapper),但解析器此时还不会运行。当收到第一次调用时, exitIdleMode 会将通道更改为 CONNECTING 并启动解析器。如果 30 分钟内没有调用, enterIdleMode 会清理解析器、负载均衡器和连接,并重新回到空闲状态。
4.2 当地址到来时:解析器 → 负载均衡器 → 子通道 → 拾取器 (Picker)
- 当解析器找到地址时,会调用
ccResolverWrapper.UpdateState,通道的updateResolverStateAndUnlock应用服务配置后,将其传递给 (applyServiceConfigAndBalancer) 负载均衡器 (updateClientConnState). - 负载均衡器(例如 round_robin)会为每个地址调用
NewSubConn,这会通过newAddrConnLocked创建addrConn。这个addrConn就是子通道的实体。 addrConn会按照resetTransportAndUnlock→tryAllAddrs→createTransport的顺序进行连接,如果失败则在退避 (backoff) 后重试。如果失败或断开连接,会向解析器请求ResolveNow(第 5 篇第 4.1 节)。- 每当子通道状态改变时,负载均衡器就会创建一个新的拾取器并上传到
ccBalancerWrapper.UpdateState,然后pickerWrapper.updatePicker会以 原子方式替换拾取器。正在等待的调用会因为与旧拾取器关联的通道关闭而被唤醒。
为什么要这样划分呢?这是为了让查找地址(DNS、xDS、Kubernetes API 等)和选择地址(首选、轮询、加权、哈希)相互解耦,从而能够 组合任意解析器和任意负载均衡器。负载均衡器端的更新通过串行执行器(grpcsync.CallbackSerializer, balancer_wrapper.go 的注释)依次处理,而调用流经的热点路径 (pick) 则只需读取一个原子值,从而确保两个世界互不等待对方的锁。
4.3 单次调用:从 Invoke 到流 (Stream)
- 生成的 Stub 会调用
ClientConn.Invoke,经过拦截器后,invoke进入newClientStream→newClientStreamWithParams。 一元调用 (Unary call) 在内部本质上也是“发送一个消息并接收一个消息的流”。 - 创建一个尝试(
newAttemptLocked),getTransport从拾取器中选择一个子通道。在所选子通道的传输层上,使用csAttempt.newStream→http2Client.NewStream发送 HEADERS(第 3 篇的createHeaderFields). SendMsg负责序列化、压缩并加上 5 字节前缀写入csAttempt.sendMsg,并通过RecvMsg→recv→parser.recvMsg读取响应。如果失败,withRetry会参考第 4 篇的shouldRetry创建新的尝试,并重新播放缓存在缓冲区中的已发送消息。
4.4 拾取器:是等待还是立即失败?
如果来了调用,但没有处于 READY 状态的子通道,会发生什么呢? pickerWrapper.pick 的一个分支给出了答案。
pickResult, err := p.Pick(info)
if err != nil {
if err == balancer.ErrNoSubConnAvailable {
continue
}
if st, ok := status.FromError(err); ok {
...
return pick{}, dropError{error: err}
}
// For all other errors, wait for ready RPCs should block and other
// RPCs should fail with unavailable.
if !failfast {
lastPickErr = err
continue
}
return pick{}, status.Error(codes.Unavailable, err.Error())
}
来源: grpc/grpc-go v1.83.2 picker_wrapper.go 摘录自 157~176 行,Apache-2.0
- 如果仍在连接中(
ErrNoSubConnAvailable),则会 等待直到新的 picker 到来。这就是为什么正在连接中的通道不会调用失败的原因。 - 如果所有子通道都是 TRANSIENT_FAILURE,picker 就会返回错误,而默认的(fail fast)行为是 立即返回 UNAVAILABLE 。
grpc.WaitForReady(true)只有带有该标志的调用才会一直等到 deadline 结束 (wait-for-ready.md 中的定义完全一致)。 - 如果返回状态错误(丢弃),则甚至不会进行重试。这是控制平面用于表示“丢弃此调用”的途径。
在生产环境中,这种差异表现为:在重启服务器的几秒钟内,fail fast 客户端会不断抛出 UNAVAILABLE(就像第 6 篇中 Collector 的 10-13 记录一样), 而 wait-for-ready 客户端则会静静等待,如果在 deadline 内服务器恢复,就会调用成功。
4.5 服务器:从 Serve 到 Handler
Server.Serve每当Accept发生handleRawConn时,就会启动一个 goroutine 来运行- 。在凭据握手之后,
NewServerTransport会交换 HTTP/2 设置, 并且serveStreams会读取帧。并发流数的默认值是无限制(math.MaxUint32,server.go:189)。 - 当 HEADERS 到来时,
operateHeaders会读取:path,content-type,grpc-timeout以创建流(无效的 content-type 会返回 415),handleStream会将路径拆分为服务名和方法名, 并在RegisterService注册表中(第 1 篇的ServiceDesc)进行查找。如果找不到,则返回 UNIMPLEMENTED。 - 如果是一元调用(unary),则由
processUnaryRPC处理;如果是流式调用,则由processStreamingRPC读取消息、调用拦截器和 handler,然后通过writeStatus写入 trailer。 GracefulStop会发送 GOAWAY 以阻止新流并等待现有流完成。第 5 篇中的 MaxConnectionAge 也使用了相同的 GOAWAY。
5. 与第 6 篇实测的对比
| 第 6 篇中测得的数据 | 在此拓扑图中 |
|---|---|
| apiserver → etcd 247 个 TCP 连接 | 247 个 etcd 客户端 = 247 个 ClientConn ,各自拥有一个 READY addrConn (因为目标只有一个 127.0.0.1) |
| 节点 A 打开的 Watch 流 242 个 | 每个 ClientConn 的 http2Client 上方有一个长连接流 |
| kubelet → containerd 2 个通道 | 运行时和镜像 ClientConn 各一个, passthrough 是 Unix 套接字而非 |
| hubble-relay 连接了 8 个节点 | 每个节点上有 ClientConn(由 relay 的 peer pool 管理,推测) |
| kubelet gRPC 探针 | 每个探针都会 ClientConn 创建和销毁(第 5 篇第 6 节) |
即使使用相同的 grpc-go, 创建多少个通道以及如何使用它们完全由编写客户端的代码决定,这种选择决定了连接数、内存和负载均衡。
6. 从哪里开始阅读
Documentation/的keepalive.md·concurrency.md·anti-patterns.md首先阅读。anti-patterns.md解释了为什么要避免旧的Dial·WithBlock。call.go→stream.go的newClientStreamWithParams·csAttempt可以追踪单个调用。picker_wrapper.go→balancer_wrapper.go→balancer/roundrobin查看选择端,resolver_wrapper.go→internal/resolver/dns查看查找端。internal/transport/http2_client.go的NewStream·reader·keepalive,controlbuf.go可以深入到写入循环中。- 对于服务器,只需阅读
server.go的handleStream→processUnaryRPC就能了解分发架构。
7. 适合新手贡献的地方
- 对于初次贡献者,grpc-go 推荐查看没有特定负责人的“Status: Help Wanted”问题(2026-10-17 时有 11 个开放问题,参阅 CONTRIBUTING)。更改行为或 API 的工作 必须先通过 Issue 达成一致,而需要多个语言共同更改的功能则必须从 gRFC 开始。
- 生成式 AI 政策存续于此。仓库中的所有交流(Issue、PR 正文、评论)都必须由人工亲自编写(翻译和润色辅助除外),自动化代理不得直接在 GitHub 上发帖, 并且 AI 生成的代码必须由提交者完全理解并承担全部责任(master 中的 CONTRIBUTING “Generative AI Policy”)。
- 阅读时发现的三个 TODO:
stream.go:634中的 hedging,clientconn.go第 1364 行的“将重新解释请求移至 pick_first 内部”(#7534),createHeaderFields的前缀头数量预计算基准测试。 这三者都需要进行设计讨论,作为第一个 PR 来说可能有点大,但它们是阅读 Issue 的好入口。 - 文档方面的候选项已收集在第 8 篇第 7 节中。
8. 局限性
- 我们没有阅读 xDS(
xds/·internal/xds/)以及 ringhash、weightedroundrobin、RLS 等高级负载均衡器。 controlbuf.go没有涉及mem/的写入调度、内存池()以及统计处理器(OpenTelemetry 集成)。- 行号基于 v1.83.2 版本。未核实与 v1.84.0 的差异(未核实)。
Titi 的小结。 因为可以把分配表(picker)整个替换掉,所以即使顾客蜂拥而至,也不用跟分配负责人排队争吵。相反,如果员工全都累倒了,是该等待(WaitForReady)还是请回(fail fast)由顾客自己决定!
参考资料
所有资料均查阅于 2026-10-17。
- grpc/grpc-go v1.83.2:
clientconn.go,call.go,stream.go,picker_wrapper.go,balancer_wrapper.go,resolver_wrapper.go,server.go,internal/transport/http2_client.go,internal/transport/http2_server.go,rpc_util.go,Documentation/, CONTRIBUTING.md(master) - grpc/grpc v1.84.0:
doc/wait-for-ready.md,doc/load-balancing.md - 引用代码:grpc-go v1.83.2,Apache License 2.0。