实时流量回放工具 TCPCopy 原理浅析

一、简述

随着互联网技术的发展,服务端的架构愈加复杂,仅依赖 QA 个人的经验很难覆盖到全面的业务场景,真实的线上流量对于服务端的测试十分必要。TCPCopy 就是一款开源,并广泛应用于各大公司的流量回放工具。相信有很多人都在自己的项目中使用 TCPCopy 进行测试,但是可能对其底层原理不甚了解。本文通过对 TCPCopy 的原理作较为简单的介绍,希望对读者有所帮助。

二、架构

TCPCopy 的架构历经几次升级,本文介绍最新的 1.0 版本架构。 如图所示,TCPCopy 由两部分组成: tcpcopy 和 intercept。tcpcopy 在线上服务器上运行并捕获在线 tcp 类型的请求数据包,修改 TCP/IP 头部信息,发送给测试服务器,巧妙地达到 “欺骗"测试服务器的目的。intercept 在辅助服务器上运行并执行一些辅助工作,例如将响应信息传递给 tcpcopy。简略的交互流程如下:
①. tcpcopy 在线上服务器抓包,解析出 ip 报文段
②. tcpcopy 修改 ip 头和 tcp 头,伪造源 ip 和端口,发包给测试机,伪造的 ip 地址参考启动时的-x 和-c 参数
③. 测试机接收请求,返回结果包,目前 ip 为 tcpcopy 伪造的 ip 和端口
④. 结果包被路由到 intercept 机器,被 intercept 捕获,intercept 解析出 ip 头和 tcp 头,返回空响应数据给 tcpcopy
⑤. tcpcopy 接收到返回数据

三、技术原理

tcpcopy 的工作模式分为在线和离线工作方式,在线方式主要用来实时捕获在线请求数据包,离线方式主要从 pcap 格式的文件中读取在线请求数据包。虽然工作方式不同,但其核心原理是相同的,下文主要分几部分介绍 tcpcopy 的核心原理 。

1.抓包与发包
tcpcopy 的核心功能可以简单概括为 “抓包” 和 “发包”,那我们就先从 “抓包” 开始谈起。 如何抓取服务器的真实流量,相信很多小伙伴和我一样,一开始面对这个问题是一头雾水,其实 linux 操作系统已经为我们提供了相关的功能,只需要我们对 linux 的高级网络编程进行了解。tcpcopy 关于抓包和发包的代码初始化都在 tcpcopy/src/communication/tc_socket.c 中,下面介绍 tcpcopy 抓包的两种方式:

raw socket

raw socket,即原始套接字,可以接收本机网卡上的数据帧或者数据包,对于监听网络的流量和分析是很有作用 。tcpcopy 中关于初始化抓包 raw socket 的代码如下图,可以看出这种方式可以支持数据链路层和 ip 层的抓包

初始化发包 raw_socket 的代码如下图,首先创建 ip 层的 raw socket 套接字,并告诉协议栈 ip 层不再追加 ip 头

构建完整的 ip 数据报,发送给对方
● 其中 dst_addr 填充目的 ip
● ip 头填充源 ip,目的 ip 等
● tcp 头填充源端口,目的端口等

pcap

pcap 是操作系统提供的一个用于捕获网络流量的应用程序接口(API),其名称来源于 “抓包”(packet capture)。linux 系统通过 libpcap 实现了 pcap,大多数流量抓取工具如 tcpdump、goreplay 等也都是使用 libpacp 进行抓包。在 windows 系统中提供了 winpcap 库实现了 pcap。

初始化发包 pcap 的代码如下图

raw socket VS pcap

既然 tcpcopy 提供了两种方式,那么哪一种方式更好呢?
对于抓包,我们关心的只是我们想要的包,如果抓包方式设置不当,系统内核就会把多余的包都抓取到,如果流量压力越大,一般丢包会越多。经过长时间测试,利用 pcap 接口抓请求数据包的方式,平均丢包率高于 raw socket 抓请求数据包的方式。tcpcopy 也默认采用了 raw socket 抓包,当然也可以采用 pcap 接口(--enable-pcap)。
对于发包,tcpcopy 默认采用了 raw socket output 接口,也可以通过 pcap_inject(--enable-dlinject)来发包,具体使用哪种方式可以在自己的实际环境中测试其性能表现来作选择。

