使用云压测回放 GoReplay 录制的请求

柯开

腾讯云高级工程师,腾讯云压测 OTeam 发起人,目前主要负责腾讯云可观测系统的开发与设计。

GoReplay 简介

GoReplay 是一个开源的流量录制回放工具。主要用于捕获实时流量并将其复制到测试环境中。这样做可以帮助开发者和测试人员在不影响实际用户的情况下,对软件进行压力测试和问题排查。GoReplay 是用 Go 语言编写的,因此它非常高效且易于部署。

GoReplay 的工作原理是监听服务器的网络接口,捕获进出的 HTTP 流量,并选择性地重新发送这些请求到另一个服务器。这对于模拟真实用户行为、测试服务器的负载能力、查找并修复 bug、以及监控服务的稳定性都非常有用。

由于 GoReplay 本身并不提供一个分布式运行方案,只能在单机上运行。在流量录制完成后,受限于单机资源瓶颈,我们很难大规模的重放录制的流量,无法有效的模拟真实用户流量的压测行为以及极限测试。同时也缺乏压测时的监控图表,无法观测系统实时状况:比如请求成功率,响应延时,QPS 等。

腾讯云云压测是一款分布式性能测试服务,支持百万级别的高并发压测,可模拟海量用户的真实业务场景。因此我们可以引入云压测,使用云压测来回放 GoReplay 录制的真实流量。通过云压测我们可以在非常多的节点上回放用户流量并产生实时报告,帮助我们判断系统实时状况,找出性能瓶颈。

本文将通过一个实例演示:使用 GoReplay 录制网关接收到的请求,将请求各个字段保存成 CSV 文件。在云压测中,通过上传CSV 参数文件,指定期望的并发数,分布式回放请求到用户指定的地址。

常用 GoReplay 使用场景

  • 性能测试:通过复制生产环境的流量到测试环境,可以在不影响真实用户的情况下对应用程序进行压力测试和性能评估。
  • 故障排除和调试:当生产环境出现问题时,可以捕获相关的流量并在一个隔离的环境中重放,以便开发人员可以安全地调试问题而不会影响实际服务。
  • 回归测试:在发布新版本之前,可以使用 GoReplay 捕获的流量来验证更改是否会引入新的错误或性能问题。
  • A/B 测试:可以将流量同时发送到两个服务版本,比较它们的表现,以便做出数据驱动的决策。
  • 流量回放测试:通过在回放时候,加大回放请求的倍数,模拟高流量情况,可以帮助确定在不同负载下所需的资源量。

GoRepaly 流量录制原理

如果你用过 tcpdump, 你会发现 tcpdump 跟 GoReplay 使用场景很接近,都是监听网络流量,再做后续处理。

tcpdump 一般被用来捕获网络流量,并打印出来,用来排查疑难的网络问题。

比如监听目标端口8080的网络包,并写入到 mypackets.pcap 文件中:

代码语言:javascript
复制
sudo tcpdump dest port 8080 -w mypackets.pcap

GoReplay 流量录制也是监听指定端口流量,录制成 gor 文件(或者发送到其他目的端),方便后续回放:

代码语言:javascript
复制
sudo gor --input-raw :8080 --output-file requests.gor

tcpdump 和 GoReplay 都是依赖 BPF 组件来进行网络流量捕获。那么 BPF 是什么,它又是如何工作的呢?

BPF 全名为 BSD Packet Filter, 被广泛应用于网络监测。BPF 运行在内核态,根据用户定义的规则直接过滤收到的包,拷贝到用户态程序可以拿到的 buffer 中。

BPF 基本结构图如下:

BPF 主要包含两个组件:

  • the network tap:

tap 主要负责从指定的网络设备中拷贝数据包,并发送到监听的应用程序。由于每个数据包间隔时间很短,且都要经过 filter 过滤。tap 不可能对每个数据包都调用一次 read 系统,那样效率很低。tap 解决方案是一次读取多个数据包数据,并包一层 header, 用来区分各个数据包直接的边界,通过这种方式来加快数据处理效率。

  • the packet filter:

filter 主要就是用来过滤包。BPF 会根据不同的 filter 就地进行过滤,而不会拷贝到内核其他 buffer 中,整体处理相当高效。

我们使用的 GoReplay 和 tcpdump 都是基于 BPF 技术。

值得一提的是,Linux 内核现在默认使用 EBPF, EBPF 是 BPF 的增强版,提供更多的功能和灵活性。我们一般叫 BPF 称做 cBPF(classic bpf), 以与 EBPF 做区分。EBPF 可参考:https://ebpf.io/what-is-ebpf/

EBPF 完全向前兼容 BPF,用户无需关注底层实现上的变化。Linux 通过这种方式来确保 tcpdump, GoReplay 这些程序在新的 Linux 系统上也能正常运行。

