Channelz 简介
Channelz 是一种工具,它提供 gRPC 中不同级别的连接的全面运行时信息。它旨在帮助调试可能遭受网络、性能、配置等问题的实时程序。gRFC 提供了 channelz 设计的详细解释,并且是所有跨语言 channelz 实现的规范参考。本博客的目的是让读者熟悉 channelz 服务以及如何使用它来调试问题。本文的背景设置在 gRPC-Go 中,但总体思想应适用于所有语言。撰写本文时,channelz 可用于 gRPC-Go 和 gRPC-Java。对 C++ 和封装语言的支持即将推出。
让我们通过一个使用 channelz 帮助调试问题的简单示例来学习 channelz。helloworld 示例的代码稍作修改,以设置一个有问题的场景。您可以在此处找到完整的源代码:客户端,服务器。
客户端设置:客户端将向指定目标发出 100 个 SayHello RPC,并使用轮询策略进行负载均衡。每次调用都有 150 毫秒的超时。RPC 响应和错误将记录下来,以供调试使用。
运行程序,我们在日志中注意到存在带有错误代码 DeadlineExceeded 的间歇性错误,如图 1 所示。
然而,对于导致截止日期超时的原因却没有任何线索,并且存在许多可能性
- 网络问题,例如:连接丢失
- 代理问题,例如:中途丢弃请求/响应
- 服务器问题,例如:请求丢失或响应缓慢


图 1. 程序日志截图
让我们打开 grpc INFO 日志以获取更多调试信息,看看能否找到有用的东西。


图 2. gRPC INFO 日志
如图 2 所示,信息日志显示与服务器的所有三个连接均已连接并准备好传输 RPC。日志中没有出现可疑事件。可以从信息日志中推断出的一件事是,所有连接始终保持活动状态,因此可以排除连接丢失的假设。
为了进一步缩小问题的根本原因,我们将向 channelz 寻求帮助。
Channelz 通过 gRPC 服务提供 gRPC 内部网络机制的统计数据。要启用 channelz,用户只需将 channelz 服务注册到其程序中的 gRPC 服务器并启动该服务器。下面的代码片段显示了将 channelz 服务注册到 grpc.Server 的 API。请注意,这已经为我们的示例客户端完成。
import "google.golang.org/grpc/channelz/service"
// s is a *grpc.Server
service.RegisterChannelzServiceToServer(s)
// call s.Serve() to serve channelz service
一个名为 grpc-zpages 的网络工具已经开发出来,可以方便地通过网页提供 channelz 数据。首先,配置 Web 应用程序以连接到提供 channelz 服务的 gRPC 端口(请参阅上一个链接中的说明)。然后,在浏览器中打开 channelz 网页。您应该会看到类似图 3 的网页。现在我们可以开始查询 channelz 了!


图 3. Channelz 主页
由于错误发生在客户端,我们首先点击 TopChannels。TopChannels 是一个没有父级的根通道集合。在 gRPC-Go 中,顶层通道是通过 NewClient 由用户创建的 ClientConn,用于进行 RPC 调用。顶层通道在 channelz 中是 Channel 类型,它是可以发出 RPC 调用的连接的抽象。


图 4. TopChannels 结果
因此,我们点击 TopChannels,出现一个像图 4 所示的页面,其中列出了所有活动的顶层通道及其相关信息。
如图 5 所示,只有一个 id = 2 的顶层通道(请注意,方括号中的文本是内存中通道对象的引用名称,可能因语言而异)。
查看 Data 部分,我们可以看到此通道上 100 个调用中有 15 个失败。


图 5. 顶层通道 (id = 2)
在右侧,它显示该通道没有子 通道,有 3 个 子通道(如图 6 中突出显示),以及 0 个 套接字。


图 6. 通道(id = 2)拥有的子通道
一个 Subchannel 是对连接的抽象,用于负载均衡。例如,您想向“google.com”发送请求。解析器将“google.com”解析为提供“google.com”的多个后端地址。在此示例中,客户端设置为轮询负载均衡器,因此所有活动后端都发送相同的流量。然后,与每个后端的(逻辑)连接表示为一个 Subchannel。在 gRPC-Go 中,SubConn 可以看作一个 Subchannel。
父通道拥有的三个子通道意味着有三个连接到三个不同的后端,用于发送 RPC。让我们查看每个连接以获取更多信息。
所以我们点击列出的第一个子通道 ID(即“4[]”),然后页面呈现如图 7 所示。我们可以看到,该子通道上的所有调用都已成功。因此,这个子通道不太可能与我们遇到的问题相关。