2.tcp 协议栈
我们知道 tcp 协议是有状态的协议,虽然上面介绍了发包的原理,但是如果不建立起来一个真正的 tcp 连接,我们发送的包也无法真正被测试服务接收。在日常的网络编程中,我们可以使用操作系统封装好 tcpsocket 接口进行编程,无需对于 tcp 状态太过关心,但在 tcpcopy 要修改数据包源 ip 及目的 ip,达到欺骗测试服务的目的,那么操作系统提供的 API 也就不满足要求了。所以,tcpcopy 实现了一个模拟的 tcp 有效状态机,这也是 tcpcopy 源码中最为复杂,最具难度的地方,具体的代码在 tcpcopy/src/tcpcopy/tc_session.c 中

tcpcopy 中定义了一个 session,用来维护不同的连接信息,对于抓的不同数据包,会做不同的处理:
–Syn 包,新的连接请求,分配源 ip,修改目的 ip, port,发送给测试机,同时新建一个 session,保存此次会话中的所有状态。
–Ack 包
● 纯 ack 包,不发送
● 有 payload(说明是具体请求),找到 session,发送给测试机。如果本 session 正在等待上次请求的响应,那么暂缓发送。
–Rst 包,如果本次会话正在等待测试机结果返回,就不发送,否则发送
–Fin 包,如果本次会话正在等待测试机结果返回,就等待,否则发送

3.路由
tcpcopy 发出请求数据包后,其路途可能不会是 “一帆风顺” 的。
● 该请求包的 ip 已经是伪造的 ip,而非 tcpcopy 所在机器的 ip,所以如果有些机器设置了 rpfilter(反向过滤技术),将会判断源 IP 地址是否是所信任的 IP 地址,如果不是,将会在 IP 层丢弃掉该数据包。
● 如果测试服务接收到了请求数据包,那么返回的数据包会返回给伪造的 ip 地址,也就是 intercept 机器所在的 ip,这个时候如果没有填写 intercept 机器的真实 ip 地址,那么就需要路由的配置了,如果没有配置正确的路由,响应包没有被 intercept 捕获,仍然无法完成一次完整的数据交互。
● intercept 捕获到响应包后,会取出 ip 数据报,将具体的数据丢弃,只返回给 tcpcopy 响应头等必要信息,减少对 tcpcopy 所在机器的网络影响。

4.intercept
刚开始接触 tcpcopy 的同学可能有和我一样的困惑,有了 tcpcopy,为什么还需要一个 intercept 呢?intercept 看起来好像十分多余,实际上却承载了重要的工作。intercept 可以理解为 tcpcopy 的服务端,其名字本身就解释了它的作用:“拦截器”。那么什么东西需要 intercept 来拦截呢?答案是测试服务的响应数据。
设想一下,如果没有 intercept,测试服务的响应数据只能返回给 tcpcopy,而 tcpcopy 是部署在线上的,也就是说测试服务会直接把响应数据全部返回给线上服务器,这毫无疑问会大大增加线上服务器的网络负载,甚至对在线的服务本身造成影响。有了 intercept 后,通过对源 ip 进行伪造,可以对测试服务进行协议栈的 “欺骗”,也就是让测试服务 “误以为” intercept 所在机器就是给它发请求的机器,而对于真实的线上机器根本 “一无所知”,这保证了线上环境不被测试环境所影响。
intercept 是一个单独的进程,其抓包方式默认采用 pcap 方式进行抓包,启动的时候需要传入-F 参数,如” tcp and src port 8080“,需要符合 libpcap 的过滤语法。也就是说 intercept 本身不与测试服务有连接,而且监听指定的端口,抓取测试服务的返回数据包,然后与 tcpcopy 进行交互。

5.性能
tcpcopy 采用了单进程单线程架构,基于 epoll/select 事件驱动,相关的代码都在 tcpcopy/src/event 中。默认在编译时使用了 epoll,也可以使用 select(--select),可以根据测试结果的性能差异选择不同的方式,理论上来说如果连接数较多的情况下,epoll 方式的性能会更好。
在实际的表现中,tcpcopy 的性能和流量的大小直接相关,也与 intercept 建立的连接数有关,单线程本身并不会成为性能的瓶颈(nginx 和 redis 都采用了单线程 +epoll 的方式,都能达到很大的并发)。因为 tcpcopy 与 intercept 直接建立连接,与测试机器不需要建立连接,也不需要耗费任何端口号,所以 tcpcopy 本身耗费的资源并不多,主要表现在对网络带宽的消耗。

四、结语

TCPCopy 是一款十分优秀的开源项目,但因为能力有限,本文只介绍了 TCPCopy 的核心技术原理,很多细节并未涉及。不过还是希望通过本文的介绍,能够对 TCPCopy 和流量回放技术感兴趣的同学有所启发!


↙↙↙阅读原文可查看相关链接,并与作者交流