对于 GoReplay 来说,在基于 EBPF 实现的系统上监听网络流量后台流程如下:

  1. filter 编译:GoReplay 使用编译器将 filter 表达式转换成 BPF bytecode。这些 bytecode 是一系列指令告知内核如何将包发送给 GoReplay;
  2. cBPF 翻译到 EBPF: 当 cBPF bytecode 加载进内核时,内核自动将 cBPF bytecode 转化成 EBPF bytecode;
  3. 执行:EBPF bytecode 将在 Verifier 层进行安全性校验, 在 JIT Compiler 进一步编译成机器码,当数据穿过网络堆栈时,被内核执行:过滤数据包并转发到 GoReplay。

使用云压测录制回放用户网关

本文以录制回放 Nginx 网关为例,其他所有类型的网关都可以按照相同的方式来录制请求,再使用云压测来回放用户请求。

1.环境准备

整个实验需要以下组件:

  1. Nginx 网关:Nginx 网关上有源源不断的用户请求,我们需要在 Nginx 网关录制下这些请求;
  2. GoReplay: 请求录制回放工具;
  3. CSV 生成服务:接收 HTTP 请求,将接收到的请求各个字段写入 CSV 文件中;
  4. 云压测:基于用户上传的 CSV 文件,回放用户录制的所有请求。

安装 GoReplay 到你的网关所在机器上

如果网关所在机器是 Linux 或 macOS,可以使用以下命令:

代码语言:javascript
复制
# 从官方GitHub仓库下载最新的二进制文件
curl -L https://github.com/buger/goreplay/releases/download/1.3.3/gor_1.3.3_x64.tar.gz | tar xz

# 将二进制文件移动到你的PATH目录中,例如/usr/local/bin
mv gor /usr/local/bin/

确保替换上的URL 中的版本号为最新的版本,仓库地址:https://github.com/buger/goreplay。

2.实验流程

2.1 将 Nginx 上的请求录制成 Gor 文件

本节参与组件(其他组件仅做完整场景展示):

  1. Nginx 网关:Nginx 网关上有源源不断的用户请求,我们需要在 Nginx 网关录制下这些请求;
  2. GoReplay: 请求录制工具。

整体架构图如下:

要开始录制流量,你需要在网关所在服务器上运行 GoReplay。以下是一个基本的命令示例,它会监听网关上的80端口,并将捕获的流量保存到一个文件中:

代码语言:javascript
复制
sudo gor --input-raw :80 --output-file requests.gor

这个命令会捕获所有通过端口80的流量,并将其保存到当前目录下的 requests.gor 文件中。请注意,你可能需要 sudo 权限来监听80端口。

2.2 将 Gor 文件转换成 CSV 参数文件

本节参与组件:

  1. GoReplay: 请求录制工具。本节使用 GoReplay 回放Gor 文件中记录的请求到 CSV 生成服务;
  2. CSV 生成服务:接收 HTTP 请求,将接收到的请求各个字段写入 CSV 文件中。

整体架构如下

在 CSV 文件中我们记录下请求各个字段, 比如 scheme, host, uri, method, base64Body。

下面是一个简单 CSV 文件样例:

scheme

host

uri

method

jsonHeaders

base64Body

http

mockhttpbin.pts.svc.cluster.local

/get?page=1

get

{"name":"kk"}

http

mockhttpbin.pts.svc.cluster.local

/post

post

{"Hello": 'world',}

dGhpcyBpcyBnb29kCg==

代码语言:javascript
复制
scheme,host,uri,method,jsonHeaders,base64Body
http,mockhttpbin.pts.svc.cluster.local,/get?page=1,get,{"name":"kk"},
http,mockhttpbin.pts.svc.cluster.local,/post,post,{"hello":"world"},dGhpcyBpcyBnb29kCg==

为什么使用 base64Body,而不是直接记录 body呢?

由于有些请求的 body 发送的二进制文件,直接写入 CSV 文件会展示成乱码。写成 base64后,可以方便后续转换成原来的结构体。

a. CSV 生成服务代码

代码语言:javascript
复制
package main

import (
"encoding/base64"
"encoding/csv"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
)

func main() {
http.HandleFunc("/", requestHandler) // 设置处理函数
log.Println("Server starting on port 8080...")
log.Fatal(http.ListenAndServe(":8080", nil))
}

func requestHandler(w http.ResponseWriter, r *http.Request) {
// 获取请求信息
scheme := "http" // 默认为http,因为Go的http包不支持直接获取scheme
if r.TLS != nil {
scheme = "https"
}
host := r.Host
uri := r.RequestURI
method := r.Method

// 将headers转换为JSON格式
headersJson, err := json.Marshal(r.Header)
if err != nil {
http.Error(w, "Error converting headers to JSON", http.StatusInternalServerError)
return
}

// 读取请求体
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error reading request body", http.StatusInternalServerError)
return
}
defer r.Body.Close()

// Base64编码请求体
base64Body := base64.StdEncoding.EncodeToString(body)

// 写入CSV文件
record := []string{scheme, host, uri, method, string(headersJson), base64Body}
err = writeToCSV(record)
if err != nil {
http.Error(w, "Error writing to CSV", http.StatusInternalServerError)
return
}

