01

前言

很多同学可能都听说过流量回放这个概念,但是会发现它比很多工程都更难推动落地,原因无外乎两点:

1. 常规自动化技术都可以通过开源方案或者二次开发方案解决:

这在流量回放上实施难度很大,原因是因为其高度依赖于公司内部的后端服务状况、环境条件、技术架构。而开源的工具也许可以解决核心思想问题、核心技术问题,或给我们一些参考,但实际的工具开发,如录制手段、回放的验证则因上述原因,几乎需要测试人员根据自己团队现状,部分甚至全部自己进行规划。

2. 回放的验证过于严格或过于宽松,导致经常误报或发现不了问题:

本篇主要是分享下我们团队从开始实施传统的流量回放到目前基于LLM的流量回放的心路历程,我们的思考以及解决方案,以及阐述这套系统的作用和效果, 希望可以帮助到您或给您一些灵感。

02

流量回放系统的作用

2.1 真实性

流量录制回放系统是以线上服务的返回作为基准的,所谓的“回放”,更多的是指将曾经线上用户使用的功能,在一个与线上环境相同的线下环境中进行回放,去验证接口的功能是否还正常。

流量的真实性实际包含着很多点:

2.1.1 UA多样性 : 测试人员自己编写的自动化脚本对于UA敏感的接口几乎毫无办法,因为无论线下模拟了多少UA,都不如真实的用户在真实的设备上产生的UA准确。

2.1.2 用户画像: 当前很多推荐系统都接入了复杂的算法,甚至动用了深度学习模型去给用户画像, 测试人员自己编写的自动化测试脚本很难模拟用户画像,这就导致反复用同一个用户身份测试一个接口,它出的数据几乎没有变化或干脆命中了缓存没有走真实的代码,而测试人员对此是毫无感知的。流量回放使用的是线上用户的真实令牌即“他就是他”,用这种方式可以比较客观的验证不同用户请求的个性化结果是否正确,也可以避免命中缓存触发服务保护等情况的出现。

2.1.3 针对性强: 测试人员很难清楚的得知,当前某个服务哪个接口的线上调用最多,即使接口路径层面有迹可查,想知道线上调用最多的接口以及其最多的请求参数、请求头就几乎没有任何办法了,因为无论是哪个生产端的服务都不可能(对于开发人员来说也没必要)存储所有的接口信息,占用大量日志存储空间。 流量回放系统回放的流量都来源于线上用户的真实使用数据,在每一个上线之前的回归测试中,都可以将使用人数较多的热点接口进行客观的回归,以保证它基本可用。

2.2 数据参考

基于线上多样性的请求数据、用户数据,即便是S0、S1级别的非常重要的接口,要求必须要进行手工自动化脚本编写的接口,也可以提供大量线上真实数据, 它可以是请求参数多样的,也可以是请求头多样的。在实际的人工编写自动化脚本时,会有比较真实的参考。

2.3 规模化

在敏捷化开发遍地的现在,根本不存在永恒不变的服务框架和后端服务结构, 经常会有诸如以下的测试场景:

2.3.1 需求或产品形态改变引起的服务合并: 当两个微服务进行合并时,这属于比较普遍的后端调用结构改变, 在进行测试回归时, 如何保证该合并后的服务接口依旧还能正常使用是具有挑战性的,因为服务的合并意味着代码的新增和删除,这种改动是无预期的,必须进行规模化的回归。

2.3.2 服务迁移: 服务上云、换云、换机房等需求,需要进行回归测试, 这种情况下因为受到多层服务结构的影响(如下图所示),我们根本无法判断需要迁走的服务到底会产生多少影响。

(服务的调用是分层次的)

2.3.3 服务技术升级: 服务端的技术非常多,无论是框架还是各种库,可以看到我们测试组自己的设备管理服务都用到了很多库(如下图所示),更不要说是企业级的生产服务,然而,服务升级又是不可避免的,他可能是主动的,例如升级以支持新特性,也可能是被动的,例如当前版本有重大漏洞,这种改动也是几乎没有预期的,测试边界无限,如何进行规模化的回归测试也是重大挑战。

2.3.4 数据库或者消息的账号更改:  例如MYSQL数据库密码变更、连接ip地址变更,kafaka消息topic变更等等,这些回归测试都需要大面积且简单的回归验证,从测试的质量角度出发,它们应该是越多越好,越全越好的,且用户调用多的接口,显然应该更加优先的进行测试。

2.4 增强测试信心

