前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >观察HTTP/2流量是困难的,但eBPF可以帮助

观察HTTP/2流量是困难的,但eBPF可以帮助

作者头像
CNCF
发布2022-03-27 10:26:20
发布2022-03-27 10:26:20
1.3K00
代码可运行
举报
文章被收录于专栏:CNCFCNCF
运行总次数:0
代码可运行

作者:Yaxiong Zhao

在当今充满微服务的世界中,获取服务之间发送的消息的可观察性对于理解和排除问题至关重要。

不幸的是,HTTP/2 的专用头压缩算法 HPACK 使得跟踪 HTTP/2 变得复杂。虽然 HPACK 有助于提高 HTTP/2 比 HTTP/1 的效率,但它的有状态算法有时会使典型的网络跟踪程序无效。这意味着像 Wireshark 这样的工具不能总是从网络流量中解码明文 HTTP/2 头。

幸运的是,通过使用 eBPF uprobe,可以在流量被压缩之前跟踪它,这样你就可以调试你的 HTTP/2(或 gRPC)应用程序。

这篇文章将回答以下问题

  • 什么情况下 Wireshark 无法解码 HTTP/2 头?
  • 为什么 HPACK 会使头解码变得复杂?
  • eBPF uprobe 如何解决 HPACK 问题?

以及分享一个演示项目,展示了如何用 ebpf uprobe 跟踪 HTTP/2 消息。

什么情况下 Wireshark 无法解码 HTTP/2 头?

Wireshark[1]是一种知名的网络嗅探工具,可以捕获 HTTP/2。但是 Wireshark 有时无法解码 HTTP/2 头。让我们看看它的实际应用。

如果我们在启动 gRPC 演示程序之前启动 Wireshark,我们可以在 Wireshark 中看到捕获的 HTTP/2 消息:

Wireshark 抓到了 HTTP/2 头帧。

让我们关注头帧[2],它相当于 HTTP 1 中的头。,记录 HTTP/2 会话的元数据。我们可以看到一个特定的 HTTP/2 头块片段有原始字节 bfbe。在这种情况下,原始字节编码 grpc-status 和 grpc-message 头。Wireshark 将其正确解码如下:

如果 Wireshark 在消息流开始之前启动,则可以解码 HTTP/2 HEADERS。

接下来,在启动 gRPC 客户端和服务器之后,让我们启动 Wireshark。同样的消息被捕获,但是原始字节不再被 Wireshark 解码:

消息流启动后,Wireshark 无法解码 HTTP/2 HEADERS。

在这里,我们可以看到 Header Block Fragment 仍然显示相同的原始字节,但明文头不能被解码。

要自己复制这个实验,请按照这里[3]的说明。

HPACK: Wireshark 的祸根

如果 Wireshark 在我们的 gRPC 应用程序开始传输消息后启动,为什么它不能解码 HTTP/2 头?

这是因为,HTTP/2 使用HPACK[4]来编码和解码头,压缩头,比 HTTP 1.x 大大提高了效率[5]

HPACK 通过在服务器和客户端维护相同的查找表来工作。在这些查找表中,头文件和/或它们的值被它们的索引所替换。因为大多数头文件都是重复传输的,所以它们被索引所取代,索引比明文头文件使用的字节少得多。因此,HPACK 使用的网络带宽显著减少。由于多个 HTTP/2 会话可以在同一个连接上复用,这种效应被放大了。

下图说明了客户机和服务器为响应头维护的表。新的头名称和值对被追加到表中,如果查找表的大小达到限制,将替换旧的条目。编码时,明文头将被它们在表中的索引所取代。要了解更多信息,请查看官方 RFC[6]

HTTP/2 的 HPACK 压缩算法要求客户端和服务器维护相同的查找表来解码头。这使得无法访问此状态的跟踪程序难以解码 HTTP/2 头。

有了这些知识,就可以清楚地解释上面的 Wireshark 实验的结果了。当 Wireshark 在启动应用程序之前启动程序时,会记录整个头的历史记录,以便 Wireshark 能够重新生成完全相同的头表。