图 7. 子通道 (id = 4)
所以我们返回,点击子通道 5(即“5[]”)。同样,网页显示子通道 5 也从未有过任何失败的调用。


图 8. 子通道 (id = 6)
最后,我们点击子通道 6。这次,有些不同。正如我们在图 8 中看到的,此子通道上 34 个 RPC 调用中有 15 个失败。请记住,父通道也恰好有 15 个失败调用。因此,子通道 6 是问题来源。子通道的状态是 READY,这意味着它已连接并准备好传输 RPC。这排除了网络连接问题。为了挖掘更多信息,我们来看看此子通道拥有的套接字。
一个 Socket 大致相当于一个文件描述符,通常可以看作是两个端点之间的 TCP 连接。在 grpc-go 中,http2Client 和 http2Server 对应于 Socket。请注意,网络监听器也被视为 Socket,并会显示在 channelz Server 信息中。


图 9. 子通道 (id = 6) 拥有套接字 (id = 8)
我们点击页面底部的套接字 8(参见图 9)。现在我们看到了如图 10 所示的页面。
该页面提供了关于套接字的全面信息,例如正在使用的安全机制、流计数、消息计数、keepalives、流量控制数字等。套接字选项信息未在截图中显示,因为它们很多且与我们正在调查的问题无关。
远程地址字段表明我们遇到的问题后端是 “127.0.0.1:10003”。这里的流计数与父子通道的调用计数完全对应。由此,我们可以知道服务器没有主动发送 DeadlineExceeded 错误。这是因为如果 DeadlineExceeded 错误是由服务器返回的,那么所有流都将成功。客户端流的成功与否与调用是否成功无关。它取决于是否已收到设置了 EOS 位的 HTTP2 帧(更多信息请参阅 gRFC)。此外,我们可以看到发送的消息数量是 34,等于调用的数量,这排除了客户端以某种方式卡住并导致超时的情况。总而言之,我们可以将问题范围缩小到在 127.0.0.1:10003 上提供服务的服务器。可能是服务器响应缓慢,或者它前面的某个代理正在丢弃请求。


图 10. 套接字 (id = 8)
如您所见,channelz 帮助我们通过几次点击就找到了问题的潜在根本原因。现在您可以专注于已查明问题的服务器上发生的事情。而且,channelz 也可以帮助加快服务器端的调试。
我们将在此停下来,让读者探索服务器端 channelz,它比客户端更简单。在 channelz 中,Server 也是一个 RPC 入口点,类似于 Channel,传入的 RPC 在此到达并得到处理。在 grpc-go 中,grpc.Server 对应于 channelz Server。与 Channel 不同,Server 只有 Socket(包括监听 Socket 和正常连接的 Socket)作为其子级。
以下是给读者的提示
- 寻找地址为 (127.0.0.1:10003) 的服务器。
- 查看调用计数。
- 转到服务器拥有的套接字。
- 查看套接字流计数和消息计数。
您应该会注意到,服务器套接字收到的消息数量与客户端套接字(套接字 8)发送的消息数量相同,这排除了中间存在行为异常的代理(丢弃请求)的情况。而且,服务器套接字发送的消息数量等于客户端接收到的消息数量,这意味着服务器未能在截止日期前发送回响应。您现在可以查看服务器代码,以验证这是否确实是原因。
服务器设置:服务器端程序启动了三个 GreeterServer,其中两个使用不会在响应客户端时施加延迟的实现(server),另一个使用在发送响应前注入 100 毫秒至 200 毫秒可变延迟的实现(slowServer)。
通过这个演示,您可以看到 channelz 如何帮助我们快速缩小问题范围,并且易于使用。有关更多资源,请参阅详细的 channelz gRFC。请在 GitHub 上找到我们:github.com/grpc/grpc-go。