在测试工作中,自动化如何降低人力根本在于有多少功能点可以完全被自动化的机器所取代,如果自动化验证的功能依旧无法判断是否可用,最终还需要人工再次验证确定或者过多的脚本、过长的流程导致误报率高、执行失败率高、维护成本高造成麻木心态泛滥,人不相信自动化的产出结果,那么一旦恶性循环开始(如下图所示),那降低测试人力便成了空谈。

03

传统的流量回放系统介绍

我们团队在去年实现了传统的流量录制回放系统,并对接了我们自己的devops平台,每当服务进行预发环境部署的时候,就会触发我们的流量回放进行批量的验证。

因为我们团队对接了腾讯云,所以在录制方面,我们直接对接了线上的nginx,在线上的nginx对流量进行采样后,写入日志,腾讯云会将日志自动的写入对应的kafaka,当流量录制回放系统从kafka中取出数据后,会对流量数据进行整理并入库,流程如下图:

值得一提的是, 流量在存入之前会对返回体进行降维,我们使用如下图的代码,将返回的json结果进行扁平化,使其变成一维的数据,这样返回体将可以被聚合和对比,我们将这种被降维的返回体称之为返回形状, 即: response_shape。

3.1 流量回放

回放方面,主要是以下三个流程:

3.1.1 测试用例集

我们的测试平台可以通过点选的方式将接口绑成一个测试用例集合,当进行回放的时候,回放系统会通过测试用例集合中的接口信息,去返查这些接口的流量信息,如下图所示:

(测试平台,测试集合的接口绑定信息,接口路径已被打码处理)

3.1.2 具体回放过程

去重逻辑:

①  先对返回形状进行去重,获取到这种返回对应的所有的请求参数,请求头。

②  对请求参数进行去重。

③  将请求头中的UA和用户token取出,进行去重操作,请看下图代码:

之后采用开源队列任务管理库celery实现解析测试用例模版,执行请求,得到返回并对比及最终将结果入库:

测试用例模版是一个json格式:

{

"host": "xxxx",

"request\_path": "/a/b/c",

"request\_headers": [去,重,后,的,header,数,组],

"request\_params": [去,重,后,的,请,求,参,数,数,组],

"request\_methd": "POST",

"response\_shape": ["data.user,data.name,data.age,data.sex,data.im.0.success,data.im.0.message","xxx","xxx"]

}

3.1.3 接口验证

接口的对比验证说明:

可以看到接口返回的对比是一个越来越松散的过程,首先我们进行最为严格的匹配,如果有不满足条件的则会被漏到下一个不太严格的匹配,一直到最终。

实际之前的所有操作都是为了这一步的验证,其验证方法非常繁琐。 原因是因为流量回放不会单独考虑某个接口或者服务,而是尽可能的通用,但通用和质量校验在这里成为了一个此消彼长的关系,即越不通用验证的点就越多,质量校验越严格,反之也成立。

严格匹配的代码如下:

通配匹配的代码如下:

04

基于LLM的流量录制回放系统

4.1 大模型对流量录制回放系统的赋能

经过上述介绍大家也不难发现一个问题,传统的流量回放系统,存在两个较大问题,几乎是无法解决的。

4.1.1 无法做到既精准又通用的验证

在流量回放结果对比那一段,我也提到了,测试的通用性和质量校验的强弱成为了一个此消彼长的关系, 我也在调研的过程中看到过有人尝试使用对象相似性算法解决这个问题,感兴趣可以移步: https://testerhome.com/column_channels/39039

但是这种方案顶多就是面多加水,水多加面的过程,没有从根本上解决这个问题, 造成此问题的根本原因在于,如何定义接口返回《正确》,程序并不能理解当前这个接口的返回具体是什么,所以我们需要从千万量级别的接口返回中找寻相同点去验证,例如: 返回状态是200、返回是json格式的、一个key必然对应一个值、每个值都有数据类型, 这当然就使得对接口验证很笼统, 但又没办法一个个接口路径去判断,该怎么办呢? 我们的想法是,LLM就可以做到这一点。

4.1.2 对于状态关联的接口无法操作

状态相关的接口,比如发评论、改头像昵称这类接口,一直是流量回放领域很难解决的问题,因为录制的是线上流量, 如果在测试环境回放,因为线上线下不共库, 接口几乎必然会报错, 除非线上的某个id,在线下正好也有。

在预发环境回放也不现实,因为预发环境与线上共库, 会影响或者污染线上的数据, 即使将用户改成测试人员自己的账号,依旧不保险, 因为发出的评论 、 发出的文章等也会被线上其它用户看到, 针对于这个问题,经过调研, 我们采用了LLM 外挂测试知识库的方案,这里先介绍下整体流程,如下图:

4.1.3 基于LLM大模型的流量回放具体方案:

① 数据准备阶段:

流量录制回放平台在每天的凌晨,会将所有的数据整合,交由AI处理出一份测试脚本。

具体的去重规则:

· 首先先从ES中取出这个服务下的某个接口路径下的所有返回形状的去重聚合结果,因为返回形状的存在,使得可以知晓,什么样的请求头、请求参数、会对应服务这样的返回。

· 上文中介绍到,为了查询性能更好,将大文本存在了ES,小文本信息存在了MYSQL,他们使用一个永不重复的key关联,这个key也是ES文档ID,所以现在得到了返回形状的去重聚合结果后,我们不仅得到了请求头的结果集合,也得到了文档ID,之后使用这些文档ID对应去MYSQL库中查询对应的请求参数,这个时候我们会发现,其实很多请求参数和请求header都是重复的,采用去重策略去重。

· 首先解析请求header,如果有用户token,则对用户token去重,因为数据多样性的关键,在于用户的多样性,其次才是基于用户的场景细分(请求参数), 在对用户token去重后, 得到最终的结果,如果header没有用户token, 则会忽略请求头, 直接处理请求参数。

· 无论是GET请求或者POST请求,其实都是很多的key、value的组合,那么去重的优先级是, 优先key不同的数据, 这个不同包括key的绝对值不同,也包括key的多与少,如图中所示,当通过这种方式去重后,一般每个接口路径下,一种返回形状,可能只能剩下5个以内的数据了,但是这会出现一个问题,因为请求参数可能会有一些id,他们的值不同则代表了不同的文章、用户,去重后会造成这类数据的减少,但这种问题几乎无法预计,所以这里直接采用了最简单的办法,对已经去重的数据做随机补充。

· 经过长时间的调研,随机补充数据在5-6个是性价比最高的, 提问不会因为数据太多或太少影响AI的回答结果、也不会造成回放速度太慢。

{

`    `"request\_path": "/request/path",

`    `"request\_method": "GET",

`    `"host": "host",

`    `request\_infos: {

`        `"code,msg,author": {

`            `"request\_headers": [

`                `{request\_headers},

`                `{request\_headers},

`                `{request\_headers},

`                `{request\_headers},

`                `{request\_headers},

`            `],

`            `"extra": [

`                `extra,

`                `extra,

`                `extra,

`                `extra,

`                `extra,

`            `],

`            `"response\_body": [

`                `{response\_body},

`                `{response\_body},

`                `{response\_body},

`                `{response\_body},

`                `{response\_body},

`            `],

`            `"reqeust\_params": [

`                `"reqeust\_params1",

`                `"reqeust\_params2",

`                `"reqeust\_params3",

`                `"reqeust\_params补充1",

`                `"reqeust\_params补充2",

`            `],

`            `"request\_bodys": [

`                `{request\_bodys1},

`                `{request\_bodys2},

`                `{request\_bodys3},

`                `{request\_bodys补充1},

`                `{request\_bodys补充2},

`            `],

`            `"fingerprint": [

`                `"fingerprint",

`                `"fingerprint",

`                `"fingerprint",

`                `"fingerprint",

`                `"fingerprint",

`            `]

`        `},

`        `"code,msg,user.$0.id,wildword.$0,data.name": {

`            `"request\_headers": [

`                `{request\_headers},

`                `{request\_headers},

`                `{request\_headers},

`                `{request\_headers},

`                `{request\_headers},

`            `],

`            `"reqeust\_params": [

`                `"reqeust\_params1",

`                `"reqeust\_params2",

`                `"reqeust\_params3",

`                `"reqeust\_params补充1",

`                `"reqeust\_params补充2",

`            `],

`            `"request\_bodys": [

`                `{request\_bodys1},

`                `{request\_bodys2},

`                `{request\_bodys3},

`                `{request\_bodys补充1},

`                `{request\_bodys补充2},

`            `],

`            `"response\_bodys": [

`                `{response\_bodys},

`                `{response\_bodys},

`                `{response\_bodys},

`                `{response\_bodys},

`                `{response\_bodys},

`            `],

`            `"fingerprint": [

`                `"fingerprint",

`                `"fingerprint",

`                `"fingerprint",

`                `"fingerprint",

`                `"fingerprint",

`            `]

`        `}

`    `}

}

· 最终得到的结果如上, 需要说明的是,可能细心你会发现,为什么返回形状里会有很多$符号和wild\_word,这是因为我们的服务不幂等且会有随机值做key的情况,还会有纯数字的字符串做key,为了兼容处理这些情况,遂采用$+数字代表数组下标,纯数字将会被认为是返回体中实际key的值,我们将随机生成的key(key中包含数字+字母),统一替换成了special_word,因为它在测试回放时没有意义。

