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 所示,info 日志表明,与服务器的所有三个连接都已连接并准备好传输 RPC。日志中没有出现可疑事件。从 info 日志中可以推断出的一件事是,所有连接始终处于活动状态,因此可以排除连接丢失的假设。
为了进一步缩小问题的根本原因,我们将请求 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 的 Web 工具被开发出来,方便地通过网页提供 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)
在右侧,显示该通道没有子 Channels、3 个 Subchannels(如图 6 中突出显示)和 0 个 Sockets。


图 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 服务器信息中。


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


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