前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >GoReplay进阶之插件实现

GoReplay进阶之插件实现

作者头像
河边一枝柳
发布2021-12-04 11:27:56
1.2K0
发布2021-12-04 11:27:56
举报

GoReplay神器最有效的功能就是在基本不影响线上服务机器运行的情况下,非侵入式地将真实流量导入到本地磁盘文件或者测试机器,实现测试机器上采用真实流量进行测试,从而保证产品发布的质量。

在上一篇文章<<HTTP流量拷贝测试神器GoReplay>>, 笔者主要对以下四个方面进行了描述。

  • 流量复制测试
  • 流量保存到文件和重放功能介绍
  • HTTP请求过滤
  • HTTP请求更改

但是我们可能还会碰到如下问题:

  1. 流量测试结果对比:这个是指,比如即将发布新的程序,将其测试结果和原先的版本测试结果进行对比,来保证即将发布的程序改动后的行为是正确的,稳定的。之前我们只能通过两台测试机器导入同样的真实流量,然后通过Log或者其他方式进行结果对比, 这个一般要进行代码修改,并且还需要写一个Log分析程序进行半自动化的结果对比分析。那么有没有一个方法可以实现实时的流量对比分析呢?有的那就是GoReplayMiddleware程序编写,本文称作插件编写。
  2. 请求重写: 虽然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

流量测试结果对比

我们基于amyangfeigor去实现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]

这个只是一个样例,如果要进行实际工程测试,你也可以去对比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从标准输出读取新的请求发送到测试机器。

对于其他的请求修改方式,方法类似,笔者就不再赘述。

参考

  1. GoRepay Wiki: https://github.com/buger/goreplay/wiki
  2. Python GorMW: https://github.com/amyangfei/GorMW
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-11-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一个程序员的修炼之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • GoReplay 插件工作原理
  • 流量测试结果对比
  • 重写请求
  • 参考
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档