GoReplay是什么
最恐怖的事情莫过于生产环境来了问题,可能是突然流量大了,也可能是接收到了奇怪的参数,在监控、日志不全的情况下,心态肯定崩了。这时候,GoReplay就应该出场了。
监控和日志也要做好啊喂(#`O′)
GoReplay是Go语言写的一个网络流量(http)转发的应用,无需侵入代码或者修改现有配置。配置简单,单文件命令行即可部署。
通过监听网卡,直接录制请求,后续可以进行流量回放、压力测试、监控等。
实现原理
图中Capture netowrk部分是基于pcap实现的,使用BPF设置指定端口的过滤表达式
是不是想到了tcpdump,所以对于比较复杂的流量,更推荐使用tcpcopy来进行流量复制
常见用法
# 1. 简单的 HTTP 流量复制:
gor –input-raw :80 –output-http “http://example.com”
# 2.HTTP 流量复制频率控制:
gor –input-tcp :28020 –output-http “http://example.com|10″
# 3.HTTP 流量复制缩小:
gor –input-raw :80 –output-tcp “replay.local:28020|10%”
# 4.HTTP 流量记录到本地文件:
gor –input-raw :80 –output-file requests.gor
# 5.HTTP 流量回放和压测:
gor –input-file “requests.gor|200%” –output-http “example.com”
# 6.HTTP 流量过滤复制:
gor –input-raw :8080 –output-http example.com –output-http-url-regexp ^www.
# 7.HTTP指定接口流量复制:
gor --input-raw :80 --http-allow-url '/api/v1' --output-stdout # --output-stdout表示直接在控制台输出
我在什么场景下使用了
线上某个程序平稳运行了很久,最近阿里云监控一直报警带宽。由于老旧项目没有接入prometheus,并不是很清楚实际运行情况,所以准备用GoReplay录制线上流量,然后对接口进行分析。
首先肯定先将线上流量保存下来,使用root运行命令行
./gor --input-raw :7018 \ # 设置监听端口
--input-raw-track-response \ # 同时保存response
--output-file logs/requests-%Y%m%d.gor \ # 按天输出文件
--output-file-append # 默认会分割文件,我们需要合并在一个文件中
分块读取
// 通过回调函数,在外面解析具体的请求
func load(filename string, iter func(i int, req *http.Request, resp *http.Response) error) error {
file, err := os.Open(filename)
if err != nil {
log.Printf("Cannot open text file: %s, err: [%v]", filename, err)
return err
}
defer file.Close()
// 设置一个读取的buffer,这里自定义了一个bufio.SplitFunc
buf := make([]byte, 0, 1024*1024)
scanner := bufio.NewScanner(file)
scanner.Buffer(buf, 10*1024*1024)
// 你没有看错,是通过emoji分割的
scanner.Split(splitByWords([]byte("🐵🙈🙉")))
var req *http.Request
var resp *http.Response
var i = 1
for scanner.Scan() {
text := scanner.Bytes()
n := bytes.IndexByte(text, '\n')
// 根据第一个字符判断是request还是response
// 这里直接使用了原生方法读取了request
if text[0] == '1' {
req, _ = http.ReadRequest(bufio.NewReader(bytes.NewReader(text[n+1:])))
} else if text[0] == '2' {
resp, _ = http.ReadResponse(bufio.NewReader(bytes.NewReader(text[n+1:])), req)
}
if i%2 == 0 {
if iter(i/2, req, resp) != nil {
return err
}
}
i += 1
}
if err := scanner.Err(); err != nil {
log.Printf("Cannot scanner text file: %s, err: [%v]", filename, err)
return err
}
return nil
}
实现bufio.SplitFunc
// copy from bufio.ScanWords
func isSpace(r rune) bool {
if r <= '\u00FF' {
// Obvious ASCII ones: \t through \r plus space. Plus two Latin-1 oddballs.
switch r {
case ' ', '\t', '\n', '\v', '\f', '\r':
return true
case '\u0085', '\u00A0':
return true
}
return false
}
// High-valued ones.
if '\u2000' <= r && r <= '\u200a' {
return true
}
switch r {
case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000':
return true
}
return false
}
func splitByWords(words []byte) bufio.SplitFunc {
return func(data []byte, atEOF bool) (advance int, token []byte, err error) {
start := 0
for width := 0; start < len(data); start += width {
var r rune
r, width = utf8.DecodeRune(data[start:])
if !isSpace(r) {
break
}
}
for width, i := 0, start; i < len(data); i += width {
_, width = utf8.DecodeRune(data[i:])
if bytes.HasSuffix(data[start:i], words) {
return i + width, data[start : i-len(words)], nil
}
}
// If we're at EOF, we have a final, non-empty, non-terminated word. Return it.
if atEOF && len(data) > start {
return len(data), data[start:], nil
}
return start, nil, nil
}
}
结语
完整代码放在Gist上了
这次实战的收货
- 对http的流量有了更深一步的理解。经过统计分析,实际上http header占据了很大一部分流量
- 对于请求频次的很高的接口,通过合并上报接口、延长轮训返回时间等手段,可以极大的降低流量
- 调用一些不常用的接口
http.ReadRequest、http.ReadResponse - 为了优化读取性能,自己实现了
bufio.SplitFunc