启动应用程序后,Wireshark 启动时,会丢失最初的 HTTP/2 帧,导致后面编码的字节 bebf 在查找表中没有相应的表项。因此 Wireshark 无法解码相应的头。

HTTP/2 头是 HTTP/2 连接的元数据。这些标头是调试微服务的关键信息。例如:path 包含被请求的资源;content-type 需要检测 gRPC 消息,然后应用 protobuf 解析;和 gRPC-status 是确定一个 gRPC 呼叫成功的必要条件。如果没有这些信息,HTTP/2 跟踪将失去它的大部分价值。

基于 Uprobe 的 HTTP/2 跟踪

如果我们不知道状态就不能正确解码 HTTP/2 流量,我们该怎么办?

幸运的是,eBPF 技术使我们能够通过探究 HTTP/2 实现来获得我们需要的信息,而不需要状态。

具体来说,eBPF uprobe 通过直接跟踪应用程序内存中的明文数据来解决 HPACK 问题。通过将 uprobe 附加到接受明文头信息作为输入的 HTTP/2 库的 API 上,uprobe 能够在被 HPACK 压缩之前直接从应用程序内存中读取头信息。

之前的一篇 eBPF 博文[7]展示了如何为用 Golang 编写的 HTTP 应用程序实现 uprobe 跟踪程序。第一步是确定要附加 BPF 探针的函数。函数的参数需要包含我们感兴趣的信息。理想情况下,参数应该具有简单的结构,这样在 BPF 代码中访问它们很容易(通过手动指针追踪)。而且该函数需要是稳定的,这样探针才能适用于各种版本。

通过研究 Golang 的 gRPC 库的源代码,我们确定 loopyWriter.writeHeader()是一个理想的跟踪点。这个函数接受明文头字段,并将它们发送到内部缓冲区。函数签名和实参的类型定义是稳定的,自2018[8]年以来没有更改过。

现在的挑战是找出数据结构的内存布局,并编写 BPF 代码以在正确的内存地址读取数据。

让我们来看看这个函数的签名:

代码语言:javascript
代码运行次数:0
复制
func (l *loopyWriter) writeHeader(streamID uint32, endStream bool, hf []hpack.HeaderField, onWrite func())

任务是读取第 3 个参数 hf 的内容,它是 HeaderField 的一个切片。我们使用 dlv 调试器来计算嵌套数据元素的偏移量,结果显示在http2-tracing/uprobe_trace/bpf_program.go[9]中。

这段代码执行 3 个任务:

  • probe_loopy_writer_write_header()获得一个指向切片中持有的 HeaderField 对象的指针。一个切片在内存中是一个 3 元组{pointer, size, capacity},BPF 代码从 SP 指针读取指针和某些偏移量的大小。
  • submit_headers()通过指针来导航 HeaderField 对象的列表,方法是将指针与 HeaderField 对象的大小递增。
  • 对于每个 HeaderField 对象,copy_header_field()将其内容复制到输出 perf 缓冲区。HeaderField 是一个由两个字符串对象组成的结构体。此外,每个字符串对象都作为一个 2 元组{pointer, size}驻留在内存中,BPF 代码从指针复制相应的字节数。

让我们运行 uprobe HTTP/2 跟踪程序,然后启动 gRPC 客户机和服务器。请注意,即使在建立 gRPC 客户机和服务器之间的连接后启动了跟踪程序,这个跟踪程序也能工作。

现在我们看到从 gRPC 服务器发送到客户端的响应头:

代码语言:javascript
代码运行次数:0
复制
[name=':status' value='200']
[name='content-type' value='application/grpc']
[name='grpc-status' value='0']
[name='grpc-message' value='']

我们还对 google.golang.org/grpc/internal/transport.(*http2Server).operateHeaders()在 probe_http2_server_operate_headers()实现了一个探针;跟踪在 gRPC 服务器上接收到的头。

这让我们可以看到 gRPC 服务器从客户端接收到的请求头:

代码语言:javascript
代码运行次数:0
复制
[name=':method' value='POST']
[name=':scheme' value='http']
[name=':path' value='/greet.Greeter/SayHello']
[name=':authority' value='localhost:50051']
[name='content-type' value='application/grpc']
[name='user-agent' value='grpc-go/1.43.0']
[name='te' value='trailers']
[name='grpc-timeout' value='9933133n']

