佳能|一文详解用 eBPF 观测 HTTP( 三 )


eBPF 数据解析 HTTP 1 、HTTP1.1以及HTTP2 数据协议都是基于TCP的 , 参考上文 , 一定有以下函数调用:

【佳能|一文详解用 eBPF 观测 HTTP】

































































connect 函数:函数签名为int connect(int sockfd const struct sockaddr *addr socklen_t addrlen) 从函数签名入参可以获取使用的socket 的fd , 以及对端地址等信息 。accept 函数:函数签名为int accept(int sockfd struct sockaddr addr socklen_t addrlen) 从函数签名入参同样可以获取使用的socket 的fd , 以及对端地址等信息 。sendmsg函数:函数签名为 ssize_t sendmsg(int sockfd const struct msghdr *msg int flags)从函数签名可以看出 , 基于此函数可以拿到发送的数据包 , 以及使用的socket 的fd信息 , 但无法直接基于入参知晓对端地址 。recvmsg函数:函数签名为 ssize_t recvmsg(int sockfd struct msghdr *msg int flags)从函数签名可以看出 , 基于此函数我们拿到接收的数据包 , 以及使用的socket 的fd信息 , 但无法直接基于入参知晓对端地址 。close 函数:函数签名为 int close(int fd)从函数签名可以看出 , 基于此函数可以拿到即将关闭的fd信息 。HTTP 1 / HTTP 1.1 短连接模式 HTTP 于1996年推出 , HTTP 1 在用户层是短连接模型 , 也就意味着每一次发送数据 , 都会伴随着connect、accept以及close 函数的调用 , 这就以为这eBPF程序可以很容易的寻找到connect 的起始点 , 将传输数据与地址进行绑定 , 进而构建服务的上下游调用关系 。可以看出HTTP 1 或者HTTP1.1 短连接模式是对于eBPF 是非常友好的协议 , 因为可以轻松的关联地址信息与数据信息 , 但回到HTTP 1/HTTP1.1 短连接模式 本身来说 , ‘友好的代价’不仅意味着带来每次TCP 连接与释放连接的消耗 , 如果两次传输数据的HTTP Header 头相同 , Header 头也存在冗余传输问题 , 比如下列数据的头Host、Accept 等字段 。HTTP 1.1 长连接 HTTP 1.1 于HTTP 1.0 发布的一年后发布(1997年) , 提供了缓存处理、带宽优化、错误通知管理、host头处理以及长连接等特性 。 而长连接的引入也部分解决了上述HTTP1中每次发送数据都需要经过三次握手以及四次挥手的过程 , 提升了数据的发送效率 。 但对于使用eBPF 观察HTTP数据来说 , 也带来了新的问题 , 上文提到建立地址与数据的绑定依赖于在connect 时进行probe , 通过connect 参数拿到数据地址 , 从而与后续的数据包绑定 。 但回到长连接情况 , 假如connect 于1小时之前建立 , 而此时才启动eBPF程序 , 所以我们只能探测到数据包函数的调用 , 如send或recv函数 。 此时应该如何建立地址与数据的关系呢? 首先可以回到探测函数的定义 , 可以发现此时虽然没有明确的地址信息 , 但是可以知道此TCP 报文使用的Socket 与FD 信息 。 因此可以使用 netlink 获取此Socket 的元信息 , 进行对长连接补充对端地址 , 进而在HTTP 1.1 长连接协议构建服务拓扑与分析数据明细 。ssize_t sendmsg(int sockfd const struct msghdr msg int flags) ssize_t recvmsg(int sockfd struct msghdr msg int flags) HTTP 2 在HTTP 1.1 发布后 , 由于冗余传输以及传输模型串行等问题 , RPC 框架基本上都是进行了私有化协议定义 , 如Dubbo 等 。 而在2015年 , HTTP2 的发布打破了以往对HTTP 协议的很多诟病 , 除解决在上述我们提到的Header 头冗余传输问题 , 还解决TCP连接数限制、传输效率、队头拥塞等问题 , 而 gRPC正式基于HTTP2 构建了高性能RPC 框架 , 也让HTTP 1 时代层出不穷的通信协议 , 也逐渐走向了归一时代 , 比如Dubbo3 全面兼容gRPC/HTTP2 协议 。特性 以下内容首先介绍一些HTTP2 与eBPF 可观察性相关的关键特性 。多路复用 HTTP 1 是一种同步、独占的协议 , 客户端发送消息 , 等待服务端响应后 , 才进行新的信息发送 , 这种模式浪费了TCP 全双工模式的特性 。 因此HTTP2 允许在单个连接上执行多个请求 , 每个请求相应使用不同的流 , 通过二进制分帧层 , 为每个帧分配一个专属的stream 标识符 , 而当接收方收到信息时 , 接收方可以将帧重组为完整消息 , 提升了数据的吞吐 。 此外可以看到由于Stream 的引入 , Header 与Data 也进行了分离设计 , 每次传输数据Heaer 帧发送后为此后Data帧的统一头部 , 进一步提示了传输效率 。首部压缩 HTTP 首部用于发送与请求和响应相关的额外信息 , HTTP2引入首部压缩概念 , 使用与正文压缩不同的技术 , 支持跨请求压缩首部 , 可以避免正文压缩使用算法的安全问题 。 HTTP2采用了基于查询表和Huffman编码的压缩方式 , 使用由预先定义的静态表和会话过程中创建的动态表 , 没有引用索引表的首部可以使用ASCII编码或者Huffman编码传输 。但随着性能的提升 , 也意味着越来越多的数据避免传输 , 这也同时意味着对eBPF 程序可感知的数据会更少 , 因此HTTP2协议的可观察性也带来了新的问题 , 以下我们使用gRPC不同模式以及Wireshark 分析HTTP2协议对eBPF 程序可观测性的挑战 。GRPC Simple RPC Simple RPC 是GRPC 最简单的通信模式 , 请求和响应都是一条二进制消息 , 如果保持连接可以类比为HTTP 1.1 的长连接模式 , 每次发送收到响应 , 之后再继续发送数据 。但与HTTP 1 不同的是首部压缩的引入 , 如果维持长连接状态 , 后续发的数据包Header 信息将只存储索引值 , 而不是原始值 , 我们可以看到下图为Wirshark 抓取的数据包 , 首次发送是包含完整Header帧数据 , 而后续Heders 帧长度降低为15 , 减少了大量重复数据的传输 。Stream 模式 Stream 模式是gRPC 常用的模式 , 包含Server-side streaming RPC , Client-side streaming RPC , Bidirectional streaming RPC , 从传输编码上来说与Simple RPC 模式没有不同 , 都分为Header 帧、Data帧等 。 但不同的在于Data 帧的数量 , Simple RPC 一次发送或响应只包含一个Data帧 模式 , 而Stream 模式可以包含多个 。1、Server-side streaming RPC:与Simple RPC 模式不同 , 在Server-side streaming RPC 中 , 当从客户端接收到请求时 , 服务器会发回一系列响应 。 此响应消息序列在客户端发起的同一 HTTP 流中发送 。 如下图所示 , 服务器收到来自客户端的消息 , 并以帧消息的形式发送多个响应消息 。 最后 , 服务器通过发送带有呼叫状态详细信息的尾随元数据来结束流 。2、Client-side streaming RPC: 在客户端流式 RPC 模式中 , 客户端向服务器发送多条消息 , 而服务器只返回一条消息 。3、Bidirectional streaming RPC:客户端和服务器都向对方发送消息流 。 客户端通过发送标头帧来设置 HTTP 流 。 建立连接后 , 客户端和服务器都可以同时发送消息 , 而无需等待对方完成 。tracepoint/kprobe的挑战 从上述wirshark 报文以及协议模式可以看出 , 历史针对HTTP1时代使用的tracepoint/kprobe 会存在以下挑战: Stream 模式: 比如在Server-side stream 下 , 假如tracepoint/kprobe 探测的点为Data帧 , 因Data 帧因为无法关联Header 帧 , 都将变成无效Data 帧 , 但对于gRPC 使用场景来说还好 , 一般RPC 发送数据和接受数据都很快 , 所以很快就会有新的Header 帧收到 , 但这时会遇到更大的挑战 , 长连接下的首部压缩 。长连接+首部压缩:当HTTP2 保持长连接 , connect 后的第一个Stream 传输的Header 会为完整数据 , 而后续Header帧如与前置Header帧存在相同Header 字段 , 则数据传输的为地址信息 , 而真正的数据信息会交给Server 或Client 端的应用层SDK 进行维护 , 而如下图eBPF tracepoints/kprobe 在stream 1 的尾部帧才进行probe , 对于后续的Header2 帧大概率不会存在完整的Header 元数据 , 如下图Wireshark 截图 , 包含了很多Header 信息的Header 长度仅仅为15 , 可以看出eBPF tracepoints/kprobe 对于这种情况很难处理 。从上文可知 , HTTP2 可以归属于有状态的协议 , 而Tracepoint/Kprobe 对有状态的协议数据很难处理完善 , 某些场景下只能做到退化处理 , 以下为使用Tracepoint/Kprobe 处理的基本流程 。Uprobe 可行吗? 从上述tracepoint/kprobe 的挑战可以看到 , HTTP 2 是一种很难被观测的协议 , 在HTTP2 的协议规范上 , 为减少Header 的传输 , client 端以及server 端都需要维护Header 的数据 , 下图是grpc 实现的HTTP2 客户端维护Header 元信息的截图 , 所以在应用层可以做到拿到完整Header数据 , 也就绕过来首部压缩问题 , 而针对应用层协议 , eBPF 提供的探测手段是Uprobe(用户态) , 而Pixie 项目也正是基于Uprobe 实践了gRPC HTTP2 流量的探测 , 详细内容可以参考此文章[1