GoReplay
神器最有效的功能就是在基本不影响线上服务机器运行的情况下,非侵入式地将真实流量导入到本地磁盘文件或者测试机器,实现测试机器上采用真实流量进行测试,从而保证产品发布的质量。
在上一篇文章<<HTTP流量拷贝测试神器GoReplay>>, 笔者主要对以下四个方面进行了描述。
- 流量复制测试
- 流量保存到文件和重放功能介绍
- HTTP请求过滤
- HTTP请求更改
但是我们可能还会碰到如下问题:
- 流量测试结果对比:这个是指,比如即将发布新的程序,将其测试结果和原先的版本测试结果进行对比,来保证即将发布的程序改动后的行为是正确的,稳定的。之前我们只能通过两台测试机器导入同样的真实流量,然后通过Log或者其他方式进行结果对比, 这个一般要进行代码修改,并且还需要写一个Log分析程序进行半自动化的结果对比分析。那么有没有一个方法可以实现实时的流量对比分析呢? 有的那就是
GoReplay
的Middleware
程序编写,本文称作插件编写。 - 请求重写: 虽然
GoReply
命令实现了部分的HTTP
请求更改功能,但是毕竟功能较弱,如果要实现完全的HTTP
请求重写,则可以采用GoRelay
的插件功能。
GoReplay 插件工作原理
GoReplay
插件采用的是进程间通信的方式,从另一个角度来说其支持任意语言实现的插件。那么GoReplay
是采用什么方式和插件通信的呢? GoReplay
插件的输入输出又是什么呢?以及有没有需要注意的点吗?
GoReplay
插件采用的是标准输入和标准输出作为进程间通信的方式GoReplay
插件可以获取到的标准输入是真实流量的原始请求
,原始响应结果
以及测试机器的响应结果
,此时想一想是不是通过后面两点就可以完成流量测试的对比功能了? 插件还可以改写原始请求
然后输出到标准输出,那么GoReplay
会用这个改写后的请求发送到测试机器。- 需要注意的是
原始请求
,原始响应结果
,测试机器的响应结果
理论上不一定是按照顺序的,因为GoReplay
采用的是异步处理。
下图来自于GoReplay
官方Wiki。
那么插件获取的内容是什么格式呢? 我弄个测试例子给大家看看, 如下所示, 是不是一脸懵。这个是十六进制的表示方式,这样的表示方式可以便于插件做标准输入的信息切分,其用\n
表示一个消息体结束。 大家发散思考下,这个协议的设计是不是有点类似于基于TCP
的应用层通讯协议的设计呢? 很多东西都是触类旁通的,我们可以从不同的东西中找到共同的东西,才会在自己设计的时候游刃有余。
3120303433303233383230303030303030313464343533646533203136333830303236343633393533383930303020300a474554202f20485454502f312e310d0a4163636570743a202a2f2a0d0a486f73743a206c6f63616c686f73743a393039300d0a4163636570742d456e636f64696e673a20677a69702c206465666c6174652c2062720d0a436f6e6e656374696f6e3a206b6565702d616c6976650d0a0d0a
实际上解码后的结果如下图所示:
在上图中第一行的内容采用空格分隔开:
- 第一部分是一个数字,可以是
1
,2
或者3
, 分别表示原始请求
,原始的响应结果
和测试机器的响应结果
。比如这个例子中就是一个原始请求。 - 第二部分是一个
ID
, 对于同一个原始请求
以及对应的原始请求的影响结果
和测试机器的响应结果
都采用同一个ID
;但对于不同的请求的ID
是不同的。那么插件就可以根据这个ID
把同一个请求的,原始响应结果和测试响应结果对应起来。 - 第三部分是一个请求到达或者响应结果收到的时间戳
- 第四个部分表示消息从开始到结束所过的过的时间,官方文档表示这个值不一定存在。等后续有空的时候看看源码,什么时候不会有?如果知道的读者也欢迎留言。
后面几行的内容就一目了然了,就是一个HTTP
的消息体。
到这里我们赶紧来实践一下插件的实现和应用吧! 本文以官方的提供的Python
样例为例, 笔者增加了一行sys.stdout.flush()
。
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import fileinput
import binascii
# Used to find end of the Headers section
EMPTY_LINE = b'\r\n\r\n'
def log(msg):
"""
Logging to STDERR as STDOUT and STDIN used for data transfer
@type msg: str or byte string
@param msg: Message to log to STDERR
"""
try:
msg = str(msg) + '\n'
except:
pass
sys.stderr.write(msg)
sys.stderr.flush()
def find_end_of_headers(byte_data):
"""
Finds where the header portion ends and the content portion begins.
@type byte_data: str or byte string
@param byte_data: Hex decoded req or resp string
"""
return byte_data.index(EMPTY_LINE) + 4
def process_stdin():
"""
Process STDIN and output to STDOUT
"""
for raw_line in fileinput.input():
line = raw_line.rstrip()
# Decode base64 encoded line
decoded = bytes.fromhex(line)
# Split into metadata and payload, the payload is headers + body
(raw_metadata, payload) = decoded.split(b'\n', 1)
# Split into headers and payload
headers_pos = find_end_of_headers(payload)
raw_headers = payload[:headers_pos]
raw_content = payload[headers_pos:]
log('===================================')
request_type_id = int(raw_metadata.split(b' ')[0])
log('Request type: {}'.format({
1: 'Request',
2: 'Original Response',
3: 'Replayed Response'
}[request_type_id]))
log('===================================')
log('Original data:')
log(line)
log('Decoded request:')
log(decoded)
encoded = binascii.hexlify(raw_metadata + b'\n' + raw_headers + raw_content).decode('ascii')
log('Encoded data:')
log(encoded)
sys.stdout.write(encoded + '\n')
sys.stdout.flush()
if __name__ == '__main__':
process_stdin()
这个例子主要展示了,如何读取输入,解析输入,以及将原始内容回写到标准输出。我们将其保存为plugin.py
, 然后运行命令行如下, 启动GoReplay
, 这样就会加载插件进程:
sudo ./gor --input-raw :9898 --output-http-track-response --input-raw-track-response --middleware "python3 plugin.py" --output-http "http://<目标机器IP>:9898"
上述例子把所有的输入都回写到了标准输出,其实并不是都有必要的。只有HTTP请求
的内容回写到了标准输出才会将流量导入到测试机器,如果不写到标准输出则表示这个请求不会发送到测试机器;当然你也可以修改这个HTTP请求
, 然后输出到标准输出,那么发送到测试机器的将是修改后的HTTP请求
。
如果想要去重写HTTP请求
或者对测试结果做对比,那免不了对HTTP
协议的操作进行封装。有一些开源作者,实现了一些GoRepay
插件的辅助库功能,让编写GoReplay
插件更加容易。比如GoReplay
作者实现了基于NodeJS
的辅助库goreplay_middleware
; 再比如开源作者amyangfei
实现了基于Python3
的辅助库gor
,通过pip
安装即可: pip install gor
。
流量测试结果对比
我们基于amyangfei
的gor
去实现GoReplay
的插件,完成原始响应结果
和测试机器的响应结果
做对比。此时的测试部署应该如下图所示, GoReplay插件进程
则从GoReplay进程
得到原始响应结果
和测试机器的响应结果
,然后进行对比。
本文采用amyangfei
作者的样例:
# coding: utf-8
import sys
from gor.middleware import AsyncioGor
def on_request(proxy, msg, **kwargs):
proxy.on('response', on_response, idx=msg.id, req=msg)
def on_response(proxy, msg, **kwargs):
proxy.on('replay', on_replay, idx=kwargs['req'].id, req=kwargs['req'], resp=msg)
def on_replay(proxy, msg, **kwargs):
replay_status = proxy.http_status(msg.http)
resp_status = proxy.http_status(kwargs['resp'].http)
if replay_status != resp_status:
sys.stderr.write('replay status [%s] diffs from response status [%s]\n' % (replay_status, resp_status))
else:
sys.stderr.write('replay status is same as response status\n')
sys.stderr.flush()
if __name__ == '__main__':
proxy = AsyncioGor()
proxy.on('request', on_request)
proxy.run()
我们将其保存为plugin.py
, 然后运行命令行如下, 启动GoReplay
, 这样就会加载插件进程:
sudo ./gor --input-raw :9898 --output-http-track-response --input-raw-track-response --middleware "python3 plugin.py" --output-http "http://<目标机器IP>:9898"
注意因为标准输入和标准输出用于进程间通信了,这个时候我们的结果输出可以采用文件或者stderr
。
- 如果结果相同的则会输出
replay status is same as response status
。 - 如果结果不同的则会输出
'replay status [%s] diffs from response status [%s]\n
这个只是一个样例,如果要进行实际工程测试,你也可以去对比HTTP头
或者HTTP Body
等。另外建议将结果输出到文件中,并且将结果不相等的原始请求
, 原始的响应结果
和测试机器的响应结果
都保存到文件,便于后续分析。
重写请求
有的时候测试过程中我们可能需要修改一些HTTP
的请求,再导入到测试机器,来达到一些测试的目的。
比如以上上一个章节的例子为基础,本人这里举一个例子,将HTTP请求
的路径为/
的修改为/test
, 这个时候重新发给测试机器的请求路径就变为/test
。
def on_request(proxy, msg, **kwargs):
if proxy.http_path(msg.http) == '/':
msg.http = proxy.set_http_path(msg.http, '/test')
proxy.on('response', on_response, idx=msg.id, req=msg)
on_request
是一个回调函数,在调用完成后,程序会直接将更新后的msg
按照定义的协议格式输出到标准输出,GoReplay
从标准输出读取新的请求发送到测试机器。
对于其他的请求修改方式,方法类似,笔者就不再赘述。