开源测试工具 录制线上流量做回归测试的正确打开方式

少年 · 2020年10月13日 · 最后由 吴大熊 回复于 2023年02月06日 · 20579 次阅读
本帖已被设为精华帖!

目录

线上流量

什么是录制线上流量回放

为什么需要录制线上流量回放

  • 项目大迭代更新,容易漏测,或者有很多没用评估到的地方。

  • 如果用线上流量做一次回归测试,可以进一步减少 bug 的风险。

  • 大大节省构造测试数据,或者构造测试数据脚本的时间,提高效率。

线上流量回放的限制是什么

  • 只回放 GET 请求

因为其他请求的回放,会对用户数据进行操作,有风险,需要排除。

除非构建多套备份数据库,但成本太高,不是很有必要。

  • 需要对比回放前后的流量

不然回放就没有意义了,你都不知道回放前后对比的差异是什么。

  • 需要去噪音

对比完了,对于一些类似时间戳的值,其实就是噪音,这些不一样很正常,我们需要剔除,不然差异没有价值。

由此可见,想要正确打开线上流量录制回放,需要解决很多问题。

而重中之重,就是 diff。

回放差异 diff

diff 实现对比和去噪

demo 实现

docker-compose

version: '2'
services:

  http-demo-record:
    image: shaonian/http-demo:gor-test-v2.9
    ports:
      - "8080:8080"

  http-demo-replay-old:
    image: shaonian/http-demo:diff-old-1

  http-demo-replay-old-noise:
    image: shaonian/http-demo:diff-old-2

  http-demo-replay-new:
    image: shaonian/http-demo:diff-new

  diff:
    image: shaonian/diff:v0.1
    command:
      - -candidate=http-demo-replay-new:8080
      - -master.primary=http-demo-replay-old:8080
      - -master.secondary=http-demo-replay-old-noise:8080
      - -service.protocol=http
      - -serviceName='Diff Test'
      - -proxy.port=:8880
      - -admin.port=:8881
      - -http.port=:8888
      - -rootUrl=localhost:8888
      - -summary.email='your@email.com'
    ports:
      - "8881:8881"
      - "8888:8888"

diff 效果

diff 限制

diff 的归类有问题。

因为 url 能携带各种各样的 param,所以 diff 设计里面不会直接把 url 作为归纳名,需要通过在请求的 header 里面增加 Canonical-Resource: http-demo 来设置。

这就出来一个问题,线上转发的流量,无法根据具体的路由来动态设置归纳名,只能统一设置成是一个服务的,比如 http-demo 这样,但是我这个 http-demo 下有很多 api,出来的差异具体是哪一个 api 呢,我也不知道,得看返回字段去猜,就很华而不实。

所以做到这里,只能自嗨,无法落地到实际项目中,想要真正落地,这一步也是一定一定要解决的!

缺陷

以上实现的方法总结起来,就是把录制 gor 组件写进 Dockerfile,并在项目运行的时候,实时录制线上流量,转发到测试环境,然后进行 diff 去噪对比。

但是这样就大功告成了吗?

并没有。

还有几个问题需要自我反问一下。

  • 我们真的需要实时录制转发吗?

其实不需要。

我们只是希望能够录制线上请求,然后根据再迭代之后用来回放测试。

如果开启实时回放,会在我们不需要测试的时候,浪费服务器的性能和资源。

  • 线上录制的回放,真的就代表全部场景吗?

其实也不对。

用户不一定不触发的场景,其实我们也需要覆盖。

录制只是让我们更容易更便捷生成测试数据而已。

  • 线上录制会有性能损耗吗?

或多或少都有影响,毕竟 gor 和 服务处于同一个容器中。

所以三个反问以后,我们的需求逐渐明确了。

我们需要一个不会影响线上服务性能的,又能快速生成测试数据回放,并且能自定义补全更多场景的测试回放。

同时,我们还需要解决 diff 的路由智能匹配的问题。

这样可以吗?

我觉得可以。

尝试的解决方案

可以通过复制粘贴人为构造回放所需的测试数据日志

上图是录制流量以后保存的 log 文件,我们可以清楚看到它的结构,所以这是不是意味着,只要我们写出来这份相同格式的 log,我们就能直接凭借这份 log 来回放呢?

对的。

此外,这个 log 里面,你可以直接根据具体的 url,设置好相应的 Canonical-Resource,就直接解决了 diff 路由归纳名的问题。

