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

1,559 阅读3分钟

GoReplay是什么

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

监控和日志也要做好啊喂(#`O′)

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

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

image.png

实现原理

图中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.ReadRequesthttp.ReadResponse
  • 为了优化读取性能,自己实现了bufio.SplitFunc