当前位置: 首页 > 科技观察

使用eBPF观察HTTP_0

时间:2023-03-14 17:41:44 科技观察

的详解作者|少轩前言随着eBPF的推出,由于其高性能、高扩展性、安全性等优势,被广泛应用于网络、安全、可观察性等领域,同时诞生了众多优秀的开源项目,如Cilium、Pixie等,而iLogtail是阿里巴巴内外数千万实例的可观察数据收集器,eBPF网络可观察特性也有望在明年8月发布。下面主要从eBPF观察HTTP1、HTTP1.1和HTTP2的角度介绍eBPF在可观察场景的应用,回顾HTTP协议本身的发展历程。eBPF基本介绍eBPF是近年来LinuxNetworking中比较流行的技术之一。它广泛应用于安全、网络和可观察性。集群规模下iptables性能急剧下降的问题。在基本功能方面,eBPF提供了一种新的方式来定制内核态和用户态之间的交互,兼顾性能和灵活性。具体表现为eBPF提供了友好的API,使得依赖libbpf、bcc等SDK成为可能。定义业务逻辑的安全嵌入式内核态执行,通过BPFMap机制(无需多副本)在内核态和用户态之间直接传递所需数据。关注可观察性时,我们可以将eBPF与Javaagent进行对比介绍。Javaagent的基本功能是在程序启动时将代理字节码编织到已有的字节码中,从而为用户程序自动添加挂钩点,而不需要修改代码,比如在函数进入和返回时添加挂钩点可以计算出这个函数的耗时。与eBPF类似,它提供了一系列内核态执行的入口函数,无需修改代码即可观察到应用程序的内部状态。以下入口点类型通常用于可观察性:kprobe:动态附加到内核调用点函数,例如,要在内核exec系统调用之前检查参数,可以在中设置SEC("kprobe/sys_exec")标头BPF程序切入。tracepoints:内核提供了一些很好的切入点,可以理解为静态kprobes,比如syscall的connect函数。uprobe:对应krobe,动态附加到用户态调用函数的入口点称为uprobe。相比于kprobe核函数的稳定性,uprobe的函数是由开发者定义的。当开发者修改函数签名时,uprobeBPF程序也需要修改函数切入点签名。perf_events:将BPF代码附加到Perf事件上,可用于性能分析。TCP和eBPF由于本文观察到的协议HTTP1、HTTP1.1、HTTP2都是基于TCP模型的,我们先回顾一下与TCP建立连接的过程。首先,客户端通过三次握手建立通信。从TCP协议的角度来看,连接代表的是状态信息,比如包括seq、ack、window/buffer等,而tcp握手就是协商这些初始值;而从操作系统的角度来看,也就是说,连接建立后,TCP在INET域中创建了一个socket,同时也占用了FD资源。对于四次挥手,从TCP协议的角度,可以理解为终止信号的释放,保持状态的释放;而从操作系统的角度来看,四次挥手也意味着SocketFD资源的回收。从应用层的角度来看,还有一个常用的概念,就是长连接,但是对于TCP传输层来说,长连接只是使用方式的区别:应用层短连接:三次握手+单次传输数据+挥手四次,代表协议HTTP1应用层长连接:三次握手+多次数据传输+挥手四次,代表协议HTTP1.1,HTTP2内核的调用参考下图TCP连接建立过程的函数,可以方便地用于eBPF程序定义tracepoints/kprobe入口点。例如,建立连接的过程可以切入accept和connect函数,释放连接的过程可以切入close过程,传输数据的过程可以切入read或write函数。基于TCP,大部分入口点都被静态化为tracepoints,所以BPF程序定义了如下入口点来覆盖上述TCP核心功能(sys_enter表示进入时entry,sys_exit表示返回时entry)。SEC(“tracepoint/syscalls/sys_enter_connect”)SEC(“tracepoint/syscalls/sys_exit_connect”)SEC(“tracepoint/syscalls/sys_enter_accept”)SEC(“tracepoint/syscalls/sys_exit_accept”)SEC(“tracepoint/syscalls/sys_enter_accept4”)SEC(“tracepoint/syscalls/sys_exit_accept4”)SEC(“tracepoint/syscalls/sys_enter_close”)SEC(“tracepoint/syscalls/sys_exit_close”)SEC(“tracepoint/syscalls/sys_enter_write”)SEC(“tracepoint/syscalls/sys_exit_write”)SEC(“tracepoint/syscalls/sys_enter_read”)SEC(“tracepoint/syscalls/sys_exit_read”)SEC(“tracepoint/syscalls/sys_enter_sendmsg”)SEC(“tracepoint/syscalls/sys_exit_sendmsg”)SEC(“tracepoint/syscalls/sys_enter_recvmsg”)SEC("tracepoint/syscalls/sys_exit_recvmsg")....结合以上概念,我们以iLogtail的eBPF工作模型为例,介绍一个eB??PF程序在observable域中实际是如何工作的。更详细的可以参考这篇分享:基于eBPF的应用可观察技术实践。如下图所示,iLogtaileBPF程序的工作空间分为KernelSpace和UserSpace。KernelSpace主要负责数据的抓取和预处理:capture:Hook模块会根据KProbe定义拦截网络数据,虚线部分是KProbe拦截的具体内核函数(使用上面介绍的SEC定义),比如connect,accept和write等。预处理:预处理模块会根据用户态配置拦截和丢弃数据,推断数据协议。只有满足要求的数据才会被传递给SendToUserSpace模块,其他数据将被丢弃。之后,SendToUserSpace模块将过滤后的数据从内核态数据通过eBPFMap传输到用户态。UserSpace模块主要负责数据的分析、聚合和管理:Analysis:Process模块会不断处理存储在eBPFMap中的网络数据。首先,因为Kernel已经推断出协议类型,Process模块??会根据这个类型进行细粒度的协议分析,比如分析MySQL协议的SQL,分析HTTP协议的状态码等。其次,由于Kernel传递的连接元数据信息只是Pid、FD等进程粒度元数据信息,而对于Kubernetes可观察场景,Pod、Container等资源定义更有意义,因此CorrelateMeta模块会为Process处理处理后的数据与绑定容器相关的元数据信息。聚合:元数据信息绑定后,聚合模块将数据进行聚合,避免数据重复传输。比如某个SQL在聚合期间被调用了1000次,聚合模块会将最终的数据抽象成XSQL:1000格式并上传。管理:整个eBPF程序与大量的进程和连接数据进行交互,因此eBPF程序中对象的生命周期需要与机器的实际状态保持一致。当进程或链接被释放时,相应的对象也需要被释放,这也对应了垃圾回收的ConnectionManagementandResponsibilities。eBPF数据分析HTTP1、HTTP1.1、HTTP2数据协议都是基于TCP的。参考上面,必然有如下函数调用:connect函数:函数签名为intconnect(intsockfd,conststructsockaddr*addr,socklen_taddrlen),可以得到使用的socket的fd和peer地址以及来自函数签名输入参数的其他信息。accept函数:函数签名为intaccept(intsockfd,structsockaddr*addr,socklen_t*addrlen),从函数签名中也可以得到所使用的socket的fd和peer的地址。sendmsg函数:函数签名为ssize_tsendmsg(intsockfd,conststructmsghdr*msg,intflags)。从函数签名可以看出,基于这个函数,可以获取发送的数据包和使用的socket的fd信息,但是不能直接根据入参知道peer地址。recvmsg函数:函数签名是ssize_trecvmsg(intsockfd,structmsghdr*msg,intflags)。从函数签名可以看出,基于这个函数,我们得到了接收到的数据包和使用的socket的fd信息,但是我们不能直接根据入参得知peer地址。close函数:函数签名是intclose(intfd)。从函数签名可以看出,基于这个函数,可以得到即将关闭的fd信息。HTTP1/HTTP1.1短连接模式HTTP于1996年推出。HTTP1是用户层的短连接模型,也就是说每次发送数据都会伴随connect、accept和close函数的调用。这被认为是eBPF程序可以很容易地找到connect的起点,将传输的数据与地址绑定,进而构建服务的上下游调用关系。可以看出,HTTP1或者HTTP1.1短连接模式对于eBPF来说是一个非常友好的协议,因为地址信息和数据信息可以很容易的关联起来,但是回到HTTP1/HTTP1.1短连接模式本身,'友好的'cost'不仅仅是指每次TCP连接的消耗和连接释放,如果两次数据传输的HTTPHeader相同,Header也存在冗余传输的问题,比如headerHost,Accept等字段以下数据。HTTP1.1长连接HTTP1.1在HTTP1.0(1997)一年后发布,提供缓存处理、带宽优化、错误通知管理、主机头处理和长连接等特性。长连接的引入也部分解决了上述HTTP1中每次数据传输需要三次握手和四次握手的过程,提高了数据传输的效率。但是对于使用eBPF来观察HTTP数据,也带来了新的问题。上面提到,地址和数据绑定的建立依赖于connect时的probe,通过connect参数获取数据地址,从而与后续的数据通信。包绑定。但是回到长连接的情况,如果connect是在1小时前建立的,此时启动了eBPF程序,那么我们只能检测到packet函数的调用,比如send或者recv函数。这时候地址和数据的关系应该怎么建立呢?首先我们可以回到检测函数的定义,可以发现此时虽然没有明确的地址信息,但是可以知道这个TCP报文使用的Socket和FD信息。因此,可以使用netlink获取这个Socket的meta信息,为长连接补充peer地址,然后在HTTP1.1长连接协议中构建服务拓扑,分析数据细节。ssize_tsendmsg(intsockfd,conststructmsghdr*msg,intflags)ssize_trecvmsg(intsockfd,structmsghdr*msg,intflags)HTTP2HTTP1.1发布后,由于冗余传输和传输模型串行问题,RPC框架基本私有化了协议定义,比如Dubbo等2015年,HTTP2的发布打破了以往对HTTP协议的诸多诟病。除了解决上文提到的Header冗余传输问题外,还解决了TCP连接数限制、传输效率、队头拥堵等问题。gRPC正式构建了基于HTTP2的高性能RPC框架,也让HTTP1时代层出不穷的通信协议逐渐走向统一时代。例如,Dubbo3完全兼容gRPC/HTTP2协议。Features下面首先介绍HTTP2的一些与eBPFobservability相关的关键特性。多路复用HTTP1是一种同步且排他的协议。客户端发送消息并等待服务器响应,然后再发送新信息。这种方式浪费了TCP全双工方式的特点。因此,HTTP2允许在单个连接上执行多个请求,并且每个请求相应地使用不同的流。通过二进制分帧层,为每个帧分配一个专用的流标识符,当接收方接收到信息时,可以将接收方的帧重新组合成完整的消息,提高数据吞吐量。另外可以看出,由于引入了Stream,Header和Data也是分开设计的。每次传输数据后,Heaer帧作为后续Data帧的统一头发送,进一步提高了传输效率。标头压缩HTTP标头用于发送与请求和响应相关的附加信息。HTTP2引入了头部压缩的概念,使用了不同于文本压缩的技术,支持跨请求压缩头部,可以避免文本压缩算法的安全问题。HTTP2采用基于查找表和哈夫曼编码的压缩方式,使用预定义的静态表和在会话期间创建的动态表。不引用索引表的头可以使用ASCII编码或霍夫曼编码传输。但是随着性能的提升,也意味着越来越多的数据被避免传输,这也意味着eBPF程序能够感知到的数据会越来越少,所以HTTP2协议的可观察性也带来了新的问题.下面我们通过gRPC和Wireshark的不同模式来分析HTTP2协议对eBPF程序的可观察性的挑战。GRPCSimpleRPCSimpleRPC是GRPC最简单的通信方式。请求和响应都是二进制消息。如果保持连接,可以类比HTTP1.1的长连接模式。每次发送响应时,都会再次发送数据。但是,与HTTP1的不同之处在于引入了标头压缩。如果保持长连接状态,则后续数据包的头部信息将只存储索引值,而不是原始值。我们可以在下图中看到Wirshark抓取到的数据包。第一次传输包含完整的Header帧数据,而后续的Heders帧长度减少为15,减少了大量重复数据的传输。Stream模式Stream模式是gRPC常用的模式,包括Server-sidestreamingRPC、Client-sidestreamingRPC、BidirectionalstreamingRPC,在传输编码上与SimpleRPC模式没有区别,分为Headerframe,Dataframe等。但区别在于Dataframe的个数,SimpleRPC在一次发送或响应中只包含一种Dataframe模式,而Stream模式可以包含多个。1.Server-sidestreamingRPC:与SimpleRPC模式不同,在Server-sidestreamingRPC中,当收到来自客户端的请求时,服务器发回一系列响应。此响应消息序列在客户端发起的同一HTTP流中发送。如下图所示,服务端收到客户端的一条消息,在帧消息中发送多条响应消息。最后,服务器通过发送带有调用状态详细信息的尾部元数据来结束流。2、客户端流式RPC:客户端流式RPC模式下,客户端向服务器发送多条消息,服务器只返回一条消息。3.双向流式RPC:客户端和服务端都向对方发送消息流。客户端通过发送标头帧来设置HTTP流。一旦建立连接,客户端和服务器就可以同时发送消息,而无需等待对方完成。tracepoint/kprobe的挑战从上面的wireshark报文和协议模式可以看出,在HTTP1时代使用的tracepoint/kprobe在历史上会有以下挑战:Stream模式:比如在Server-sidestream下,如果tracepoint/kprobe检测到的点是Dataframe,因为Dataframe不能和Headerframe关联起来,会变成无效的Dataframe,但是对于gRPC的使用场景是可以的。一般RPC发送数据和接收数据的速度都非常快,所以很快就会有一个新的Header帧被接收,但是这时候就会遇到一个更大的挑战,长连接下的header压缩。长连接+头压缩:当HTTP2保持长连接时,connect后第一个Stream传输的Header将是完整的数据,如果后续的Header帧与前一个Header帧具有相同的Header字段,则数据传输将是地址信息。真实的数据信息将交由服务端或客户端的应用层SDK进行维护。如下图,eBPFtracepoints/kprobe只在stream1的结束帧进行探测,后续的Header2帧很有可能没有完整的Header元数据如下图Wireshark截图所示,长度包含大量Header信息的Header只有15个,可见eBPFtracepoints/kprobe很难处理这种情况。从上面可以看出,HTTP2可以归结为有状态的协议,Tracepoint/Kprobe很难处理有状态的协议数据。在某些场景下,它只能被降级。下面是使用Tracepoint/Kprobe处理的基本流程。Uprobe会起作用吗?从上面的tracepoint/kprobe挑战,我们可以看出HTTP2是一个很难观察的协议。在HTTP2协议规范中,为了减少Header的传输,客户端和服务端都需要维护Header数据,如下图是grpc实现的HTTP2客户端维护headermeta信息的截图,所以可以在应用层获取完整的头部数据,绕过了头部压缩的问题。对于应用层协议,eBPF提供的检测方式是Uprobe(用户态),Pixie项目也是基于Uprobe来实践gRPCHTTP2流量的检测。详情请参考本文[1]。下图展示了使用Uprobe观察GogRPC流量的基本流程。比如writeHeader的函数定义为func(l*loopyWriter)writeHeader(streamIDuint32,endStreambool,hf[]hpack.HeaderField,onWritefunc()),可以看到显式的Header文本。Kprobe和Uprobe对比从上面可以看出Uprobe实现简单,不存在数据退化的问题,但是Uprobe真的完美吗?兼容性:以上方案只是基于GolanggRPC的具体方法进行检测,也就是说以上观察只能覆盖GolanggRPC流量,其他GolangHTTP2库不支持。多语言:Uprobe只能基于方法签名进行检测,更适合C/GO等纯编译语言。对于Java等JVM语言,由于符号表是在运行时动态生成的,虽然可以依赖一些javaagents将java程序转换为Uprobe使用,但与纯编译语言相比,用户成本或修改成本还是会更高。稳定性:与tracepoint/kprobe相比,Uprobe不稳定。如果检测到的函数的函数签名发生变化,则意味着Uprobe程序将无法运行,因为函数注册表的变化会使Uprobe无法找到入口点。两种方案的对比如下。可以看出,这两种方案对于HTTP2(有状态)观察有一些权衡:方法稳定性、多语言兼容性和易于数据完整性。kprobe/tracepoint强、强、复杂,存在数据降级。UprobeWeakweakweak简单完整的总结以上我们回顾了从HTTP1到HTTP2时代的协议变迁,也看到了HTTP2为提升传输效率所做的种种努力,而正是HTTP2巨大的效率提升也让gRPC选择构建直接基于HTTP2协议,也正是这样的选择,使得gRPC在RPC的争夺之后成为了一个无形的事实协议。但我们也看到,协议的进步意味着数据交互变少,观察数据变得更加困难。例如HTTP2使用eBPF没有完美的解决方案,或者使用Kprobe观察、选择多语言、流量拓扑分析,但允许丢失流量细节的风险;或者使用Uprobe观察,选择数据的细节,拓扑结构,但允许多语言兼容性问题。iLogtail致力于打造一个涵盖Trace、Metrics、Logging可观察性的统一代理。eBPF作为可观察领域流行的采集技术,提供了无侵入、安全、高效的流量观察能力。预计在8月份,我们会在iLogtailCpp正式开源后,发布这部分功能。欢迎大家关注,相互交流。参考:TCP的几种状态:https://www.s0nnet.com/archives/tcp-statusHTTP2.0的总结:https://liyaoli.com/2015-04-18/HTTP-2.0.htmlTransmissionControlProtocol:https:///en.wikipedia.org/wiki/Transmission_Control_ProtocolComputerNetworks:https://www.cse.iitk.ac.in/users/dheeraj/cs425/lec18.htmlHypertext_Transfer_Protocol:https://en.wikipedia.org/wiki/Hypertext_Transfer_ProtocolgRPC:ADeep深入了解通信模式:https://thenewstack.io/grpc-a-deep-dive-into-the-communication-pattern/ebpf2-http2-tracing:https://blog.px.dev/ebpf-http2-tracing/深入理解Linuxsocket:https://www.modb.pro/db/153725基于eBPF的ApplicationObservable技术实践:https://www.bilibili.com/video/BV1Gg411d7tq[1]:https://blog.px.dev/ebpf-http2-tracing/