522 lines
25 KiB
Markdown
522 lines
25 KiB
Markdown
# 22 | 压测平台:如何解决 GoReplay 动态数据关联?
|
||
|
||
你好,我是高楼。
|
||
|
||
在第 6 讲,我们说过目前主流的流量回放工具都无法轻易解决 session 的问题,所以从系统安全的角度来说,工具需要做对应的改造。
|
||
|
||
这节课,我们来聊一下 GoReplay 如何通过改造解决回放过程中动态数据关联的问题。
|
||
|
||
## 关联是什么?
|
||
|
||
我们可以把关联简单地理解为把服务端返回的某个值,传递给后续的调用使用。我们可以在很多场景用到它。举个例子,我们常见的“Session ID”就是一个典型的需要关联的数据。它需要在交互过程中标识一个客户端身份,这个身份要在后续的调用中一直存在,否则服务端就不认识这个客户端了。
|
||
|
||
对每一个性能测试工具来说,关联是应该具备的基本功能,GoReplay 也不例外。
|
||
|
||
但是有很多新手同学对关联的逻辑并不是十分理解,甚至有人觉得关联和参数化(流量数据)是一样的,因为它们用的都是动态的数据,并且关联过来的数据也可以用到参数化(流量数据)中。其实,这二者还是有所不同的,因为关联的数据后续脚本中会用到,但参数化就不会。
|
||
|
||
现在有很多全链路压测都是由单接口基准创建的,这样一来,关联就用得比较少。因为接口级的基准场景都是一发一收就结束了,不需要将数据保存下来再发送出去。
|
||
|
||
那么正常情况下,什么样的场景需要关联呢?一般情况下, 它们需要满足下面几个条件:
|
||
|
||
1. 数据是由服务器端生成的;
|
||
2. 数据在每一次请求时都是动态变化的;
|
||
3. 数据在后续的请求中需要再发送出去。
|
||
|
||
你可以通过这张示意图加深一下理解:
|
||
|
||
![图片](https://static001.geekbang.org/resource/image/6f/62/6f28747510866b140821bfd83ea9bb62.jpg?wh=1264x802)
|
||
|
||
好了,我们知道了关联的基本概念和适用场景,那么在 GoReplay 中又如何改造呢?
|
||
|
||
作为一款流量回放工具,我们知道GoReplay的核心原理就是基于流量文件去倍数回放请求。很显然,这个流量文件是个死的东西,是不能动态参数数据的,那么,我们又该怎么办呢?
|
||
|
||
这时候,我们就需要搬出 GoReplay 的中间件了。
|
||
|
||
## 中间件是什么?
|
||
|
||
[中间件](https://github.com/buger/goreplay/wiki/middleware)( [Middleware](https://github.com/buger/goreplay/wiki/middleware) )是一个在 STDIN(标准输入) 接收请求、响应 payload (有效请求负载)并在 STDOUT(标准输出) 发出修改请求的程序。你可以在中间件上实现任何自定义逻辑,比如认证、复杂的重写和筛选请求等。
|
||
|
||
通过传入 Middleware 参数,我们可以发送命令给 GoReplay,GoReplay 会拉起一个进程执行这个命令。在录制过程中,GoReplay 通过获取进程的 STDIN 和 STDOUT 与输入输出插件进程进行通信,中间件内部逻辑为 STDERR,数据流向大致如下:
|
||
|
||
```bash
|
||
Original request +--------------+
|
||
+-------------+----------STDIN---------->+ |
|
||
| Gor input | | Middleware |
|
||
+-------------+----------STDIN---------->+ |
|
||
Original response +------+---+---+
|
||
| ^
|
||
+-------------+ Modified request v |
|
||
| Gor output +<---------STDOUT-----------------+ |
|
||
+-----+-------+ |
|
||
| |
|
||
| Replayed response |
|
||
+------------------STDIN----------------->----+
|
||
|
||
```
|
||
|
||
需要注意的是,如果希望记录原始响应和回放响应,不要忘记添加 **– output-http-track-response** 和 **– input-raw-track-response** 参数。
|
||
|
||
GoReplay 支持用任何语言编写中间件的协议,同时中间件程序还需要格外注意一点,就是中间件和 Gor 的所有通信都是异步,因此,我们不能保证原始请求和响应消息会一个接一个地出现。如果业务逻辑依赖于原始响应或回放响应,那么中间件应用程序就应该处理好状态,也就是要做好动态数据的处理动作。
|
||
|
||
为了简化中间件的功能实现,官方为 [node.js](https://github.com/buger/goreplay/tree/master/middleware) 和 Go (即将推出)提供了包。
|
||
|
||
## 如何使用中间件?
|
||
|
||
那么,应该怎样使用中间件呢?
|
||
|
||
下面就是一个简单的使用 bash echo 中间件的示例,我们用它来打印对应的 payload 类型:
|
||
|
||
```
|
||
#!/usr/bin/env bash
|
||
#
|
||
# `xxd` utility included into vim-common package
|
||
# It allow hex decoding/encoding
|
||
#
|
||
# This example may broke if you request contains `null` string, you may consider using pipes instead.
|
||
# See: https://github.com/buger/gor/issues/309
|
||
#
|
||
|
||
function log {
|
||
# Logging to stderr, because stdout/stdin used for data transfer
|
||
# 记录到 stderr,因 为 stdout/stdin 用于数据传输
|
||
>&2 echo "[DEBUG][ECHO] $1"
|
||
}
|
||
|
||
while read line; do
|
||
decoded=$(echo -e "$line" | xxd -r -p)
|
||
|
||
header=$(echo -e "$decoded" | head -n +1)
|
||
payload=$(echo -e "$decoded" | tail -n +2)
|
||
|
||
encoded=$(echo -e "$header\n$payload" | xxd -p | tr -d "\\n")
|
||
|
||
log ""
|
||
log "==================================="
|
||
|
||
case ${header:0:1} in
|
||
"1")
|
||
log "Request type: Request"
|
||
;;
|
||
"2")
|
||
log "Request type: Original Response"
|
||
;;
|
||
"3")
|
||
log "Request type: Replayed Response"
|
||
;;
|
||
*)
|
||
log "Unknown request type $header"
|
||
esac
|
||
echo "$encoded"
|
||
|
||
log "==================================="
|
||
|
||
log "Original data: $line"
|
||
log "Decoded request: $decoded"
|
||
log "Encoded data: $encoded"
|
||
done;
|
||
|
||
```
|
||
|
||
这里我们使用【会员登录接口】来做演示。
|
||
|
||
![图片](https://static001.geekbang.org/resource/image/yy/18/yy0d0a5afb7e1a295c0e10b94b989518.png?wh=1282x941)
|
||
|
||
首先,通过指定 Middleware 可执行文件的命令,也就是使用 Middleware 参数在 GoReplay 启用中间件功能:
|
||
|
||
```bash
|
||
sudo ./goreplay --input-raw :8081 --middleware "./echo.sh" --output-http "http://staging.server"
|
||
|
||
```
|
||
|
||
接下来,我们通过 Postman 对【会员登录】接口做一次测试。
|
||
|
||
![图片](https://static001.geekbang.org/resource/image/0c/c4/0cd47ef4bfd1b47513bc36d312db33c4.png?wh=633x479)
|
||
|
||
通过控制台我们看到,中间件程序已经成功把经过的流量信息全部打印出来了。
|
||
|
||
```bash
|
||
Interface: en0 . BPF Filter: ((tcp dst port 8081) and (dst host fe80::8f6:ee40:ebd1:bec or dst host 192.168.3.58))
|
||
Interface: awdl0 . BPF Filter: ((tcp dst port 8081) and (dst host fe80::50a5:ceff:feeb:47e3))
|
||
Interface: llw0 . BPF Filter: ((tcp dst port 8081) and (dst host fe80::50a5:ceff:feeb:47e3))
|
||
Interface: utun0 . BPF Filter: ((tcp dst port 8081) and (dst host fe80::d9a3:ab1b:f8e4:4de))
|
||
Interface: utun1 . BPF Filter: ((tcp dst port 8081) and (dst host fe80::c2a0:19a0:9d9d:6699))
|
||
Interface: utun2 . BPF Filter: ((tcp dst port 8081) and (dst host fe80::771:4985:8642:7857))
|
||
Interface: utun3 . BPF Filter: ((tcp dst port 8081) and (dst host fe80::4a93:e598:6e37:37b3))
|
||
Interface: lo0 . BPF Filter: ((tcp dst port 8081) and (dst host 127.0.0.1 or dst host ::1 or dst host fe80::1))
|
||
2021/11/14 17:16:34 [PPID 8021 and PID 8022] Version:1.3.0
|
||
[DEBUG][ECHO]
|
||
[DEBUG][ECHO] ===================================
|
||
[DEBUG][ECHO] Request type: Request
|
||
[DEBUG][ECHO] ===================================
|
||
[DEBUG][ECHO] Original data: 3120636239663166393130303030303030313535353939393337203136333638383133393937363036333730303020300a504f5354202f61646d696e2f6c6f67696e20485454502f312e310d0a436f6e74656e742d547970653a206170706c69636174696f6e2f6a736f6e0d0a417574686f72697a6174696f6e3a20747275650d0a557365722d4167656e743a20506f73746d616e52756e74696d652f372e32382e340d0a4163636570743a202a2f2a0d0a506f73746d616e2d546f6b656e3a2034666132356536622d666434362d346539362d386166362d6636633562613066303033660d0a486f73743a206c6f63616c686f73743a383038310d0a4163636570742d456e636f64696e673a20677a69702c206465666c6174652c2062720d0a436f6e6e656374696f6e3a206b6565702d616c6976650d0a436f6e74656e742d4c656e6774683a2035320d0a0d0a7b0a202020202270617373776f7264223a2022313233343536222c0a2020202022757365726e616d65223a202274657374220a7d
|
||
[DEBUG][ECHO] Decoded request: 1 cb9f1f910000000155599937 1636881399760637000 0
|
||
POST /admin/login HTTP/1.1
|
||
Content-Type: application/json
|
||
Authorization: true
|
||
User-Agent: PostmanRuntime/7.28.4
|
||
Accept: */*
|
||
Postman-Token: 4fa25e6b-fd46-4e96-8af6-f6c5ba0f003f
|
||
Host: localhost:8081
|
||
Accept-Encoding: gzip, deflate, br
|
||
Connection: keep-alive
|
||
Content-Length: 52
|
||
|
||
{
|
||
"password": "123456",
|
||
"username": "test"
|
||
}
|
||
[DEBUG][ECHO] Encoded data: 3120636239663166393130303030303030313535353939393337203136333638383133393937363036333730303020300a504f5354202f61646d696e2f6c6f67696e20485454502f312e310d0a436f6e74656e742d547970653a206170706c69636174696f6e2f6a736f6e0d0a417574686f72697a6174696f6e3a20747275650d0a557365722d4167656e743a20506f73746d616e52756e74696d652f372e32382e340d0a4163636570743a202a2f2a0d0a506f73746d616e2d546f6b656e3a2034666132356536622d666434362d346539362d386166362d6636633562613066303033660d0a486f73743a206c6f63616c686f73743a383038310d0a4163636570742d456e636f64696e673a20677a69702c206465666c6174652c2062720d0a436f6e6e656374696f6e3a206b6565702d616c6976650d0a436f6e74656e742d4c656e6774683a2035320d0a0d0a7b0a202020202270617373776f7264223a2022313233343536222c0a2020202022757365726e616d65223a202274657374220a7d0a
|
||
|
||
```
|
||
|
||
到这里,我们已经了解了中间件的基本功能和使用方法,接下来我们回到这节课的主题,如何实现关联操作?
|
||
|
||
## 如何实现回放关联?
|
||
|
||
这里我们引入“会员登录”和“查询所有后台资源分类”两个接口为例。
|
||
|
||
你可以先看看这张整体的请求交互示意图:
|
||
|
||
![图片](https://static001.geekbang.org/resource/image/8d/ce/8da2fa743c61455yyd80f63ebe1e18ce.jpg?wh=1424x1953)
|
||
|
||
* 会员登录
|
||
|
||
![图片](https://static001.geekbang.org/resource/image/yy/18/yy0d0a5afb7e1a295c0e10b94b989518.png?wh=1282x941)
|
||
|
||
* 查询所有后台资源分类
|
||
|
||
![图片](https://static001.geekbang.org/resource/image/1a/d8/1a1dbcd1f3eb0566308bfed7fa03efd8.png?wh=1296x754)
|
||
|
||
我们知道 token 是有时效的,如果失效,那么二次请求服务端校验就会失败。如下图:
|
||
|
||
![图片](https://static001.geekbang.org/resource/image/b2/96/b20756aca3d9886966984da4096ee596.png?wh=1244x795)
|
||
|
||
下面我们具体来演示下如何解决token关联的问题。
|
||
|
||
第一步,创建一个流量录制的命令:
|
||
|
||
```shell
|
||
#!/bin/bash
|
||
|
||
PORT="8081"
|
||
OUT_FILE="request.gor"
|
||
|
||
sudo ./goreplay --input-raw :$PORT --output-file=$OUT_FILE -output-file-append --input-raw-track-response --prettify-http
|
||
|
||
```
|
||
|
||
录制下的流量文件如下:
|
||
|
||
```shell
|
||
1 d1ae1f9100000001e404ee86 1635588156669182000 0
|
||
POST /admin/login HTTP/1.1
|
||
Content-Type: application/json
|
||
Authorization: true
|
||
User-Agent: PostmanRuntime/7.28.4
|
||
Accept: */*
|
||
Postman-Token: 480f15ca-53df-44fd-8980-5e9118b2107e
|
||
Host: localhost:8081
|
||
Accept-Encoding: gzip, deflate, br
|
||
Connection: keep-alive
|
||
Content-Length: 52
|
||
|
||
{
|
||
"password": "123456",
|
||
"username": "test"
|
||
}
|
||
|
||
🐵🙈🙉
|
||
2 d1ae1f9100000001e404ee86 1635588156828973000 458000
|
||
HTTP/1.1 200
|
||
Content-Length: 254
|
||
Vary: Origin
|
||
Vary: Access-Control-Request-Method
|
||
Vary: Access-Control-Request-Headers
|
||
X-Content-Type-Options: nosniff
|
||
X-XSS-Protection: 1; mode=block
|
||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||
Pragma: no-cache
|
||
Expires: 0
|
||
X-Frame-Options: DENY
|
||
Content-Type: application/json
|
||
Date: Sat, 30 Oct 2021 10:02:36 GMT
|
||
Keep-Alive: timeout=60
|
||
Connection: keep-alive
|
||
|
||
{"code":200,"message":"操作成功","data":{"tokenHead":"","token":"eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0IiwiY3JlYXRlZCI6MTYzNTU4ODE1Njc5NCwiZXhwIjoxNjM1NTg4MjE2fQ.-wsZa0gijz2KfCF-eAYK1Tt-pd_vw2_LShShlIDCQOsHjOZZlGl8yX2MncZlO9St_oPj1JdBaERjfEU6iu12qw"}}
|
||
|
||
|
||
|
||
🐵🙈🙉
|
||
1 d1ae1f9100000001e405029a 1635588192031592000 0
|
||
GET /resource/listAll HTTP/1.1
|
||
token: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0IiwiY3JlYXRlZCI6MTYzNTU4ODE1Njc5NCwiZXhwIjoxNjM1NTg4MjE2fQ.-wsZa0gijz2KfCF-eAYK1Tt-pd_vw2_LShShlIDCQOsHjOZZlGl8yX2MncZlO9St_oPj1JdBaERjfEU6iu12qw
|
||
User-Agent: PostmanRuntime/7.28.4
|
||
Accept: */*
|
||
Postman-Token: 4bc2152e-dbd4-4b2a-b880-72ed5ee4303a
|
||
Host: localhost:8081
|
||
Accept-Encoding: gzip, deflate, br
|
||
Connection: keep-alive
|
||
|
||
🐵🙈🙉
|
||
2 d1ae1f9100000001e405029a 1635588192064563000 1084000
|
||
HTTP/1.1 200
|
||
Content-Length: 3997
|
||
Vary: Origin
|
||
Vary: Access-Control-Request-Method
|
||
Vary: Access-Control-Request-Headers
|
||
X-Content-Type-Options: nosniff
|
||
X-XSS-Protection: 1; mode=block
|
||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||
Pragma: no-cache
|
||
Expires: 0
|
||
X-Frame-Options: DENY
|
||
Content-Type: application/json
|
||
Date: Sat, 30 Oct 2021 10:03:12 GMT
|
||
Keep-Alive: timeout=60
|
||
Connection: keep-alive
|
||
|
||
{"code":200,"message":"操作成功","data":[{"id":1,"createTime":"2020-02-04T09:04:55.000+00:00","name":"商品品牌管理","url":"/brand/**","description":null,"categoryId":1},{"id":2,"createTime":"2020-02-04T09:05:35.000+00:00","name":"商品属性分类管理","url":"/productAttribute/**","description":null,"categoryId":1},{"id":3,"createTime":"2020-02-04T09:06:13.000+00:00","name":"商品属性管理","url":"/productAttribute/**","description":null,"categoryId":1},{"id":4,"createTime":"2020-02-04T09:07:15.000+00:00","name":"商品分类管理","url":"/productCategory/**","description":null,"categoryId":1},{"id":5,"createTime":"2020-02-04T09:09:16.000+00:00","name":"商品管理","url":"/product/**","description":null,"categoryId":1},{"id":6,"createTime":"2020-02-04T09:09:53.000+00:00","name":"商品库存管理","url":"/sku/**","description":null,"categoryId":1},{"id":8,"createTime":"2020-02-05T06:43:37.000+00:00","name":"订单管理","url":"/order/**","description":"","categoryId":2},{"id":9,"createTime":"2020-02-05T06:44:22.000+00:00","name":" 订单退货申请管理","url":"/returnApply/**","description":"","categoryId":2},{"id":10,"createTime":"2020-02-05T06:45:08.000+00:00","name":"退货原因管理","url":"/returnReason/**","description":"","categoryId":2},{"id":11,"createTime":"2020-02-05T06:45:43.000+00:00","name":"订单设置管理","url":"/orderSetting/**","description":"","categoryId":2},{"id":12,"createTime":"2020-02-05T06:46:23.000+00:00","name":"收货地址管理","url":"/companyAddress/**","description":"","categoryId":2},{"id":13,"createTime":"2020-02-07T08:37:22.000+00:00","name":"优惠券管理","url":"/coupon/**","description":"","categoryId":3},{"id":14,"createTime":"2020-02-07T08:37:59.000+00:00","name":"优惠券领取记录管理","url":"/couponHistory/**","description":"","categoryId":3},{"id":15,"createTime":"2020-02-07T08:38:28.000+00:00","name":"限时购活动管理","url":"/flash/**","description":"","categoryId":3},{"id":16,"createTime":"2020-02-07T08:38:59.000+00:00","name":"限时购商品关系管理","url":"/flashProductRelation/**","description":"","categoryId":3},{"id":17,"createTime":"2020-02-07T08:39:22.000+00:00","name":"限时购场次管理","url":"/flashSession/**","description":"","categoryId":3},{"id":18,"createTime":"2020-02-07T08:40:07.000+00:00","name":"首页轮播广告管理","url":"/home/advertise/**","description":"","categoryId":3},{"id":19,"createTime":"2020-02-07T08:40:34.000+00:00","name":"首页品牌管理","url":"/home/brand/**","description":"","categoryId":3},{"id":20,"createTime":"2020-02-07T08:41:06.000+00:00","name":"首页新品管理","url":"/home/newProduct/**","description":"","categoryId":3},{"id":21,"createTime":"2020-02-07T08:42:16.000+00:00","name":"首页人气推荐管理","url":"/home/recommendProduct/**","description":"","categoryId":3},{"id":22,"createTime":"2020-02-07T08:42:48.000+00:00","name":"首页专题推荐管理","url":"/home/recommendSubject/**","description":"","categoryId":3},{"id":23,"createTime":"2020-02-07T08:44:56.000+00:00","name":" 商品优选管理","url":"/prefrenceArea/**","description":"","categoryId":5},{"id":24,"createTime":"2020-02-07T08:45:39.000+00:00","name":"商品专题管理","url":"/subject/**","description":"","categoryId":5},{"id":25,"createTime":"2020-02-07T08:47:34.000+00:00","name":"后台用户管理","url":"/admin/**","description":"","categoryId":4},{"id":26,"createTime":"2020-02-07T08:48:24.000+00:00","name":"后台用户角色管理","url":"/role/**","description":"","categoryId":4},{"id":27,"createTime":"2020-02-07T08:48:48.000+00:00","name":"后台菜单管理","url":"/menu/**","description":"","categoryId":4},{"id":28,"createTime":"2020-02-07T08:49:18.000+00:00","name":"后台资源分类管理","url":"/resourceCategory/**","description":"","categoryId":4},{"id":29,"createTime":"2020-02-07T08:49:45.000+00:00","name":"后台资源管理","url":"/resource/**","description":"","categoryId":4}]}
|
||
🐵🙈🙉
|
||
|
||
```
|
||
|
||
第二步,创建一个流量回放 Shell 脚本。
|
||
|
||
```shell
|
||
#!/bin/bash
|
||
## Usage: ./replay.sh
|
||
|
||
OUTPUT="http://127.0.0.1:8081"
|
||
INPUT_FILE="requests.gor"
|
||
|
||
sudo ./goreplay --input-file $INPUT_FILE --input-file-loop --output-http=$OUTPUT --prettify-http --output-http-track-response --output-stdout
|
||
|
||
```
|
||
|
||
第三步,我们尝试进行一次回放操作。
|
||
|
||
![图片](https://static001.geekbang.org/resource/image/c8/e4/c8e10a5c7b6621cc54d4971b217959e4.png?wh=750x355)
|
||
|
||
等待一会,我们看到回放的【查询所有后台资源分类】接口已经失败了,提示 token 失效了。
|
||
|
||
要怎么解决这个问题呢?
|
||
|
||
在这种情况下,我们需要实时将来自录制的 token 关联到来自回放响应的 token 上 ,然后使用关联的 token 修改回放的请求。我们使用 GoReplay 存储库中的这个方便的[示例](https://github.com/buger/goreplay/blob/master/examples/middleware/token_modifier.go)进行扩展。
|
||
|
||
所涉及的基本算法你可以看看下面这张图片。
|
||
![](https://static001.geekbang.org/resource/image/c0/00/c0b9bd3a17b2d34f0ec2d4e1ea5f0e00.jpg?wh=1920x1040)
|
||
|
||
因为原始服务器没有预定义的token,而回放服务器有自己的token,它不能与原始服务器同步。所以不使用中间件或者中间件只使用请求有效 payload,都会使得token失效。
|
||
|
||
为了解决这个问题,我们的中间件应该考虑回放和源服务器的响应,存储’ originalToken -> replayedToken '别名,并使用此 token 重写所有请求以使用回放别名。
|
||
|
||
顺着这个思路,我们看下第四步,创建 token 关联中间件程序。
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"bufio"
|
||
"bytes"
|
||
"encoding/hex"
|
||
"fmt"
|
||
"github.com/bitly/go-simplejson"
|
||
"github.com/buger/goreplay/proto"
|
||
"os"
|
||
)
|
||
|
||
// requestID -> originalToken
|
||
// 请求 ID -> 原始 Token
|
||
var originalTokens map[string][]byte
|
||
|
||
// originalToken -> replayedToken
|
||
// 原始 Token -> 回放 Token
|
||
var tokenAliases map[string][]byte
|
||
|
||
var json_data interface{}
|
||
|
||
|
||
|
||
func main() {
|
||
originalTokens = make(map[string][]byte)
|
||
tokenAliases = make(map[string][]byte)
|
||
|
||
scanner := bufio.NewScanner(os.Stdin)
|
||
|
||
for scanner.Scan() {
|
||
encoded := scanner.Bytes()
|
||
buf := make([]byte, len(encoded)/2)
|
||
hex.Decode(buf, encoded)
|
||
|
||
process(buf)
|
||
}
|
||
}
|
||
|
||
func process(buf []byte) {
|
||
// First byte indicate payload type, possible values:
|
||
// 1 - Request
|
||
// 2 - Response
|
||
// 3 - ReplayedResponse
|
||
// 第一个字节表示有效负载类型,可能的值:
|
||
// 1 - 请求
|
||
// 2 - 响应
|
||
// 3 - 回放响应
|
||
payloadType := buf[0]
|
||
headerSize := bytes.IndexByte(buf, '\n') + 1
|
||
header := buf[:headerSize-1]
|
||
|
||
// Header contains space separated values of: request type, request id, and request start time (or round-trip time for responses)
|
||
// Header 包含空格分隔的值:请求类型,请求 id,请求开始时间(或响应的往返时间)
|
||
meta := bytes.Split(header, []byte(" "))
|
||
|
||
// For each request you should receive 3 payloads (request, response, replayed response) with same request id
|
||
// 对于每个请求,你应该收到 3 个有效负载(request, response, replayed response),具有相同的请求 id
|
||
reqID := string(meta[1])
|
||
payload := buf[headerSize:]
|
||
|
||
Debug("Received payload:", string(buf))
|
||
|
||
switch payloadType {
|
||
case '1': // Request
|
||
if bytes.Equal(proto.Path(payload), []byte("/admin/login")) {
|
||
originalTokens[reqID] = []byte{}
|
||
Debug("Found token request:", reqID)
|
||
} else {
|
||
//token, vs, _ := proto.PathParam(payload, []byte("token")) //取到回放响应的 token 值
|
||
token := proto.Header(payload, []byte("token")) //取到原始的 token 值
|
||
|
||
Debug("Received token:", string(token))
|
||
|
||
if len(token) != 0 { // If there is GET token param
|
||
Debug("If there is GET token param")
|
||
Debug("tokenAliases", tokenAliases)
|
||
if alias, ok := tokenAliases[string(token)]; ok { //检查要替换的 token 值是否存在
|
||
Debug("Received alias")
|
||
// Rewrite original token to alias
|
||
payload = proto.SetHeader(payload, []byte("token"), alias) //将原始的 token 替换成回放的 token
|
||
|
||
// Copy modified payload to our buffer
|
||
buf = append(buf[:headerSize], payload...)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Emitting data back
|
||
os.Stdout.Write(encode(buf)) //重写请求准备发往回放服务
|
||
case '2': // Original response
|
||
if _, ok := originalTokens[reqID]; ok {
|
||
jsonObject, err := simplejson.NewJson([]byte(proto.Body(payload)))
|
||
if err != nil {
|
||
fmt.Println(err)
|
||
}
|
||
|
||
result := jsonObject.Get("data")
|
||
token := result.Get("token")
|
||
secureToken:=token
|
||
|
||
f ,_:=secureToken.Bytes()
|
||
|
||
originalTokens[reqID] = f
|
||
Debug("Remember origial token:", f)
|
||
|
||
}
|
||
case '3': // Replayed response
|
||
if originalToken, ok := originalTokens[reqID]; ok {
|
||
delete(originalTokens, reqID)
|
||
|
||
|
||
|
||
jsonObject, err := simplejson.NewJson([]byte(proto.Body(payload)))
|
||
if err != nil {
|
||
fmt.Println(err)
|
||
}
|
||
result := jsonObject.Get("data")
|
||
token := result.Get("token")
|
||
f ,_:=token.Bytes()
|
||
tokenAliases[string(originalToken)] = f //拿到现在的 token 值用来替换掉过去的 token 值
|
||
|
||
Debug("Create alias for new token token, was:", string(originalToken), "now:", string(f))
|
||
}
|
||
}
|
||
}
|
||
|
||
func encode(buf []byte) []byte {
|
||
dst := make([]byte, len(buf)*2+1)
|
||
hex.Encode(dst, buf)
|
||
dst[len(dst)-1] = '\n'
|
||
|
||
return dst
|
||
}
|
||
|
||
func Debug(args ...interface{}) {
|
||
if os.Getenv("GOR_TEST") == "" { // if we are not testing
|
||
fmt.Fprint(os.Stderr, "[DEBUG][TOKEN-MOD] ")
|
||
fmt.Fprintln(os.Stderr, args...)
|
||
}
|
||
|
||
}
|
||
|
||
```
|
||
|
||
我们可以使用 process 函数异步处理原始请求或回放响应从而重新设置 token。由于 GoReplay 的每个三元组(请求、响应、回放响应)共享一个请求 ID,因此到达中间件的第一个响应可以将它的 token 关联到请求 ID。当第二个响应到达时,我们就可以访问两个 token了。我们可以将原始 token 关联到回放的 token,并能够一一对应(因为第二个响应类型也可用)。
|
||
|
||
好了,这样中间件就写完了,我们一起来测试一下。
|
||
|
||
我们先创建一个运行中间件的 Shell 脚本 middleware\_wrapper.sh。
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
go run token_modifier.go
|
||
|
||
```
|
||
|
||
第二步,修改启动回放的 Shell 脚本 replay.sh。
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
## Usage: ./replay.sh
|
||
|
||
OUTPUT="http://127.0.0.1:8081"
|
||
MIDDLEWARE="./middleware_wrapper.sh"
|
||
INPUT_FILE="requests.gor"
|
||
|
||
sudo ./goreplay --input-file $INPUT_FILE --input-file-loop --output-http=$OUTPUT --middleware $MIDDLEWARE --prettify-http --output-http-track-response --output-stdout
|
||
|
||
```
|
||
|
||
最后一步,我们就要紧盯运行控制台了。
|
||
|
||
* 登录接口实时返回的 token。
|
||
|
||
![图片](https://static001.geekbang.org/resource/image/c4/90/c4a97041689054c34c2b721a48ecd190.png?wh=1881x695)
|
||
|
||
* 【查询所有后台资源分类】接口,可以看到已经成功替换到回放响应的 token了。
|
||
|
||
![图片](https://static001.geekbang.org/resource/image/de/98/de4c63d1d179f2b5b4804e5febac1898.png?wh=750x386)
|
||
|
||
* 【查询所有后台资源分类】接口,回放响应的数据也是正常的。
|
||
|
||
![图片](https://static001.geekbang.org/resource/image/55/24/55b26162d5dacaa39a0a161d5f83f124.png?wh=750x321)
|
||
|
||
* 服务端日志显示正常。
|
||
|
||
![图片](https://static001.geekbang.org/resource/image/71/yy/71e8fd0b45f03d5861c1debe0cd504yy.png?wh=599x348)
|
||
|
||
好了,到这里,我们的动态数据关联功能就已经实现了。
|
||
|
||
## 总结
|
||
|
||
好了,这节课就讲到这里。刚才,我们一起梳理了关联的基本概念、 GoReplay 中间件( Middleware )原理、常用的用法。我们还通过例子演示了GoReplay 如何通过扩展 Middleware 做到关联功能。实际上,我们可以在中间件上实现任何自定义逻辑,比如认证、复杂的重写和筛选请求等。
|
||
|
||
下一节课,我们将进入具体的分布式改造环节,我会通过案例演示如何做分布式平台改造工作。
|
||
|
||
## 课后题
|
||
|
||
学完这节课,请你思考两个问题:
|
||
|
||
1. 你有没有使用过 Middleware,谈谈你对 Middleware 应用的一些心得吧!
|
||
2. 相比 JMeter,你觉得 GoReplay 关联的难度在什么地方?
|
||
|
||
欢迎你在留言区与我交流讨论。当然了,你也可以把这节课分享给你身边的朋友,他们的一些想法或许会让你有更大的收获。我们下节课见!
|
||
|