安装
1、安装go环境
-- 下载压缩包 https://studygolang.com/dl
wget https://dl.google.com/go/go1.21.4.linux-amd64.tar.gz
-- 解压
tar -C /usr/local -zxvf go1.21.4.linux-amd64.tar.gz
-- 添加环境变量
vim /etc/profile
export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin
-- 生效环境变量
source /etc/profile
-- 获取go版本,进行验证
go version
2、安装goreplay
-- 下载压缩包
wget https://github.com/buger/goreplay/releases/download/1.3.3/gor_1.3.3_x64.tar.gz
-- 解压
tar -zxvf gor_1.3.3_x64.tar.gz
-- 将可执行命令,加到环境变量中
mv gor /usr/local/bin
3、监听端口进行流量复制
-- 服务器监听8125端口,--output-stdout 指定输出端为控制台
gor --input-raw :8125 --output-stdout
使用post方式访问 http://10.1.1.6:8125/logs/save?logContent=test1
10.1.1.6 服务器控制台日志打印:
流量复制命令行选项
可用输入
–input-raw 用于捕捉 HTTP 流量,指定 IP 地址或接口和应用程序端口。
–input-file 承受流量输入的文件(–output-file),用来离线流量重放。
–input-tcp 用于接受来自多个Gor 实例的流量,可以实现多个Gor的聚合。
可用输出
–output-http 转发收到的流量到给定的ip+端口。
–output-file 记录收到的流量到文件,用于保留流量、离线复制流量。
–output-tcp 将收到的流量转发给另一个 Gor 实例。
–output-stdout 将收到的流量输出到控制台。
请求过滤
在某些时候我们只期望把生产环境的特定流量重放,或者禁止一些流量重放,这时候我们可以使用 GoReplay 的过滤功能;GoReplay 提供以下选项来提供过滤功能:
--http-allow-header: 允许重放的 HTTP 头 (支持正则)
--http-disallow-header: 不允许的 HTTP 头 (支持正则)
--http-allow-method: 允许重放的 HTTP 方法
--http-allow-url: 允许重放的 URL (支持正则)
--http-disallow-url: 不允许的 HTTP URL (支持正则)
下文是一些流量复制的命令,通过灵活组合不同的命令行选项,实现各种流量复制任务:
//离线流量复制: 监听本机8125 端口,将流量记录到文件
gor --input-raw :8125 --output-file requests_origin.gor
//离线流量复制: 从本地文件解析流量,发送到测试服务器
gor --input-file requests_origin.gor --output-http http://172.16.14.60:8125
//在线流量复制:监听本机8125 端口,将流量发送到测试服务器
gor --input-raw :8125 --output-http http://172.16.14.60:8125
//在线流量复制,限制请求方法
gor --input-raw :8125 --output-http http://172.16.14.60:8125 --http-allow-method: GET
//在线流量复制,限制URL
gor --input-raw :8125 --output-http http://172.16.14.60:8125 --http-allow-url: /api
离线流量复制
离线的流量复制分为两步:流量录制和流量重放。
1、在线上服务器启动流量录制,监听服务端口,流量输出到文件。
gor --input-raw :8125 --output-file requests_origin.gor
在服务器上生成requests_origin.gor文件,记录了请求信息:请求方法、路径、参数、header等
2、在测试服务器上启动流量重放,流量入口选择为文件,输出到测试应用。
gor --input-file /home/gor/requests_origin.gor --output-http http://172.16.14.60:8125
在线流量复制
分别在本地、1.6服务器上启动efs 实例,端口为8125。在1.6 上启动gor,监听 8125端口,转发到本地进行流量复杂验证。
gor --input-raw :8125 --output-http http://172.16.14.60:8125
使用post方式访问 http://10.1.1.6:8125/logs/save?logContent=test1。本地收到请求
测试:流量复制是否会影响原始请求
使用jmeter进行测试,进行如下七个阶段的测试,监控各个阶段请求的响应时间:
阶段1:无流量复制
阶段2:打开流量复制,镜像服务未启动正常
阶段3:启动镜像服务
阶段4:关闭镜像服务器
阶段5:启动镜像服务,使用sleep 模拟慢请求
阶段6:关闭镜像服务器
阶段7:关闭流量复制
下图分别是RT在300ms、100ms、10ms 的接口是整个操作过程中的请求RT变化,可以看到进行流量复制一定程度上会影响原始请求。
其他特性支持
1、流量放大
在进行离线流量重放时,通过 requests_origin.gor|200% 这种方式指定文件名,GoReplay 会以两倍的速率进行请求重放
gor --input-file "requests_origin.gor|200%" --output-http "http://127.0.0.1:9798"
2、流量收缩
使用 goreplay做流量复制时,如果线上流量较大测试环境扛不住,可以配置通过 绝对数量限制 和 百分比数量限制 两种方式限制复制的流量。
绝对数量限制: 使用 --output-http "ADDRESS|N" 形式的参数时,GoReplay 会保证镜像的流量请求每秒不会超过 “N” 个。
百分比限制限制: 使用 --output-http "ADDRESS|N%" 形式的参数时,GoReplay 会保证镜像的流量维持在总流量的 “N%”
3、记录响应信息
想要完整的记录原始响应和重放响应的信息,需要添加如下两个配置:-input-raw-track-response, -output-http-track-response。
-input-raw-track-response 选项配置后,会记录原始请求的响应。-output-http-track-response选项配置后会记录重放请求的响应。
如下命令:监听当前服务器的8125端口,将流量复制到 172.16.14.60:8125 端口,同时记录到requests_origin文件,包括原始响应和重放响应。
gor
--input-raw :8125
--output-http http://172.16.14.60:8125
--output-file /home/gor/requests_origin.gor
-input-raw-track-response
-output-http-track-response
4、流量文件解析
1 df891fbdac100e3cbdcd1d84 1701737200353284133 0
POST /logs/save?logContent=test1 HTTP/1.1
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.8
Accept: */*
Cache-Control: no-cache
Postman-Token: 0f312cea-6133-448e-a5f4-702281b4de7a
Host: 10.1.1.6:8125
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 0
🐵🙈🙉
2 df891fbdac100e3cbdcd1d84 1701737200417378904 727175
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 05 Dec 2023 00:46:39 GMT
Keep-Alive: timeout=60
Connection: keep-alive
83
{"success":true,"entity":"操作成功","list":null,"existList":null,"map":null,"schoolManager":false,"serviceCode":null,"count":0}
0
🐵🙈🙉
3 df891fbdac100e3cbdcd1d84 1701737201362941251 16637663
HTTP/1.1 200
Transfer-Encoding: chunked
Connection: keep-alive
Content-Type: application/json
Date: Tue, 05 Dec 2023 00:46:39 GMT
Keep-Alive: timeout=60
83
{"success":true,"entity":"操作成功","list":null,"existList":null,"map":null,"schoolManager":false,"serviceCode":null,"count":0}
0
-
文件以三个猴做分割,分为多个请求和响应。
-
第一行开头的1、2、3代表这快内容是请求、原始响应、重放响应。
-
第一行中间是请求ID,在所有请求中是唯一的,原始响应和重放响也会保持不变,用于请求和响应之间建立关联
-
第四个参数它表示延迟(消息开始和结束之间的持续时间)
-
然后就是正常的http 请求、响应的header 和body
5、使用middleware 实现自动化比较
middleware 模块
middleware是一个Gor的可扩展模块,通过 STDIN 接受请求和响应内容,通过 STDOUT发出修改后的请求。
Original request +--------------+
+-------------+----------STDIN---------->+ |
| Gor input | | Middleware |
+-------------+----------STDIN---------->+ |
Original response (1) +------+---+---+
| ^
+-------------+ Modified request v |
| Gor output +<---------STDOUT-----------------+ |
+-----+-------+ |
| |
| Replayed response |
+------------------STDIN----------------->----+
实现思路
1、利用gor 提供的middleWare模块,这个模块可以读取到原始请求、原始响应、重放响应。
2、原始请求、原始响应、重放响应可以使用requestId进行关联,从而实现比较。
实现细节
官方dome :
https://github.com/buger/goreplay/blob/master/examples/middleware/echo.java
命令行:gor --input-raw :8125 --output-http http://172.16.14.60:8125 --output-file /home/gor/requests_origin.gor -input-raw-track-response -output-http-track-response --middleware "java -jar /home/gor/gor_diff-jar-with-dependencies.jar"
java 代码:
package org.example;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.binary.StringUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
/**
* description
*
* @author king
* 2023年11月25日
*/
@Slf4j
public class Echo {
public static void main(String[] args) throws DecoderException {
if (args != null) {
for (String arg : args) {
log.info(arg);
}
}
Map<String, String> requestMap = new HashMap<>();
Map<String, String> responseMap = new HashMap<>();
BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
String line = null;
try {
while ((line = stdin.readLine()) != null) {
String decodedLine = decodeHexString(line);
LogType type = getType(decodedLine);
String requestId = getRequestId(decodedLine);
log.info("type:{}, requestId: {}", type, requestId);
if (type == LogType.ORIGINAL_REQUEST) {
requestMap.put(requestId, decodedLine);
} else if (type == LogType.ORIGINAL_RESPONSE || type == LogType.REPLAYED_RESPONSE) {
if (responseMap.containsKey(requestId)) {
log.info("requestId: {} Response compare", requestId);
String decodedLine2 = responseMap.get(requestId);
String response = getResponse(decodedLine);
String response2 = getResponse(decodedLine2);
boolean res = compareBody(response, response2);
if (!res) {
log.error("请求:{} 的响应不一致\n 原始请求:{} \n 原始响应:{} \n 重放响应:{}",
requestId, requestMap.get(requestId), type == LogType.ORIGINAL_RESPONSE ? decodedLine : decodedLine2,
type == LogType.REPLAYED_RESPONSE ? decodedLine : decodedLine2);
} else {
log.info("请求:{} 的响应一致", requestId);
}
requestMap.remove(requestId);
responseMap.remove(requestId);
} else {
responseMap.put(requestId, decodedLine);
log.info("type:{}, requestId: {} add Response to Map", type, requestId);
}
}
// log.info("type:{}, requestId: {}, line: {}", type, requestId, line);
System.out.println(line);
}
} catch (IOException e) {
log.error("error", e);
}
}
public static String decodeHexString(String s) throws DecoderException {
return new String(Hex.decodeHex(s.toCharArray()));
}
public static LogType getType(String info) {
int id = Integer.parseInt(info.substring(0, 1));
return LogType.getById(id);
}
public static String getRequestId(String info) {
int to = info.indexOf(' ', 2);
return info.substring(2, to);
}
public static String getResponse(String info) {
return info.substring(info.indexOf("\n") + 1);
}
public static boolean compareBody(String response1, String response2) {
if (response1 == null && response2 == null) {
return true;
}
if (response1 == null || response2 == null) {
return false;
}
return StringUtils.equals(getBody(response1), getBody(response2));
}
public static String getBody(String response) {
if (!response.contains("\r\n\r\n")) {
return "";
}
int start = response.indexOf("\r\n", response.indexOf("\r\n\r\n") + 4) + 2;
int end = response.indexOf("\r\n", start);
return response.substring(start, end);
}
}
执行效果:
行动吧,在路上总比一直观望的要好,未来的你肯定会感谢现在拼搏的自己!如果想学习提升找不到资料,没人答疑解惑时,请及时加入群: 786229024,里面有各种测试开发资料和技术可以一起交流哦。
最后: 下方这份完整的软件测试视频教程已经整理上传完成,需要的朋友们可以自行领取【保证100%免费】
软件测试面试文档
我们学习必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有字节大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。