// 发送响应
fmt.Fprintf(w, "Request logged")
}

func writeToCSV(record []string) error {
file, err := os.OpenFile("requests.csv", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return err
}
defer file.Close()

writer := csv.NewWriter(file)
defer writer.Flush()

return writer.Write(record)
}

b. 编译并运行 CSV 生成服务

将上述文件保存成 main.go文件,直接运行代码。

代码语言:javascript
复制
go run main.go

c. 回放流量到 CSV 生成服务上, 用来生成 CSV 文件。

代码语言:javascript
复制
gor --input-file requests.gor --output-http "http://csv-server-address:8080" --http-original-host true

这个命令会读取 requests.gor 文件中的流量,并将其回放到 CSV 生成服务上, CSV 生成服务默认会将接收到的请求写成 requests.csv 文件里;且生成的流量 host 为请求原本的 host 而非 CSV 服务的地址。

2.3 在云压测上使用 CSV 参数文件回放请求

云压测支持用户上传 CSV 文件作为参数文件。您可以动态引用其中的测试数据,供脚本里的变量使用。这样,当施压机并发执行这段代码,每条请求能动态、逐行获取 CSV 里的每行数据,作为请求参数使用。

a. 登录腾讯云云压测,云压测对于首次使用的用户提供一个免费的压测资源包。

b. 测试场景-> 新建场景 → 创建脚本模式

云压测脚本模式支持原生 JavaScript ES2015(ES6)+ 语法,并提供额外函数,帮助您在脚本模式下,快速编排压测场景。

您可在控制台的在线编辑器里,用 JavaScript 代码描述您的压测场景所需的请求编排、变量定义、结果断言、通用函数等逻辑。


c. 指定压测并发数,从上海,广州两个地域分布式发起压测。

d. 上传之前录制的 CSV 文件,作为参数文件。

e. 编写压测脚本,施压机每次执行压测脚本时候,读取 CSV 文件中下一行,利用CSV 文件中记录的字段重新构造出原始请求。

压测脚本如下:

代码语言:javascript
复制
// Send a http get request
import http from 'pts/http';
import { check, sleep } from 'pts';
import util from 'pts/util';
import dataset from 'pts/dataset';

export default function () {
// 读取csv文件各个字段
var method = dataset.get("method")
var scheme = dataset.get("scheme")
var host = dataset.get("host")
var uri = dataset.get("uri")
var jsonHeaders = dataset.get("jsonHeaders")
var base64Body = dataset.get("base64Body")

var headers = JSON.parse(jsonHeaders)
var body = util.base64Decoding(base64Body, "std", "b")

// 构造请求
var req = {
method: method,
url: scheme + "://" + host + uri,
headers: headers,
body: body
}

// 发送请求
var resp = http.do(req)

// simple get request
console.log(resp.body);
check('status is 200', () => resp.statusCode === 200, resp);
// sleep 1 second
sleep(1);
}

f. 保存并运行,即可运行压测脚本,回放流量。查看压测报告及请求采样,观察请求是否符合预期。

请求采样:

总结

通过以上案例,我们展示了如何使用 GoReplay 录制网关流量,并使用云压测脚本模式重新构造用户录制的请求,分布式的回放录制的流量。在压测过程中我们还能产生实时报表,帮助监测被压测服务 QPS,响应时间,错误率等,从而对被压测服务整体健康状况有一个直观的掌控。

云压测除开提供上述脚本模式外,还提供其他模式,包括:

简单模式:以白屏化的方式使用我们交互式 UI 组合 GET,POST,PUT,PATCH,DELETE 请求。

JMeter 模式:使用原生的 JMeter JMX 文件进行压测,用户只需设置指定线程数,即可分布式在多台机器上同时发起压测。

若您有任何压测需求,欢迎试用腾讯云云压测,体验一个灵活、好用、且在不断迭代中的全新压测平台。

联系我们

如有任何疑问,欢迎扫码进入官方交流群~

关于腾讯云可观测平台

腾讯云可观测平台(Tencent Cloud Observability Platform,TCOP)基于指标、链路、日志、事件的全类型监控数据,结合强大的可视化和告警能力,为您提供一体化监控解决方案。满足您全链路、端到端的统一监控诉求,提高运维排障效率,为业务的健康和稳定保驾护航。功能模块有:

  • Prometheus 监控:开箱即用的 Prometheus 托管服务;
  • 应用性能监控 APM:支持无侵入式探针,零配置获得开箱即用的应用观测能力;
  • 云拨测 CAT:利用分布于全球的监测网络,提供模拟终端用户体验的拨测服务;
  • 前端性能监控 RUM:Web、小程序等大前端领域的页面质量和性能监测;
  • Grafana 可视化服务:提供免运维、免搭建的 Grafana 托管服务;
  • 云压测 PTS:模拟海量用户的真实业务场景,全方位验证系统可用性和稳定性;
  • ......等等