而且,我们根本不需要真的到线上去录制,伪造一份这样格式的 log,甚至还可以直接修改补全一些没有的场景进去,就可以直接以此为范本,作为回放 log 的效果了。

这样也根本不需要担心线上录制会影响线上服务器的性能和资源。

解决所有问题以后,还有什么不优雅的地方

那 log 我也得复制粘贴去生成,而且 log 里面的时间戳排序,我也得自己造,这样看似方便,其实只是方便了不用手写代码来编造测试数据,可以直接通过编写 log 就能回放流量。

也就是,这样的方案,只是降低了测试技术栈的门槛,提高了一点点的效率。

而且还有个问题,很多的数据,我其实是动态生成的,我传进去之前,还得通过其他接口去获取返回值,再动态填进去,这样写 log 并不能实现啊。

还有,很多参数也有时效问题,过段时间 token 过期了,我替换 token 也很麻烦。

就算,设置成万能的 token,那涉及到用户的数据,比如有些业务场景 token 里面包含了某类用户具体信息的时候,万能 token 就不管用了,因为有很多自定义的数据要去测。

所以,看似解决完所有技术栈问题以后,其实还有很多业务问题,导致它使用场景有限,甚至无法完全落地。

正确打开方式

为什么要拘泥于用线上流量来回放呢?

如果我的脚本能够批量构造大量且覆盖众多场景,且可高度自定义的请求,再将这些请求直接去请求 diff,不就能直接对比出前后有什么差异吗?

何况,就算我的内部 rpc 服务调用更改,变得更加复杂,但是暴露在外给用户的业务操作,是不会发生大改的。

而且此前,基于项目 shaonian/boomer_locust 的压测工具,我之前已经实现了全链路压测的业务逻辑覆盖。

所以这里完全可以引出一个全新的概念,用可控速度的压测工具,以及高度灵活的编程脚本,实现大批量构造测试数据,模拟业务场景压力,并直接实现前后对比差异的不同。

因为数据全部都是新构造好的,所以不止 GET 请求我可以做,POST PUT DELETE 请求我也可以,因为数据都是我构造上传的,如果在测试环境中,完全删掉都不会有影响,而且只要设置好前后的测试脏数据的清理,其实线上数据库都能做。(当然,直接做到 stage 环境数据库就可以了,prod 没必要。)

进一步完善

既然正确打开了前后版本的快速 diff 测试,那么如何进一步完善呢?

当然是提高脚本的业务覆盖场景,已经代码覆盖率。

如何判断自己的构造回归流量,尽可能覆盖完全呢?

我们可以引入代码的实时染色,在本地就先测好覆盖率,再去部署上线。

这个代码实时染色,可以基于 goc 在 vscode 的插件来实现。

至此,快速构造测试数据,对比前后版本的方案成型,且可根据业务定制脚本,可落地实现,真正意义上地实现回归 diff 测试。

由此为基础以后,下一步,当然就是精准化测试,也是未来测试的大势所趋。

精准测试的概念:
借助一定的技术手段、通过辅助算法对传统软件测试过程进行可视化、分析及优化的过程,使得测试过程更加可视化、智能、可信和精准。


精准测试的目标:
精准测试的核心思想就是使用非常精确和智能的软件来解决传统软件测试过程中存在的问题,从根本上引领从经验型方法向技术型方法的转型。
质量的评估不再完全靠个人经验和业务熟悉度,而是通过精准的数据来判定。
在测试资源有限的前提下,将用例精简到更加有针对性,提高测试效率,有效的减少漏测风险。

精准测试的核心 - 双向追溯:

正向追溯: 开发人员可以看到测试人员执行用例的代码细节,以方便进行缺陷的修复,测试数据可以直接为开发调试提供依据,快速定位并修复缺陷。
逆向追溯: 测试人员通过修改的源代码快速确定测试用例的范围,极大减少回归测试的盲目性和工作量,快速修订测试用例,达到测试覆盖率最大化。

PS:技术交流 QQ 群 552643038

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
最佳回复
monster 回复

按日志格式造 request 实现不难,难的是把业务逻辑和规则融入进去,就比如你的第二个问题,才是第一个问题的核心。

举个例子:

线上日志

GET /product
HOST prod.com
PARAM token: old-token
PARAM name: 1-prod-product

发起 prod 请求前,复制一份到 test,更改 HOST 和 匹配规则