② 数据存储阶段:

流量录制回放平台在每天的凌晨,会将所有的数据整合,交由AI处理出一份测试脚本。

AI的调度方面,使用了DIFY工作流来处理,工作流流程如图,这个流程比较详细了:

实际的工作流程起点是将一个类似上面提到的去重补充的数据结构,给到工作流,与此同时,还会给上次的数据,上次的自动生成脚本,上次的人工审核脚本。

工作流会先判断这个接口路径之前有没有生成过,如果是第一次,就直接生成脚本了,如果不是,需要先根据上次的结果和本次的数据,通过大模型分析得出是否需要更新,这块因为一些原因,我不能把prompt给出,不过你可以遵循下面的逻辑:

· 不好用语言表达的,可以直接给示例代码,更适合模型理解。

· 如果上下文太多,可以拆分,一个模型负责接收原始数据出验证建议,一个模型根据建议和关键数据生成脚本,一个模型负责验证生成的代码符合代码示例和验证建议,并且可以执行。

· 对于接口信息,markdown格式更适合模型理解。

好,说回来,如果大模型判断需要更新,则继续走更新的逻辑。

在工作流中,将数据转化为markdown的格式,代码如下:

③ 数据落库以及前端联动规则:

· AI在生成对应的脚本后,存在下面几种情况:

- ai_script没有记录,这说明这个接口就没有生成过脚本,直接写入即可,并将ai_update字段更新成new。

- ai_script有记录, 但是human_script没有记录, 这说明这个路径一直没有被人工审核, 直接更新ai_script即可,并将ai_update设置成new。

- ai_script有记录, human_script也有记录, 这说明这个脚本之前被人工审核过, 这个时候需要将已有的ai_script和human_script都取出来,加上本次组装的prompt,询问AI是否需要更新,如果不需要则废弃, 如果需要则入库,并将ai_update设置成new。

· 测试人员会在测试平台看到对应接口的new标签,说明此接口需要更新,在人工审核之后,会将脚本更新到human_script , 并将ai_update设置成done,这样前端就不展示new的标志了,如下图:

(接口路径信息已做打码处理)

(审核页面,关键信息已做打码处理)

④ 回放

· 数据绑定

前端测试平台支持将不同的接口路径绑定成一个测试集合,这个上面已经说过,不再赘述,当一个用户选定一个测试用例集合时开始回放时,则会针对这些接口数据,进行整合。

· 执行&数据收集

数据收集阶段,因为每个接口的路径都对应了审核通过的测试脚本,所以直接将测试脚本查出来在服务端进行运行即可,日志会统一收集在一起且当脚本验证某个字段出错时,脚本只会报错不会中止,会将所有发现的问题都统一输出在日志中,通过分析这个log,就可以知道具体是否通过。数据收集流程如下图:

值得一提的是,对log日志中的错误,我们也会使用大模型帮我们分析问题所在,它会进行错误的整理,一旦有错误,告警信息将非常直观可读,在执行task时就比较简单了,系统直接将代码拿过来运行就可以了,对于系统来讲,这只是一份python的脚本代码,非常好维护:

05

后期规划

5.1 目前的产出:

5.1.1我们对接了devops流水线,在服务部署预发环境和测试环境,会进行回放(写接口回放在测试环境,读接口回放在预发环境)。

5.1.2 将团队所有的业务后端都接入了进来,共录制了接口257个,根据其返回形状生成的脚本583个。

5.2 这套系统目前还存在一些问题亟待解决

5.2.1 审核耗时越来越高: 因为脚本需要人工审核,随着接口数量越来越多,脚本也就越来越多,当有较大的业务改版,接口大量发生变化时,审核难度会很高,所以后期如何减少审核时间是一个难题。

5.2.2 测试结果有间隙期: 录制的流量只存当前只存过去3天的,如果有老接口加减了字段,那么会有3天的间隙期,这期间,接口老版本的脚本还会继续执行,新版本也会执行,这个时候老版本就会报错,3天以后此情况就会消失,老版本的接口脚本也会一并删除,如何减少甚至消除这个间隙期需要后期解决。

5.2.3 测试结果不稳定:  因为接口并不幂等(这块具体的团队可能表现不同),所以会存在返回形状非常多的状况,一个接口路径对应很多返回形状,那么此接口的回放结果就会不稳定,如何保证忽略一些字段或者合并一些返回形状,加强回访的稳定性,也是一个需要解决的问题。

06

结尾

我们讨论了从为何要进行流量回放,到搭建一个传统的流量回放系统,发现了它的不足,到实现了一个基于LLM的流量回放系统的心路历程已经介绍完了,希望会对大家有用或给大家一些启发。


Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