测试 HTTP 服务,为了覆盖更多的场景,可以考虑录制线上流量,在测试环境进行重放。之前用 tcpcopy 比较多,最近遇到一些需求,需要在 HTTP 层做一些过滤,例如只录制指定 URL 的请求。
经过调研,发现 goreplay,其前称是 gor,很适合这个场景,有以下优点。
- 支持 HTTP 请求的录制和重放,可以在线上录制请求,在测试环境进行重放。
- 支持 HTTP 层面的流量过滤,可以只挑选我们感兴趣的流量。
- 支持请求放大,用于性能测试。
1. 用法
免 root 运行,抓包并不需要 root 权限,同样的方法适用于 tcpdump,其实 goreplay 和 tcpdump 一样,都用 libpcap 来抓包。
$ sudo setcap "cap_net_raw,cap_net_admin+eip" ./goreplay
抓取 80 端口的 HTTP 请求,只抓请求 URL 是 /api/v1 的,并输出到终端。这个比 tcpdump 更直观,打印到终端的是我们熟悉的 HTTP 协议。
第一行是 goreplay 自定义的 header,平常使用可以不必理会,不是实际抓到的包,从第二行开始才是实际抓到的包。
$ ./goreplay --input-raw :80 --http-allow-url '/api/v1' --output-stdout
抓取 80 端口的所有请求,并保存到文件。实际会分批保存为 request_0.gor,request_1.gor 这种文件名。
$ ./goreplay --input-raw :80 --output-file 'request.gor'
重放请求,例如 host2.com 是我们的新机房域名。这种重放,会根据请求的时间戳,按照抓取时的请求顺序重放。
例如抓取的时候,第一秒 10 个请求,第二秒 20 个请求,那么重放的时候,也会按照这个顺序。并且读完 request.gor 文件,就会停止。
上面我们看到了 goreplay 自定义的 header,其第三个字段,是一个纳秒级的时间戳,根据这个来保证重放的顺序和速率。
$ ./goreplay --input-file 'request.gor' --output-http 'http://host2.com'
如果是性能测试,可以不考虑请求的顺序和速率,并且要求无限循环。
# --input-file 从文件中获取请求数据,重放的时候 100x 倍速
# --input-file-loop 无限循环,而不是读完这个文件就停止
# --output-http 发送请求到 http://host2.com
# --output-http-workers 并发 100 发请求
# --stats --output-http-stats 每 5 秒输出一次 TPS 数据
$ ./goreplay --input-file 'request.gor|10000%' --input-file-loop --output-http 'http://host2.com' --output-http-workers 100 --stats --output-http-stats
更多的命令行参数及用法,可以查看 goreplay 源码的 settings.go 文件。
其输出的 stats 含义,可以看看 这个帖子 的解释。
2. 坑
使用过程中遇到的坑。
- 如果 HTTP 请求不符合规范,可能会抓不到包。遇到过 HTTP 请求头里面的 Content-Length 不等于实际的 Body 大小,goreplay 认为其请求未结束。
- input-file 是单 goroutine 在跑,会有性能瓶颈。测试的时候,读取的 RPS 在 1.7w - 1.8w 左右,如果压测需求大于这个,需要开多个进程同时跑。
3. 深入
goreplay 是用 golang 编写的,抓包的时候调用 gopacket,后者通过 cgo 来调用 libpcap。从编译开始,从源码层面学习一下其实现。
TL;DR
3.1 编译
由于 goreplay 使用了第三方 C 代码,不能使用 Go 的交叉编译功能来跨平台编译。只能在 Linux 下编译 Linux 使用的可执行文件。
在 Redhat 系发型版下,可以使用 yum 来安装依赖。
$ sudo yum install libpcap libpcap-devel
或者从源码编译 libpcap。
# 安装依赖
$ sudo yum install gcc flex byacc bison
$ wget http://www.tcpdump.org/release/libpcap-1.8.1.tar.gz && tar xzf libpcap-1.8.1.tar.gz
$ cd libpcap-1.8.1
$ ./configure
$ sudo make install
cd 到 goreplay 的源码目录,执行命令。
# 纯静态编译
$ go build -ldflags '-extldflags "-static"'
如果编译成功,在当前目录会生成一个 goreplay 文件,试运行一下。
[vagrant@localhost goreplay]$ ./goreplay
Version:
2018/06/08 08:06:22 Required at least 1 input and 1 output
不依赖任何库。
[vagrant@localhost goreplay]$ ldd goreplay
not a dynamic executable
一个合法的可执行文件。
[vagrant@localhost goreplay]$ file goreplay
goreplay: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=e334de401c00057ca56a33c0136dc5c86debee61, not stripped
如果遇到以下编译错误。
[vagrant@localhost goreplay]$ go build -ldflags '-extldflags "-static"'
# github.com/buger/goreplay
/home/vagrant/local/go/pkg/tool/linux_amd64/link: running gcc failed: exit status 1
/usr/bin/ld: cannot find -lpthread
/usr/bin/ld: cannot find -lc
collect2: error: ld returned 1 exit status
需要安装 glibc 的静态库。
$ sudo yum install glibc-static.x86_64
如果不是通过 go get 来获取 goreplay,而是通过 git clone 下来的,那么会缺少一些第三方库的依赖,通过 go get 命令补充就好。
$ go get github.com/Shopify/sarama
3.2 input 和 output
input 和 output 是 goreplay 对数据流的抽象,在源码目录有很多 input_xxx.go 和 output_xxx.go,实现了 goreplay 的核心功能。
在启动的时候,会解析命令行参数中指定的 input 和 output,接着启动 emitter,从 input 中读数据,写到 output 中。
emitter.go 中核心代码如下:
for _, in := range Plugins.Inputs {
go CopyMulty(in, Plugins.Outputs...)
}
多个 input 之间是并行的,但单个 input 到多个 output,是串行的。所有 input 都实现了 io.Reader 接口,output 都实现了 io.Writer 接口。所以阅读代码时,input 的入口是 Read() 方法,output 的入口是 Write() 方法。
3.3 UDP 抓包
抓包是核心功能,也算是一种 input 的类型。不过 goreplay 的实现中,实现上和 HTTP 协议绑定的很死。我参考 goreplay 的代码,实现了 goreplay-udp,用法上和 goreplay 保持一致。