GoReplay实战 - 是谁占用了带宽|Go主题月 GoR

GoReplay是什么

最恐怖的事情莫过于生产环境来了问题,可能是突然流量大了,也可能是接收到了奇怪的参数,在监控日志不全的情况下,心态肯定崩了。这时候,GoReplay就应该出场了。

1
bash复制代码监控和日志也要做好啊喂(#`O′)

GoReplay是Go语言写的一个网络流量(http)转发的应用,无需侵入代码或者修改现有配置。配置简单,单文件命令行即可部署。

通过监听网卡,直接录制请求,后续可以进行流量回放、压力测试、监控等。

image.png

实现原理

图中Capture netowrk部分是基于pcap实现的,使用BPF设置指定端口的过滤表达式

1
复制代码是不是想到了tcpdump,所以对于比较复杂的流量,更推荐使用tcpcopy来进行流量复制

常见用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bash复制代码# 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运行命令行

1
2
3
4
bash复制代码./gor --input-raw :7018 \ # 设置监听端口
--input-raw-track-response \ # 同时保存response
--output-file logs/requests-%Y%m%d.gor \ # 按天输出文件
--output-file-append # 默认会分割文件,我们需要合并在一个文件中

分块读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
go复制代码// 通过回调函数,在外面解析具体的请求
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
go复制代码// 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.ReadRequesthttp.ReadResponse
  • 为了优化读取性能,自己实现了bufio.SplitFunc

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%