引言
本文主要介绍一种流量回放技术,通过采集底层流量数据,使用创新的Mock方式将数据应用在功能回归测试场景,更好的保障业务迭代质量。
应用场景
- 日常上线回归测试:确保每次上线核心流程不受影响,可以接入pipeline流水线。
- 开发重构类需求:微服务拆分、框架升级、组件升级等。
应用特性
- 线上流量测试:更真实、更全面、测试用例维护成本更低。
- 支持写接口回放:所有下游调用都会被mock返回,不会真实写入数据库,不会污染应用数据。
一、背景
1.1、背景
需求改动难以评估影响面,这是很多业务面临的痛点问题,接下来介绍两个真实案例。
1、真实案例一:组件升级,导致线上服务查询超时。
- 原因:老版本组件查询数据库时会自动类型转换(int -> string),新版本不会,导致查询没命中索引。如果下图所示,如果id字段在数据库是字符串类型,组件升级前会命中索引,组件升级后不会。
2、真实案例二:框架升级,引发多起工单问题。
- 原因 :Laravel框架由5.5.44升级到5.5.48,Cache设置有效期参数由分钟改成了秒,官方无任何发布说明。(参考:Laravel 5.5.48 的巨坑)
小结:以上案例常见测试手段都无法提前发现问题,故障复盘的时候没有很好的改进措施。
1.2、开源方案
常见开源的回归测试解决方案,有:tcpcopy、goreplay、diffy等,适合性能测试(真实流量压测)、适合只读接口功能回归测试等。
常见开源方案存在以下痛点问题:
1、不支持写接口回放:写接口存在非幂等性,有脏数据风险。
- 非幂等性:写接口每次回放结果可能不一样,比如:第一次回放提示写入成功,第二次回放可能提示主键冲突,每次回放差异巨大没法对比测试。
- 有脏数据风险:回放的时候会真实写入数据库,可能会因为你的测试导致数据库存在很多脏数据。
2、依赖下游测试环境:依赖下游服务和下游数据源。
- 依赖下游服务:下游服务必须稳定才能进行测试,随着微服务的拆分这可能会是一个挑战。
- 依赖下游数据源:线上日志获取的请求在线下测试可能行不通。比如:同一个用户ID线上有数据,线下测试可能没有数据,因为依赖的数据源不一样。
以下列举几个支持写接口回放的开源解决方案,后续我们实现的时候会重要参考。
- rdebug:滴滴开源的php解决方案
- sharingan:滴滴开源的go解决方案
- jvm-sandbox-repeater:阿里开源的java解决方案,MTSC 2019 年度开源贡献奖。
二、原理
2.1、基本原理
基本原理:录制服务线上流量,通过回放对比发现潜在问题。先流量录制,再流量回放。
1、流量录制:上图绿色背景部分,以线上网络流量采集为主。
- 思路:不仅录制接口请求和返回(上图1和8),还录制接口所有子调用请求和返回(上图2到7),将这些流量(上图1到8)都存储起来。
- 难点:如何采集线上服务网络流量?(技术点:
流量拦截
)、如何将网络流量串联起来?(技术点:链路追踪
)
2、流量回放:上图红色背景部分,以线下网络流量对比为主。
- 思路:回放过程中,会保持所有输入一致(含子调用返回值),对比所有输出的差异(含子调用请求差异),噪音、时间等干扰因素会被排除。
- 难点:如何重定向到Mock Server?(技术点:
连接重定向
)、Mock Server如何匹配流量返回?(技术点:流量匹配
)
3、原理应用:各语言实现方案有差异,本文主要介绍PHP和JAVA实现。
- PHP语言:主要参考开源的解决方案,做了部分改进,如:平台化改造、并发回放支持、存储优化等。
- JAVA语言:基于上述原理创新实现,首个基于网络层流量实现写接口回放的方案。
2.2、PHP开源方案
PHP开源方案实现用到了动态库加载技术,先做个简单的介绍。
2.2.1、动态库加载技术
- 动态库加载技术:使用LD_PRELOAD优先加载动态链接库。LD_PRELOAD是一个Linux环境变量,允许程序运行前优先加载动态链接库(可以用来写游戏外挂)。
- 动态库加载应用:如何知道应用程序在执行期间打开了那些文件?(方案之一:可以通过重写libc open函数动态加载来实现)
2.2.2、流量录制实现
1、流量拦截
:获取上图1、2、3、4流量。
- 方案:通过动态库加载技术,重写libc的read/write函数完成。PHP应用所有网络调用都会经过libc,重写网络流量读写函数可以获取网络流量。
2、链路追踪
:串联上图1、2、3、4流量。
- 方案:利用语言特性,通过线程ID串联。PHP请求通常在一个线程内串行完成,通过libc拦截流量很容易获取线程ID。(优势:无侵入)
2.2.3、流量回放实现
1、连接重定向
:所有下游调用连接重定向到Mock Server ,由Mock Server匹配线上录制流量返回。
- 方案:通过动态库加载技术,重写libc connect函数完成 。(修改连接地址到统一的Mock Server)
2、流量匹配
:Mock Server收到下游调用请求,会尽可能的匹配线上录制的流量,并给出相应返回值。
- 方案:将回放请求按照固定大小切分(默认16字节),分别和线上录制的请求匹配打分,取得分最高的流量。
- 打分:包含最小单元数据得1分,不包含最小单元数据得0分,噪音等因素会干扰。
2.3、JAVA创新方案
2.3.1、流量录制实现
1、方案对比:和PHP开源方案对比
- 共同点 :
流量拦截
方式一样,都可以在libc层获取流量。底层网络调用都通过libc来完成,通过libc层拦截,可以获取所有网络调用流量。 - 差异点 :
链路追踪
方式不一样,JAVA涉及多线程。
2、方案实现:分场景实现有差异
-
BIO场景 :常见http、mysql等下游调用,和接口请求在同一个线程,无需特别处理可以串联流量。(
方案:在libc层拦截和串联流量
) -
NIO场景 :常见redis、dubbo等下游调用,和接口请求不在一个线程,需要通过字节码增强技术拦截上报流量。(
原理:同skywalking
) -
录制示例 :以NIO场景的Redis letture组件为例,我们和skywalking基于同样的拦截点(如图2-3-1),同样是基于Command指令(如图2-3-2),只是获取的信息不同(Skywalking方案:获取指令,如:
GET foo
;我们方案:获取网络字节流,如:*2 $3 GET $3 foo
)。
2.3.2、流量回放实现
1、方案对比:和PHP开源方案对比
a、共同点:绝大部分能力可以复用
- PHP和JAVA语言流量回放都在统一平台进行,绝大部分能力可以复用。可复用核心能力:
连接重定向
、流量匹配
、时间回退、协议解析、噪音去除、流量染色等。
b、差异点:部分场景需要特殊处理。
2、方案实现:本地缓存场景示例
a、流量录制:扩展协议,不限网络流量录制
流量拦截
:通过字节码增强技术,拦截本地缓存方法调用,序列化后存储。(已支持GuavaCache 、CaffeineCache组件 )链路追踪
:本地缓存一般都是同步调用,不需要特别处理,根据接口所在主线程串联即可。
b、流量回放:构造请求,统一流量匹配返回
连接重定向
:没有网络调用,需要构造TCP请求,和其他请求一样通过Mock Server获取录制流量。流量匹配
: Mock Server按照统一的策略打分匹配返回即可,核心流程不变。
2.3.3、方案对比
和开源阿里方案对比:解决问题场景一样,比较知名的是:jvm-sandbox-repeater。
1、优势
a、插件维护成本更低。
- 我们方案:方案更偏底层,很多组件可以复用。如:同步调用的组件(如: mysql、http 等),统一都在socket包拦截。
- 阿里方案:方案偏应用层,不同组件无法复用。组件都需要单独实现,可能还需要区分版本,很多团队尝试后放弃根因。
b、回放流程可以通用。
- 我们方案:不同语言大部分能力都可以复用,能做到体验一致。
- 阿里方案:仅支持JAVA语言。
2、不足
a、部分场景不支持:只支持无状态协议回放,有状态协议不支持,如:https、grpc等。
- 方案:部分含不支持协议的接口可以设置过滤,常见协议都支持, 如:http、redis、mysql 、dubbo等
b、可读性相对较差:我们方案使用网络流量录制和回放,相对可读性差,不利于后续噪音去除和定位问题。
- 方案:需要解析协议,可以直接复用开源PHP/GO解决方案。如:mysql协议,只展示其中sql语句即可。
2.4、小结
- 整体思路:分语言录制,统一平台回放。先流量录制(线上为主,网络流量采集为主),再流量回放(线下为主,网络流量对比为主)。
- 流量录制:不同语言实现方式有差别,需单独实现(PHP利用语言特性在libc层就可以实现,JAVA有异步场景需要特殊处理)。
- 流量回放:不同语言实现方式大同小异,可以复用(核心能力都可以复用,包含:连接重定向、流量匹配、时间回退、协议解析、噪音去除等)。
三、结语
如果你对流量回放原理和应用感兴趣,欢迎点赞、收藏、评论(必回)、转发~