if host == "test.com" && "url == /product" 匹配 替换类规则 RULE [1,2]

RULE 1 代表登录类替换逻辑,若 PARAM 匹配到 token cookie ..., 替换成万能 token 或调用函数造 token。
RULE 2 代表关联类替换逻辑,若 PARAM 匹配到 name ... ,根据 get_product(host,token) 获取对应环境的值并替换。

GET /product
HOST test.com
PARAM token: new-token
PARAM name: 2-test-product

然后同时发出,然后 diffy json(response_prod, response_test)

这里还可以做噪音匹配规则,这样对照组都能省,直接去噪。
比如增加 NOISE_RULE [1]
NOISE_RULE 1 代表通用类去噪音规则,diffy --ignore time, createtime, updatetime...

最后,再借助 boomer 登记 diffy,就可以自己直接实现 diffy 归纳路由。

if diff := jsondiff.Diff(response_prod, response_test, diffopts.IgnorePaths([]string{"/time"})); diff != "": {
    boomer.RecordFailure("GET", "/product", 0, string(diff))
}

PS:

正确打开方式之前的两幅图,一个是 buger/goreplay 的回放,以及 opendiffy/diffy 的差异对比,这也是传统模式的 录制 + 回放,主要讲解传统模式 录制 + 回放 的原理,以及落地以后的一些缺陷。

而后提出的优化,也是基于体验这个流程以后,弥补其中的一些不足,并提出优化实现方案。

共收到 36 条回复 时间 点赞
少年 回复

你改了是生效在新版本的,为啥还要让改动在老版本生效?你只能说改完大不了回放在旧版本里面会返回 error ,那也算 diff。

  • 你测试新版版,不是以老版本的 response 作为对比的基准么,我是说接口入参改了新增了一个入参 userId,老版本的 response 就不可靠了,这种情况不适用

归类是同一个路由的归类,不带任何参数的归类。

  • 截图里面不是很清楚的有 get /ping1 么,header 或者 url 里面不能直接拿得到吗

这个东西作用是有,但是感觉适用范围不是很大
另外,这个主要是在接口路由不改且接口入参没有改动的情况下,来快速测试一些优化代码版本,或者重构代码版本,或者升级架构如 rpc 版本的快速回归测试

本质上是 diff 接口,你接口路由不改,新旧版本都有解析路由参数的逻辑,都能用。要是你接口路由都改了,那就没有可比性了,都不是一个接口了。另外,这个主要是在接口路由不改的情况下,来快速测试一些优化代码版本,或者重构代码版本,或者升级架构如 rpc 版本的快速回归测试。

简而言之,就是你后端服务的前后迭代,不需要前端去更改业务调度逻辑和更改接口的,都能用。或者,你只要确保同一个请求,传入的参数(比如一些随机生成和查数据库传入的不同的值)在前后版本都有逻辑可以处理,也能用。

少年 回复

接口入参改了,旧版本的结果就完全没用了吧

少年 · #2 · 2020年10月13日 Author
仅楼主可见
仅楼主可见

快速构造的测试数据,对比的基准是啥?

就是新旧版本的镜像对比,以旧版本镜像为基准,对比新版本镜像 response 的 diff。

你只要确保同一个请求,传入的参数(比如一些随机生成和查数据库传入的不同的值)在前后版本都有逻辑可以处理,也能用。
这意思还是不行嘛,改了入参没有改 urlpath,少了入参也还行,多了或者改了老版本就完全不可靠了。

另外这个 diff 归类是什么操作?请求里面没有 path 吗?

没懂你想要干嘛?你改了是生效在新版本的,为啥还要让改动在老版本生效?你只能说改完大不了回放在旧版本里面会返回 error ,那也算 diff。

归类是同一个路由的归类,不带任何参数的归类。

感觉现阶段流量录制回放是个伪命题啊,不解决外部服务、数据库跟中间件的依赖很难落地,后边要做的东西很多

少年 #12 · 2020年10月14日 Author
截图里面不是很清楚的有get /ping1么,header或者url里面不能直接拿得到吗

diffy 设计里面不以 url 来归纳的,毕竟参数不同,所以是通过人为在回放前,自己在回放流量的 header 里面增加 Canonical-Resource 来实现归纳。

你测试新版版,不是以老版本的response作为对比的基准么,我是说接口入参改了新增了一个入参userId,老版本的response就不可靠了,这种情况不适用

