HTTP SLI 调研 (3)
HTTP服务端READ和WRITE的对应
对于HTTPS,下面以OpenSSL加密库为例:
首先:
- 同一个ConnectionSocket内在时间上相邻的HTTP请求(read)与响应(write)是对应的
- (Socket的)文件描述符在同一个进程内部是不同线程共享的,在不同进程之间是隔离的
因此:
- 可以通过(进程ID,socket描述符)二元组决定是否为同一个socket
以OpenSSL为例,
加密的HTTP数据在解密后调用SSL_read函数从SSL/TLS连接读取到buffer中:
#include <openssl/ssl.h>
int SSL_read_ex(SSL *ssl, void *buf, size_t num, size_t *readbytes);
int SSL_read(SSL *ssl, void *buf, int num);
明文的HTTP数据在加密前调用SSL_write函数写入到SSL/TLS连接中:
#include <openssl/ssl.h>
int SSL_write(SSL *ssl, const void *buf, int num);
通过在SSL_read和SSL_write注入用户态探针,可以检查其是否HTTP相关的请求和响应。
- 如何检查是否为同一个进程? ebpf提供了bpf_get_current_pid_tgid函数来查看内核视角到PID(包括进程ID和线程ID)

- 如何检查是否为同一个socket? 注意到SSL_read和SSL_write函数都有SSL结构体指针,通过一下函数可以获得socket的描述符
#include <openssl/ssl.h> int SSL_get_fd(const SSL *ssl); int SSL_get_rfd(const SSL *ssl); int SSL_get_wfd(const SSL *ssl);
从WRITE到内核态的跟踪
TCP_ACK
在TCP层收到客户端对服务端发出的HTTP响应的ACK后,内核最终会调用tcp_ack函数。
如下,tcp_ack函数的第二个参数skb为来自客户端的ACK数据包。要确定这个tcp_ack是否对应我们服务器的HTTP响应,仅仅通过sock结构体来判断是不够的, 因为这个ACK可能是针对该TCP连接的其它数据包。如,重复的ACK。

但是tcp_ack函数最终会调用tcp_clean_rtx_queue,尝试将已经被ack的数据包从重传队列中删除。
在TCP层中包含数据的片段一经发送,片段的一份复制就放在名为重传队列的数据结构中,此时启动重传计时器。

tcp_clean_rtx_queue函数会遍历重传队列中的skb:

被ACK的skb最终会调用tcp_rtx_queue_unlink_and_free函数删除:

如果HTTP数据是明文传输的,我们可以考虑对这个tcp_rtx_queue_unlink_and_free函数注入内核态探针。通过skb我们可以在内核中tcp数据包,从而确定这个被删除的数据包是不是服务器发出的http响应。

上图为struct sk_buff结构体中用于存放部分数据包的线性数据区域。
但对于加密的HTTPS流量,上述方法行不通,因为skb中的数据是加密的。目前没有找到更好方法
Nginx源代码调研

通过阅读Nginx的源代码,对于http和https请求和响应可以观察到以上的函数调用过程。
两个函数都是在用户态下的单个线程下完成的,因此可以通过线程标识符判断是否对应。
再在tcp_rtx_queue_unlink_and_free注入内核态探针从而得到tcp处理时间和网络延迟。
第一阶段开发思路
- 在recv函数和writev函数处注入用户态探针,抓取HTTP请求和响应报文
- 通过线程ID及recv和writev函数调用的时序关系将HTTP请求与响应对应
- 在tcp_rtx_queue_unlink_and_free注入内核态探针抓包,对比writev发出的HTTP响应数据包,计算内核处理时间和网络延迟。
dev_queue_xmit
tcp_ack
tcp_rate_skb_delivered