基于 uprobe 的跟踪程序用于生产环境需要进一步的考虑,你可以在脚注中阅读有关的内容。要尝试这个演示,请查看这里的说明。

结论

由于 HPACK 头压缩算法,跟踪 HTTP/2 流量变得很困难。这篇文章演示了另一种捕获消息的方法,即用 eBPF uprobe 直接跟踪 HTTP/2 库中的适当函数。

重要的是要理解这种方法有利有弊。其主要优点是无论何时部署跟踪器,都可以跟踪消息。然而,一个显著的缺点是,这种方法是特定于一个单一的 HTTP/2 库(在这个例子中是 Golang 的库);对于其他库,这个练习必须重复进行,如果上游代码发生更改,则可能需要进行维护。在未来,我们正在考虑为库提供 USDT,这将给我们提供更稳定的跟踪点,并减轻 uprobe 的一些缺点。最后,我们的目标是优化一种开箱即用的方法,而不需考虑部署顺序,这就是导致我们采用基于根的 eBPF 方法的原因。

寻找演示代码?在这里[10]找到它。

问题吗?在Slack[11]或 Twitter 上 @pixie_run 找到我们。

脚注

  • 这个演示项目只跟踪 HTTP/2 头,而不是数据帧。要跟踪数据帧,你需要识别 Golang net/http2 库函数,该函数接受数据帧作为参数,并找出相关数据结构的内存布局。对于示例实现,请查看 Pixie 的代码[12]
  • uprobe BPF 代码内存布局是硬编码的。如果数据结构的内存布局在 Golang 版本之间发生改变,这段代码将会失灵。这可以通过查询与可执行文件捆绑在一起的 DWARF 信息来解决。对于示例实现,请查看 Pixie 的DWARF query API[13]
  • 现有的 BPF 代码依赖于 Golang 的基于堆栈的调用约定,这将在 Golang 1.17 和更新版本的基于寄存器的调用约定中失灵。Pixie 团队正在为此开发一个新的框架。有关更新,请关注这 GitHub 问题[14]

参考资料

[1]Wireshark: https://www.wireshark.org/

[2]头帧: https://datatracker.ietf.org/doc/html/rfc7540#section-6.2

[3]这里: https://github.com/pixie-io/pixie-demos/tree/main/http2-tracing#trace-http2-headers-with-wireshark

[4]HPACK: https://httpwg.org/specs/rfc7541.html

[5]比 HTTP 1.x 大大提高了效率: https://blog.cloudflare.com/hpack-the-silent-killer-feature-of-http-2/

[6]官方 RFC: https://httpwg.org/specs/rfc7541.html

[7]之前的一篇 eBPF 博文: https://blog.px.dev/ebpf-http-tracing/#tracing-with-uprobes

[8]2018: https://github.com/grpc/grpc-go/commits/master/internal/transport/controlbuf.go

[9]http2-tracing/uprobe_trace/bpf_program.go: https://github.com/pixie-io/pixie-demos/blob/main/http2-tracing/uprobe_trace/bpf_program.go

[10]这里: https://github.com/pixie-io/pixie-demos/tree/main/http2-tracing

[11]Slack: https://slackin.px.dev/

[12]代码: https://github.com/pixie-io/pixie/blob/78931cbbbad08578386fa864155f6d57a63d4d73/src/stirling/source_connectors/socket_tracer/bcc_bpf/go_http2_trace.c#L1026

[13]DWARF query API: https://github.com/pixie-io/pixie/blob/78931cbbbad08578386fa864155f6d57a63d4d73/src/stirling/source_connectors/socket_tracer/uprobe_symaddrs.cc#L171

[14]这 GitHub 问题: https://github.com/pixie-io/pixie/issues/335

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-01-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 CNCF 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么情况下 Wireshark 无法解码 HTTP/2 头?
  • HPACK: Wireshark 的祸根
  • 基于 Uprobe 的 HTTP/2 跟踪
  • 结论
  • 脚注
    • 参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档