主要是看有哪些 diff,比如这个就是我想要的 diff,我要看的是,除了我预计之外的 diff,有没有什么我没有预计到的 diff,而不是追求没有 diff 。

少年 #13 · 2020年10月14日 Author
cool 回复

是的,录制的限制太多了,所以用脚本快速构造流量,只追求 diff 的实现。

第一个疑问:文中的流量录制,在测试环境回放,是不是需要测试环境的数据和线上的数据是一致的?举个例子,线上的用户获取自己的用户信息这种。
第二个疑问:文末最后说完善 diff 框架,直接不做线上流量录制,其实是线上流量录制被放弃了吗?
@ShaoNianyr

少年 #15 · 2020年10月15日 Author
恒温 回复

是啊。

其实我最后想表达的意思是,我们不应该只局限于 录制 + 回放 这两个组合,能实现同样测试效果的组合,为什么不能组合使用呢?

比如:

  • 录制 + 规则校验(所需数据不同的测试,通过规则校验来实现)
  • 造数据 + 回放(既然是我造的数据,又不是录的生产数据,那我 POST PUT DELETE 完全没毛病)

甚至可以两者按需组合。

帮同学问,

对文中"录制"有疑问,高度自定义的构造数据脚步提高覆盖场景,怎么看起来等于做接口自动化 + 压测呢?那线上数据如果还是得脚本去造,回放还有意义吗,完善接口自动化覆盖度就完了吧

少年 #17 · 2020年10月15日 Author
恒温 回复

可以理解为,把线上的流量录制成特定的日志格式,脚本是解析日志格式去发请求的,只要格式保持一致,就通用于大部分的项目。然后,涉及标记的动态数据得替换生成。

不过能理解成接口自动化 + 压测也对,原理都是把请求经过处理,同时对多个服务发出,并实时 diffy 响应。只不过一个是手写代码构造 request 一个是读线上日志构造 request。

最初纠结于没有利用到线上数据,但是楼主的做法提醒了不止关注 “录制” 和 “回放” 的传统做法,只要能通过一些手段拿到覆盖更全的数据去伪造接近线上数据的场景,也能做到有效的回放(发布后快速回归功能),就是不用局限于线上流量录制这种方式。

再求解:

  1. 从线上环境拿接口请求日志来二次处理,伪造成录制需要的格式有难度吗;
  2. 像登录 token 或者 cookie 这种有时效的参数,要保证回放能请求通过,怎么对应上录制此请求时的用户帐号呢?
少年 #20 · 2020年10月15日 Author
恒温 回复

具体例子已给出 ~ 见最佳回复

恒温 将本帖设为了精华贴 10月15日 23:31
monster 回复

按日志格式造 request 实现不难,难的是把业务逻辑和规则融入进去,就比如你的第二个问题,才是第一个问题的核心。

举个例子:

线上日志

GET /product
HOST prod.com
PARAM token: old-token
PARAM name: 1-prod-product

发起 prod 请求前,复制一份到 test,更改 HOST 和 匹配规则

if host == "test.com" && "url == /product" 匹配 替换类规则 RULE [1,2]

RULE 1 代表登录类替换逻辑,若 PARAM 匹配到 token cookie ..., 替换成万能 token 或调用函数造 token。
RULE 2 代表关联类替换逻辑,若 PARAM 匹配到 name ... ,根据 get_product(host,token) 获取对应环境的值并替换。

GET /product
HOST test.com
PARAM token: new-token
PARAM name: 2-test-product

然后同时发出,然后 diffy json(response_prod, response_test)

这里还可以做噪音匹配规则,这样对照组都能省,直接去噪。
比如增加 NOISE_RULE [1]
NOISE_RULE 1 代表通用类去噪音规则,diffy --ignore time, createtime, updatetime...

最后,再借助 boomer 登记 diffy,就可以自己直接实现 diffy 归纳路由。

if diff := jsondiff.Diff(response_prod, response_test, diffopts.IgnorePaths([]string{"/time"})); diff != "": {
    boomer.RecordFailure("GET", "/product", 0, string(diff))
}

PS:

正确打开方式之前的两幅图,一个是 buger/goreplay 的回放,以及 opendiffy/diffy 的差异对比,这也是传统模式的 录制 + 回放,主要讲解传统模式 录制 + 回放 的原理,以及落地以后的一些缺陷。

而后提出的优化,也是基于体验这个流程以后,弥补其中的一些不足,并提出优化实现方案。

少年 #24 · 2020年10月19日 Author
泰斯特 回复

hhhhh

还是那熟悉的文章风格,把题目改成 录制录上录 就更完美了 233

请教下楼主,goreplay log 中第一行的字符串分别代表什么

少年 #26 · 2020年11月19日 Author
pablo 回复

时间戳,上下游响应之类的关联信息,你要想伪造那份 log 不好搞,不如直接自己写个 log 生成 request 的发生器

少年 回复

好像明白楼主的意思了就是抛弃传统 gorepaly+diffy 的模式,去做 “录制 + 回放”。可以是自己定制伪造请求,然后 diff 前后差异也可以通过保存版本镜像对比 response

对写接口怎么处理,例如写缓存、数据库、外部调用

仅楼主可见

接口测试工具可以试用一下国产的接口测试和接口文档生产工具 apipost:https://www.apipost.cn/?dt=2020

少年 #31 · 2020年12月30日 Author
jun3.wj3 回复

写接口是不能直接回放的,会涉及到很多数据操作和脏数据的问题。

如果要回放,需要代码入口处封装一层 fakeDB,然后实现 fakeSqlQuery,然后所有符合某种规则的 sql 都 mock 返回一致的数据。

这些东西我们搞了一整年了,反正坑不少。需要专业的开发团队投入大量时间搞。

少年 #33 · 2020年12月30日 Author
eryi 回复

是啊,落实下来工程量还是很大的

少年 回复

不光是工程量,这里很多东西,测试开发一般是搞不定的,得基础团队做,偏中间件一点

少年 #35 · 2020年12月31日 Author
eryi 回复

回放对比的时候,回放路径的所有中间件和数据库肯定要保持一致性,所以如果是修改适配中间件保持其一致性,成本有点高,还是做规则校验和 interface 层面的 fake 与 mock 更解耦一点。

请问一下,你回放使用 diffy 降噪的时候有遇到过 java.lang.StackOverflowError: null 这个问题吗?
我大概回放的 1000+ 条数据 diffy 服务就会报错
java.lang.StackOverflowError: null
at java.util.regex.Pattern$Branch.match(Pattern.java:4618)
at java.util.regex.Pattern$BranchConn.match(Pattern.java:4582)
at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)
at java.util.regex.Pattern$Curly.match0(Pattern.java:4293)
at java.util.regex.Pattern$Curly.match(Pattern.java:4248)
at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)
at java.util.regex.Pattern$Branch.match(Pattern.java:4618)
at java.util.regex.Pattern$Branch.match(Pattern.java:4616)
at java.util.regex.Pattern$BmpCharProperty.match(Pattern.java:3812)
at java.util.regex.Pattern$Start.match(Pattern.java:3475)
at java.util.regex.Matcher.search(Matcher.java:1248)
at java.util.regex.Matcher.find(Matcher.java:664)
at java.util.Formatter.parse(Formatter.java:2549)
at java.util.Formatter.format(Formatter.java:2501)
at java.util.Formatter.format(Formatter.java:2455)
at java.lang.String.format(String.java:2940)
at com.fasterxml.jackson.core.base.ParserMinimalBase._reportUnexpectedChar(ParserMinimalBase.java:587)
at com.fasterxml.jackson.core.json.ReaderBasedJsonParser._handleOddValue(ReaderBasedJsonParser.java:1902)
at com.fasterxml.jackson.core.json.ReaderBasedJsonParser.nextToken(ReaderBasedJsonParser.java:757)
at com.fasterxml.jackson.databind.ObjectMapper._readTreeAndClose(ObjectMapper.java:4042)
at com.fasterxml.jackson.databind.ObjectMapper.readTree(ObjectMapper.java:2551)
at ai.diffy.lifter.JsonLifter$.decode(JsonLifter.scala:52)
at ai.diffy.lifter.StringLifter$.$anonfun$lift$1(StringLifter.scala:9)
at com.twitter.util.Try$.apply(Try.scala:26)

在路上 服务端接口测试指南 中提及了此贴 05月08日 17:28

请问下您的差异去噪,是本身相同环境两次相同请求结果的差异去噪?

moku 如何让线上录制回放落地的具体思路 中提及了此贴 10月16日 21:01

真实场景中 同一个请求在 3 个环境不能同时有效 (cookie 校验失败,接口无权限),这种情况,大佬你们是怎么做的?

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册