共计 19918 个字符,预计需要花费 50 分钟才能阅读完成。
文章导读
本文次要介绍了流量录制与回放技术在压测场景下的利用。通过浏览本篇文章,你将理解到开源的录制工具如何外部系统集成、如何进行二次开发以反对 Dubbo 流量录制、怎么通过 Java 类加载机制解决 jar 包版本抵触问题、以及流量录制在自动化测试场景下的利用与价值等。文章共约 1.4 万字,配图 17 张。本篇文章是对我集体过来一年所负责的工作的总结,外面波及到了很多技术点,集体从中学到了很多货色,也心愿这篇文章能让大家有所播种。当然集体能力无限,文中不妥之处也欢送大家指教。具体章节安顿如下:
1. 前言
本篇文章记录和总结了本人过来一年所主导的我的项目——流量录制与回放,该我的项目次要用于为业务团队提供压测服务。作为我的项目负责人,我承当了我的项目约 70% 的工作,所以这个我的项目承载了本人很多的记忆。从需要提出、技术调研、选型验证、问题处理、方案设计、两周内上线最小可运行零碎、推广应用、反对年中 / 终全链路压测、迭代优化、反对 dubbo 流量录制、到新场景落地产生价值。这里列举每一项本人都深度参加了,因而也从中学习到了很多货色。蕴含但不限于 go 语言、网络常识、Dubbo 协定细节,以及 Java 类加载机制等。除此之外,我的项目所产生的价值也让本人很欣慰。我的项目上线一年,帮忙业务线发现了十几个性能问题,帮忙中间件团队发现了根底组件多个重大的问题。总的来说,这个我的项目对于我集体来说具备不凡意义,受害良多。这里把过来一年的我的项目经验记录下来,做个总结。本篇文章着重讲实现思路,不会贴太多代码,有趣味的敌人能够依据思路本人定制一套。好了,上面开始注释吧。
2. 我的项目背景
我的项目的呈现源自业务团队的一个诉求——应用线上实在的流量进行压测,使压测更为“实在”一些。之所以业务团队感觉应用老的压测平台(基于 Jmeter 实现)不实在,是因为压测数据的多样性有余,对代码的覆盖度不够。惯例压测工作通常都是对利用的 TOP 30 接口进行压测,如果人工去欠缺这些接口的压测数据,老本是会十分高的。基于这个需要,咱们调研了一些工具,并最终抉择了 Go 语言编写的 GoReplay 作为流量录制和回放工具。至于为什么抉择这个工具,接下来聊聊。
3. 技术选型与验证
3.1 技术选型
一开始选型的时候,经验不足,并没有思考太多因素,只从功能性和知名度两个维度进行了调研。首先性能上肯定要能满足咱们的需要,比方具备流量过滤性能,这样能够按需录制指定接口。其次,候选项最好有大厂背书,github 上有很多 star。依据这两个要求,选出了如下几个工具:
图 1:技术选型
第一个是选型是阿里开源的工具,全称是 jvm-sandbox-repeater,这个工具其实是基于 JVM-Sandbox 实现的。原理上,工具通过字节码加强的模式,对指标接口进行拦挡,以获取接口参数和返回值,成果等价于 AOP 中的盘绕告诉 (Around advice)。
第二个选型是 GoReplay,基于 Go 语言实现。底层依赖 pcap 库提供流量录制能力。驰名的 tcpdump 也依赖了 pcap 库,所以能够把 GoReplay 看成极简版的 tcpdump,因为其反对的协定很繁多,只反对录制 http 流量。
第三个选型是 Nginx 的流量镜像模块 ngx_http_mirror_module,基于这个模块,能够将流量镜像到一台机器上,实现流量录制。
第四个选型是阿里云云效里的子产品——双引擎回归测试平台,从名字上能够看进去,这个零碎是为回归测试开发的。而咱们需要是做压测,所以这个服务里的很多性能咱们用不到。
通过比拟筛选后,咱们抉择了 GoReplay 作为流量录制工具。在剖析 GoReplay 优缺点之前,先来剖析下其余几个工具存在的问题。
- jvm-sandbox-repeater 这个插件底层基于 JVM-Sandbox 实现,应用时须要把两个我的项目的代码都加载到指标利用内,对利用运行时环境有侵入。如果两个我的项目代码存在问题,造成相似 OOM 这种问题,会对指标利用造成很大大的影响。另外因为方向小众,导致 JVM-Sandbox 利用并不是很宽泛,社区活跃度较低。因而咱们放心呈现问题官网无奈及时修复,所以这个选型待定。
- ngx_http_mirror_module 看起来是个不错的抉择,出世“名门”。但问题也有一些。首先只能反对 http 流量,而咱们当前肯定会反对 dubbo 流量录制。其次这个插件要把申请镜像一份进来,势必要耗费机器的 TCP 连接数、网络带宽等资源。思考到咱们的流量录制会继续运行在网关上,所以这些资源耗费肯定要思考。最初,这个模块没法做到对指定接口进行镜像,且镜像性能开关须要批改 nginx 配置实现。线上的配置是不可能,尤其是网关这种外围利用的配置是不能轻易改变的。综合这些因素,这个选型也被放弃了。
- 阿里云的引擎回归测试平台在咱们调研时,本身的性能也在打磨,用起来挺麻烦的。其次这个产品属于云效的子产品,不独自发售。另外这个产品次要还是用于回归测试的,与咱们的场景存在较大偏差,所以也放弃了。
接着来说一下 GoReplay 的优缺点,先说长处:
- 单体程序,除了 pcap 库,没有其余依赖,也无需配置,所以环境筹备很简略
- 自身是个可执行程序,可间接运行,很轻量。只有传入适合的参数就能录制,易使用
- github 上的 star 数较多,知名度较大,且社区沉闷
- 反对流量过滤性能、按倍速回放性能、回放时改写接口参数等性能,性能上贴合咱们的需要
- 资源耗费小,不侵入业务利用 JVM 运行时环境,对指标利用影响较小
对于以 Java 技术栈为根底的公司来说,GoReplay 因为是 Go 语言开发的,技术栈差别很大,日后的保护和拓展是个大问题。所以单凭这一点,淘汰掉这个选型也是很失常的。但因为其长处也绝对突出,综合其余选型的优缺点思考后,咱们最终还是抉择了 GoReplay 作为最终的选型。最初大家可能会纳闷,为啥不抉择 tcpdump。起因有两点,咱们的需要比拟少,用 tcpdump 有种大炮打蚊子的感觉。另一方面,tcpdump 给咱们的感觉是太简单了,驾驭不住(流下了没有技术的眼泪😭),因而咱们一开始就没怎么思考过这个选型。
选型 | 语言 | 是否开源 | 长处 | 毛病 |
---|---|---|---|---|
GoReplay | Go | ✅ | 1. 开源我的项目,代码简略,不便定制 2. 单体继续,依赖少,无需配置,环境筹备简略 3. 工具很轻量,易使用 3. 性能绝对丰盛,可能满足咱们所有的需要 4. 自带回放性能,可能间接应用录制数据,无需独自开发 5. 资源耗费少,且不侵入指标利用的 JVM 运行时环境,影响小 6. 提供了插件机制,且插件实现不限度语言,不便拓展 |
1. 利用不够宽泛,无大公司背书,成熟度不够 2. 问题比拟多,1.2.0 版本官网间接不举荐应用 3. 接上一条,对使用者的要求较高,出问题状况下要能本人读源码解决,官网响应速度个别 4. 社区版只反对 HTTP 协定,不反对二进制协定,且外围逻辑与 HTTP 协定耦合了,拓展较麻烦 5. 只反对命令行启动,没有内置服务,不好进行集成 |
JVM-Sandbox jvm-sandbox-repeater |
Java | ✅ | 1. 通过加强的形式,能够间接对 Java 类办法进行录制,非常弱小 2. 性能比拟丰盛,较为合乎需要 3. 对业务代码通明无侵入 |
1. 会对利用运行时环境有肯定侵入,如果产生问题,对利用可能会造成一些影响 2. 工具自身依然偏差测试回归,所以导致一些性能在咱们的场景下没法应用,比方不能应用它的回放性能进行高倍速压测 3. 社区活跃度较低,有进行保护的危险 4. 底层实现的确比较复杂,保护老本也比拟高。再次留下了没有技术的眼泪😢 5. 须要搭配其余的辅助零碎,整合老本不低 |
ngx_http_mirror_module | C | ✅ | 1. nginx 出品,成熟度能够保障 2. 配置比较简单 |
1. 不不便启停,也不反对过滤 2. 必须和 nginx 搭配只用,因而应用范畴也比拟受限 |
阿里云引擎回归测试平台 | – | ❌ | – | – |
3.2 选型验证
选型实现后,紧接着要进行性能、性能、资源耗费等方面的验证,测试选型是否符合要求。依据咱们的需要,做了如下的验证:
- 录制性能验证,验证流量录制的是否残缺,蕴含申请数量完整性和申请数据准确性。以及在流量较大状况下,资源耗费状况验证
- 流量过滤性能验证,验证是否过滤指定接口的流量,以及流量的完整性
- 回放性能验证,验证流量回放是否能如预期工作,回放的申请量是否合乎预期
- 倍速回放验证,验证倍速性能是否合乎预期,以及高倍速回放下资源耗费状况
以上几个验证过后在线下都通过了,成果很不错,大家也都挺称心的。可是倍速回放这个性能,在生产环境上进行验证时,回放压力死活上不去,只能压到约 600 的 QPS。之后不论再怎么增压,QPS 始终都在这个水位。咱们与业务线共事应用不同的录制数据在线上测试了多轮均不行,开始认为是机器资源呈现了瓶颈。可是咱们看了 CPU 和内存耗费都非常低,TCP 连接数和带宽也是很充裕的,因而资源是不存在瓶颈的。这里也凸显了一个问题,晚期咱们只对工具做了功能测试,没有做性能测试,导致这个问题没有尽早裸露进去。于是我本人在线下用 nginx 和 tomcat 搭建了一个测试服务,进行了一些性能测试,发现随随便便就能压到几千的 QPS。看到这个后果哭笑不得,脑裂了😭。起初发现是因为线下的服务的 RT 太短了,与线上差别很大导致的。于是让线程随机睡眠几十到上百毫秒,此时成果和线上很靠近。到这里基本上可能大抵确定问题范畴了,应该是 GoReplay 呈现了问题。然而 GoReplay 是 Go 语言写的,大家对 Go 语言都没教训。眼看着问题解决唾手可得,可就是无处下手,很窒息。起初大佬们拍板决定投入工夫深刻 GoReplay 源码,通过剖析源码寻找问题,自此我开始了 Go 语言的学习之路。原打算两周给个初步论断,没想到一周就找到了问题。原来是因为 GoReplay v1.1.0 版本的应用文档与代码实现呈现了很大的偏差,导致依照文档操作就是达不到预期成果。具体细节如下:
图 2:GoReplay 应用阐明
先来看看坑爹的文档是怎么说的,--output-http-workers
这个参数示意有多少个协程同时用于产生 http 申请,默认值是 0,也就是无限度。再来看看代码(output_http.go)是怎么实现的:
图 3:GoRepaly 协程并发数决策逻辑
文档里说默认 http 发送协程数无限度,后果代码里设置了 10,差别太大了。为什么 10 个协程不够用呢,因为协程须要原地期待响应后果,也就是会被阻塞住,所以 10 个协程可能打出的 QPS 是无限的。起因找到后,咱们明确设定 –output-http-workers 参数值,倍速回放的 QPS 最终验证下来可能达到要求。
这个问题产生后,咱们对 GoReplay 产生了很大的狐疑,感觉这个问题比拟低级。这样的问题都会呈现,那前面是否还会呈现有其余问题呢,所以用起来心里发毛。当然,因为这个我的项目保护的人很少,根本能够认定是集体我的项目。且该我的项目通过没有大规模的利用,尤其没有大公司的背书,呈现这样的问题也能了解,没必要太苛责。因而前面碰到问题只能见招拆招了,反正代码都有了,间接白盒审计吧。
3.3 总结与反思
先说说选型过程中存在的问题吧。从下面的形容上来看,我在选型和验证过程均犯了一些较为重大的谬误,被本人活泼的上了一课。在选型阶段,对于知名度,竟然认为 star 比拟多就算比拟有名了,当初想想还是太童稚了。比起知名度,成熟度其实更重要,稳固坑少上班早🤣。另外,可观测性也肯定要思考,否则查问题时你将体验到什么是无助感。
在验证阶段,性能验证没有太大问题。但性能验证只是象征性的搞了一下,最终在与业务线共事一起验证时翻车了。所以验证期间,性能测试是不能马虎的,一旦相干问题上线后才发现,那就很被动了。
依据这次的技术选型经验做个总结,当前搞技术选型时再翻出来看看。选型维度总结如下:
维度 | 阐明 |
---|---|
功能性 | 1. 选型的性能是否可能满足需要,如果不满足,二次开发的老本是怎么的 |
成熟度 | 1. 在相干畛域内,选型是否通过大范畴应用。比方 Java Web 畛域,Spring 技术栈根本人尽皆知 2. 一些小众畛域的选型可能利用并不是很宽泛,那只能本人多去看看 issue,搜寻一些踩坑记录,自行评估了 |
可观测性 | 1. 外部状态数据是否有观测伎俩,比方 GoReplay 会把外部状态数据定时打印进去 2. 方不不便接入公司的监控零碎也要思考,毕竟人肉察看太吃力 |
验证总结如下:
- 依据要求一项一项的去验证选型的性能是否合乎预期,能够搞个验证的 checklist 进去,逐项确认
- 从多个可能的方面对选型进行性能测试,在此过程中留神察看各种资源耗费状况。比方 GoReplay 流量录制、过滤和回放性能都是必须要做性能测试的
- 对选型的长时间运行的稳定性要进行验证,对验证期间存在的异常情况留神观测和剖析
- 更严格一点,能够做一些故障测试。比方杀过程,断网等
对于选型更具体的实战经验,能够参考李运华大佬的文章:如何正确的应用开源我的项目。
4. 具体实际
当技术选型和验证都实现后,接下来就是要把想法变为事实的时候了。依照当初小步快跑,疾速迭代的模式,启动阶段通常咱们仅会布局最外围的性能,保障流程走通。接下来再依据需要的优先级进行迭代,逐步完善。接下来,我将在依照我的项目的迭代过程来进行介绍。
4.1 最小可用零碎
4.1.1 需要介绍
序号 | 分类 | 需要点 | 阐明 |
---|---|---|---|
1 | 录制 | 流量过滤,按需录制 | 反对按 HTTP 申请门路过滤流量,这样能够录制指定接口的流量 |
2 | 录制时长可指定 | 可设定录制时长,个别状况下都是录制 10 分钟,把流量波峰录制下来 | |
3 | 录制工作详情 | 蕴含录制状态、录制后果统计等信息 | |
4 | 回放 | 回放时长可指定 | 反对设定 1 ~ 10 分钟的回放时长 |
5 | 回放倍速可指定 | 依据录制时的 QPS,按倍数进行流量放大,最小粒度为 1 倍速 | |
6 | 回放过程容许人为终止 | 在发现被压测利用呈现问题时,可人为终止回放过程 | |
7 | 回放工作详情 | 蕴含回放状态、回放后果统计 |
以上就是我的项目启动阶段的需要列表,这些都是最根本需要。只有实现这些需要,一个最小可用的零碎就实现了。
4.1.2 技术计划简介
4.1.2.1 架构图
图 4:压测系统一期架构图
下面的架构图通过编辑,与理论有肯定差别,但不影响解说。须要阐明的是,咱们的网关服务、压测机以及压测服务都是别离由多台形成,所有网关和压测实例均部署了 GoRepaly 及其控制器。这里为了简化架构图,只画了一台机器。上面对一些外围流程进行介绍。
4.1.2.2 Gor 控制器
在介绍其余内容之前,先说一下 Gor 控制器的用处。用一句话介绍:引入这个中间层的目标是为了将 GoReplay 这个命令行工具与咱们的压测系统进行整合。这个模块是咱们本人开发,最早应用 shell 编写的(苦不堪言😭),起初用 Go 语言重写了。Gor 控制器次要负责上面一些事件:
- 把握 GoRepaly 生杀大权,能够调起和终止 GoReplay 程序
- 屏蔽掉 GoReplay 应用细节,升高复杂度,进步易用性
- 回传状态,在 GoReplay 启动前、完结后、其余标志性事件完结后都会向压测系统回传状态
- 对录制和回放产生数据进行解决与回传
- 打日志,记录 GoRepaly 输入的状态数据,便于后续排查
GoReplay 自身只提供最根本的性能,能够把其设想成一个只有底盘、轮子、方向盘和发动机等根本配件的汽车,尽管能开起来,然而比拟吃力。而咱们的 Gor 控制器相当于在其根底上提供了一键启停,转向助力、车联网等加强性能,让其变得更好用。当然这里只是一个近似的比喻,不要纠结合理性哈。通晓控制器的用处后,上面介绍启动和回放的执行过程。
4.1.2.3 录制过程介绍
用户的录制命令首先会发送给压测服务,压测服务本来能够通过 SSH 间接将录制命令发送给 Gor 控制器的,但出于平安思考必须绕道运维零碎。Gor 控制器收到录制命令后,参数验证无误,就会调起 GoReplay。录制完结后,Gor 控制器会将状态回传给压测系统,由压测断定录制工作是否完结。具体的流程如下:
- 用户设定录制参数,提交录制申请给压测服务
- 压测服务生成压测工作,并依据用户指定的参数生成录制命令
- 录制命令经由运维零碎下发到具体的机器上
- Gor 控制器收到录制命令,回传“录制行将开始”的状态给压测服务,随后调起 GoReplay
- 录制完结,GoReplay 退出,Gor 控制器回传“录制完结”状态给压测服务
- Gor 控制器回传其余信息给压测系统
- 压测服务断定录制工作完结后,告诉压测机将录制数据读取到本地文件中
- 录制工作完结
这里阐明一下,要想应用 GoReplay 倍速回放性能,必须要将录制数据存储到文件中。而后通过上面的参数设置倍速:
# 三倍速回放
gor --input-file "requests.gor|300%" --output-http "test.com"
4.1.2.4 回放过程介绍
回放过程与录制过程根本类似,只不过回放的命令是固定发送给压测机的,具体过程就不赘述了。上面说几个不同点:
- 给回放流量打上压测标:回放流量要与实在流量辨别开,须要一个标记,也就是压测标
- 按需改写参数:比方把 user-agent 改为 goreplay,或者减少测试账号的 token 信息
- GoReplay 运行时状态收集:蕴含 QPS,工作队列积压状况等,这些信息能够帮忙理解 GoReplay 的运行状态
4.1.3 不足之处
这个最小可用零碎在线上差不多运行了 4 个月,没有呈现过太大的问题,但依然有一些不足之处。次要有两点:
- 命令传递的链路略长,增大的出错的概率和排查的难度。比方运维零碎的接口偶然失败,要害还没有日志,一开始基本没法查问题
- Gor 控制器是用 shell 写的,约 300 行。shell 语法和 Java 差别比拟大,代码也不好调试。同时对于简单的逻辑,比方生成 JSON 字符串,写起来很麻烦,后续保护老本较高
这两点有余始终随同着咱们的开发和运维工作,直到前面进行了一些优化,才算是彻底解决掉了这些问题。
4.2 继续优化
图 5:Gor 控制器优化后的架构图
针对后面存在的痛点,咱们进行了针对性的改良。重点应用 Go 语言重写了 gor 控制器,新的控制器名称为 gor-server。从名称上能够看出,咱们内置了一个 HTTP 服务。基于这个服务,压测服务下发命令终于不必再绕道运维零碎了。同时所有的模块都在咱们的掌控中,开发和保护的效率显著变高了。
4.3 反对 Dubbo 流量录制
咱们外部采纳 Dubbo 作为 RPC 框架,利用之间的调用均是通过 Dubbo 来实现的,因而咱们对 Dubbo 流量录制也有较大的需要。在针对网关流量录制获得肯定成绩后,一些负责外部零碎的共事也心愿通过 GoReplay 来进行压测。为了满足外部的应用需要,咱们对 GoReplay 进行了二次开发,以便反对 Dubbo 流量的录制与回放。
4.3.1 Dubbo 协定介绍
要对 Dubbo 录制进行反对,需首先搞懂 Dubbo 协定内容。Dubbo 是一个二进制协定,它的编码规定如下图所示:
图 6:Dubbo 协定图示;起源:Dubbo 官方网站
上面简略对协定做个介绍,依照图示程序顺次介绍各字段的含意。
字段 | 位数(bit) | 含意 | 阐明 |
---|---|---|---|
Magic High | 8 | 魔数高位 | 固定为 0xda |
Magic Low | 8 | 魔数低位 | 固定为 0xbb |
Req/Res | 1 | 数据包类型 | 0 – Response 1 – Request |
2way | 1 | 调用形式 | 0 – 单向调用 1 – 双向调用 |
Event | 1 | 事件标识 | 比方心跳事件 |
Serialization ID | 5 | 序列化器编号 | 2 – Hessian2Serialization<br/>3 – JavaSerialization<br/>4 – CompactedJavaSerialization<br/>6 – FastJsonSerialization …… |
Status | 8 | 响应状态 | 状态列表如下: 20 – OK 30 – CLIENT_TIMEOUT 31 – SERVER_TIMEOUT 40 – BAD_REQUEST 50 – BAD_RESPONSE …… |
Request ID | 64 | 申请 ID | 响应头中也会携带雷同的 ID,用于将申请和响应关联起来 |
Data Length | 32 | 数据长度 | 用于标识 Variable Part 局部的长度 |
Variable Part(payload) | – | 数据载荷 |
通晓了协定内容后,咱们把官网的 demo 跑起来,抓个包钻研一下。
图 7:dubbo 申请抓包
首先咱们能够看到占用两个字节的魔数 0xdabb,接下来的 14 个字节是协定头中的其余内容,简略剖析一下:
图 8:dubbo 申请头数据分析
下面标注的比较清楚了,这里略微解释一下。从第三个字节能够看出这个数据包是一个 Dubbo 申请,因为是第一个申请,所以申请 ID 是 0。数据的长度是 0xdc,换算成十进制为 220 个字节。加上 16 个字节的音讯头,总长度正好是 236,与抓包结果显示的长度是统一。
4.3.2 Dubbo 协定解析
咱们对 Dubbo 流量录制进行反对,首先须要依照 Dubbo 协定对数据包进行解码,以判断录制到的数据是不是 Dubbo 申请。那么问题来了,如何判断所录制到的 TCP 报文段里的数据是 Dubbo 申请呢?答案如下:
- 首先判断数据长度是不是大于等于协定头的长度,即 16 个字节
- 判断数据前两个字节是否为魔数 0xdabb
- 判断第 17 个比特位是不是 1,不为 1 可抛弃掉
通过下面的检测可疾速判断出数据是否合乎 Dubbo 申请格局。如果检测通过,那接下来又如何判断录制到的申请数据是否残缺呢?答案是通过比拟录制到的数据长度 L1 和 Data Length 字段给出的长度 L2,依据比拟后果进行后续操作。有如下几种状况:
- L1 == L2,阐明数据接管残缺,无需额定的解决逻辑
- L1 < L2,阐明还有一部分数据没有接管,持续期待余下数据
- L1 > L2,阐明多收到了一些数据,这些数据并不属于以后申请,此时要依据 L2 来切分收到的数据
三种状况示意图如下:
图 9:应用层接收端几种状况
看到这里,必定有同学想说,这不就是典型的 TCP“粘包”和“拆包”问题。不过我并不想用这两个词来阐明上述的一些状况。TCP 是一个面向字节流的协定,协定自身并不存在所谓的“粘包”和“拆包”问题。TCP 在传输数据过程中,并不会理睬下层数据是如何定义的,在它看来都是一个个的字节罢了,它只负责把这些字节牢靠有序的运送到指标过程。至于状况 2 和状况 3,那是应用层应该去解决的事件。因而,咱们能够在 Dubbo 的代码中找到相干的解决逻辑,有趣味的同学能够浏览 NettyCodecAdapter.InternalDecoder#decode 办法代码。
本大节内容就到这里,最初给大家留下一个问题。在 GoReplay 的代码中,并没有对状况 3 进行解决。为什么录制 HTTP 协定流量不会出错?
4.3.3 GoReplay 革新
4.3.3.1 革新介绍
GoReplay 社区版目前只反对 HTTP 流量录制,其商业版反对局部二进制协定,但不反对 Dubbo。所以为了满足外部应用需要,只能进行二次开发了。但因为社区版代码与 HTTP 协定解决逻辑耦合比拟大,因而想要反对一种新的协定录制,还是比拟麻烦的。在咱们的实现中,对 GoReplay 的革新次要蕴含 Dubbo 协定辨认,Dubbo 流量过滤,数据包完整性判断等。数据包的解码和反序列化则是交给 Java 程序来实现的,序列化后果转成 JSON 进行存储。成果如下:
图 10:Dubbo 流量录制成果
GoReplay 用三个猴头 🐵🙈🙉 作为申请分隔符,第一眼看到感觉挺搞笑的。
4.3.3.2 GoReplay 插件机制介绍
大家可能很好奇 GoReplay 是怎么和 Java 程序配合工作的,原理倒也是很简略。先看一下怎么开启 GoReplay 的插件模式:
gor --input-raw :80 --middleware "java -jar xxx.jar" --output-file request.gor
通过 middleware 参数能够传递一条命令给 GoRepaly,GoReplay 会拉起一个过程执行这个命令。在录制过程中,GoReplay 通过获取过程的规范输出和输入与插件过程进行通信。数据流向大抵如下:
+-------------+ Original request +--------------+ Modified request +-------------+
| Gor input |----------STDIN---------->| Middleware |----------STDOUT---------->| Gor output |
+-------------+ +--------------+ +-------------+
input-raw java -jar xxx.jar output-file
4.3.3.3 Dubbo 解码插件实现思路
Dubbo 协定的解码还是比拟容易实现的,毕竟很多代码 Dubbo 框架曾经写好了,咱们只须要按需对代码进行批改定制即可。协定头的解析逻辑在 DubboCodec#decodeBody 办法中,音讯体的解析逻辑在 DecodeableRpcInvocation#decode(Channel, InputStream) 办法中。因为 GoReplay 曾经对数数据进行过解析和解决,因而在插件里很多字段就没必要解析了,只有解析出 Serialization ID 即可。这个字段将领导咱们进行后续的反序列化操作。
对于音讯体的解码略微麻烦点,咱们把 DecodeableRpcInvocation 这个类代码拷贝一份放在插件我的项目中,并进行了批改。删除了不须要的逻辑,只保留了 decode 办法,将其变成了工具类。思考到咱们的插件不不便引入要录制利用的 jar 包,所以在批改 decode 办法时,还要留神把和类型相干的逻辑移除掉。批改后的代码大抵如下:
public class RpcInvocationCodec {public static MyRpcInvocation decode(byte[] bytes, int serializationId) {ObjectInput in = CodecSupport.getSerializationById(serializationId).deserialize(null, input);
MyRpcInvocation rpcInvocation = new MyRpcInvocation();
String dubboVersion = in.readUTF();
// ......
rpcInvocation.setMethodName(in.readUTF());
// 原代码:Class<?>[] pts = DubboCodec.EMPTY_CLASS_ARRAY;
// 批改后把 pts 类型改成 String[],泛化调用时须要用到类型列表
String[] pts = desc2className(int.readUTF());
Object[] args = new Object[pts.length];
for (int i = 0; i < args.length; i++) {// 原代码:args[i] = in.readObject(pts[i]);
// 批改后不在依赖具体类型,间接反序列化成 Map
args[i] = in.readObject();}
rpcInvocation.setArguments(args);
rpcInvocation.setParameterTypeNames(pts);
return rpcInvocation;
}
}
仅从代码开发的角度来说,难度并不是很大,当然前提是要对 Dubbo 的源码有肯定的理解。对我来说,工夫次要花在 GoRepaly 的革新上,次要起因是对 Go 语言不熟,边写边查导致效率很低。当性能写好,调试结束,看到后果正确输入,的确很开心。然而,这种开心也仅维持了很短的工夫。不久在与业务共事进行线上验证的时候,插件花色解体,局面一度非常难堪。报错信息看的我一脸懵逼,一时半会解决不了,为了保留点脸面,连忙终止了验证🤪。预先排查发现,在将一些的非凡的反序列化数据转化成 JSON 格局时,呈现了死循环,造成 StackOverflowError 谬误产生。因为插件主流程是单线程的,且仅捕捉了 Exception,所以造成了插件谬误退出。
图 11:循环依赖导致 Gson 框架报错
这个谬误通知咱们,类之间呈现了循环援用,咱们的插件代码也的确没有对循环援用进行解决,这个谬误产生是正当的。但当找到造成这个谬误的业务代码时,并没找到循环援用,直到我本地调试时才发现了猫腻。业务代码相似的代码如下:
public class Outer {
private Inner inner;
public class Inner {
private Long xyz;
public class Inner() {}
}
}
问题出在了外部类上,Inner 会隐式持有 Outer 援用。不出意外,这应该是编译器干的。源码背后了无机密,咱们把外部类的 class 文件反编译一下,所有就明了了。
图 12:外部类反编译后果
这应该算是 Java 基本知识了,奈何平时用的少,第一眼看到代码时,没看出了暗藏在其中的循环援用。到这里解释很正当,这就完结了么?其实还没有,实际上 Gson 序列化 Outer 时并不会报错,调试发现其会排除掉 this$0
这个字段,排除逻辑如下:
public final class Excluder
public boolean excludeField(Field field, boolean serialize) {
// ......
// 判断字段是否是合成的
if (field.isSynthetic()) {return true;}
}
}
那么咱们在把录制的流量转成 JSON 时为什么会报错呢?起因是咱们的插件反序列化时拿不到接口参数的类型信息,所以咱们把参数反序列化成了 Map
对象,这样 this$0
这个字段和值也会作为键值对存储到 Map 中。此时 Gson 的过滤规定就不失效了,没法过滤掉 this$0
这个字段,造成了死循环,最终导致栈溢出。晓得起因后,这么问题怎么解决呢?下一大节开展。
4.3.3.4 直击问题
我开始思考是不是能够人为荡涤一下 Map 里的数据,但发现如同很难搞。如果 Map 的数据结构很简单,比方嵌套了很多层,荡涤逻辑可能不好实现。还有我也不分明这外面会不会有其余的一些弯弯绕,所以放弃了这个思路,这种脏活累活还是丢给反序列化工具去做吧。咱们要想方法把拿到接口的参数类型,插件怎么拿到业务利用 api 的参数类型呢?一种形式是在插件启动时,把指标利用的 jar 包下载到本地,而后由独自的类加载器进行加载。但这里会有一个问题,业务利用的 api jar 包外面也存在着一些依赖,这些依赖难道要递归去下载?第二种形式,则简略粗犷点,间接在插件我的项目中引入业务利用 api 依赖,而后打成 fat jar。这样既不须要搞独自的类加载器,也不必去递归下载其余的依赖。惟一比拟显著的毛病就是会在插件我的项目 pom 中引入一些不相干的依赖,但与收益相比,这个毛病基本算不上什么。为了不便,咱们把很多业务利用的 api 都依赖了进来。一番操作后,咱们失去了如下的 pom 配置:
<project>
<groupId>com.xxx.middleware</groupId>
<artifactId>DubboParser</artifactId>
<version>1.0</version>
<dependencies>
<dependency>
<groupId>com.xxx</groupId>
<artifactId>app-api-1</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>com.xxx</groupId>
<artifactId>app-api-2</artifactId>
<version>1.0</version>
</dependency>
......
<dependencies>
</project>
接着要改一下 RpcInvocationCodec#decode 办法,其实就是把代码还原回去😓:
public class RpcInvocationCodec {public static MyRpcInvocation decode(byte[] bytes, int serializationId) {ObjectInput in = CodecSupport.getSerializationById(serializationId).deserialize(null, input);
MyRpcInvocation rpcInvocation = new MyRpcInvocation();
String dubboVersion = in.readUTF();
// ......
rpcInvocation.setMethodName(in.readUTF());
// 解析接口参数类型
Class<?>[] pts = ReflectUtils.desc2classArray(desc);
Object args = new Object[pts.length];
for (int i = 0; i < args.length; i++) {
// 依据具体类型进行反序列化
args[i] = in.readObject(pts[i]);
}
rpcInvocation.setArguments(args);
rpcInvocation.setParameterTypeNames(pts);
return rpcInvocation;
}
}
代码调整结束,择日在上线验证,一切正常,可喜可贺。但不久后,我发现这外面存在着一些隐患。如果哪天在线上产生了,将会给排查工作带来比拟大的艰难。
4.3.3.5 潜在的问题
思考这样的状况,业务利用 A 和利用 B 的 api jar 包同时依赖了一些外部的公共包,公共包的版本可能不统一。这时候,咱们怎么解决依赖抵触?如果外部的公共包做的不好,存在兼容性问题怎么办。
图 13:依赖抵触示意图
比方这里的 common 包版本抵触了,而且 3.0 不兼容 1.0,怎么解决呢?
简略点解决,咱们就不在插件 pom 里依赖所有的业务利用的 api 包了,而是只依赖一个。然而害处是,每次都要为不同的利用独自构建插件代码,显然咱们不喜爱这样的做法。
再进一步,咱们不在插件中依赖业务利用的 api 包,放弃插件代码洁净,就不必每次都打包了。那怎么获取业务利用的 api jar 包呢?答案是为每个 api jar 专门建个我的项目,再把我的项目打成 fat jar,插件代码应用自定义类加载器去加载业务类。插件启动时,依据配置去把 jar 包下载到机器上即可。每次只须要加载一个 jar 包,所以也就不存在依赖抵触问题了。做到这一步,问题就能够解决了。
更进一步,新近在浏览阿里开源的 jvm-sandbox 我的项目源码时,发现了这个我的项目实现了一种带有路由性能的类加载器。那咱们的插件是否也搞个相似的加载器呢?出于好奇,尝试了一下,发现是能够的。最终的实现如下:
图 14:自定义类加载机制示意图
一级类加载器具备依据包名“片段”进行路由的性能,二级类加载器负责具体的加载工作。利用 api jar 包对立放在一个文件夹下,只有二级类加载器能够进行加载。对于 JDK 中的一些类,比方 List,还是要交给 JVM 内置的类加载器进行加载。最初阐明一下,搞这个领路由性能的类加载器,次要目标是为了玩。尽管能达到目标,但在理论我的项目中,还是用上一种办法稳当点。
4.4 开花结果,落地新场景
咱们的流量录制与回放零碎次要的,也是过后惟一的应用场景是做压测。零碎稳固后,咱们也在思考还有没有其余的场景能够搞。正好在技术选型阶段试用过 jvm-sandbox-repeater,这个工具次要利用场景是做流量比照测试。对于代码重构这种不影响接口返回值构造的改变,能够通过流量比照测试来验证改变是否有问题。因为大佬们感觉 jvm-sandbox-repeater 和底层的 jvm-sandbox 有点重,技术复杂度也比拟高。加之没有资源来开发和保护这两个工具,因而心愿咱们基于流量录制和回放零碎来做这个事件,先把流程跑通。
我的项目由 QA 团队主导,流量重放与 diff 性能由他们开发,咱们则提供底层的录制能力。零碎的工作示意图如下:
图 15:比照测试示意图
咱们的录制零碎为重放器提供实时的流量数据,重放器拿到数据后立刻向预发和线上环境重放。重放后,重放器能够别离拿到两个环境返回的后果,而后再把后果传给比对模块进行后续的比对。最初把比对后果存入到数据库中,比对过程中,用户能够看到哪些申请比对失败了。对于录制模块来说,要留神过滤重放流量。否则会造成接口 QPS 倍增,重放变压测了🤣,喜提故障一枚。
这个我的项目上线 3 个月,帮忙业务线发现了 3 个比较严重的 bug,6 个个别的问题,价值初现。尽管我的项目不是咱们主导的,然而作为底层服务的提供方,咱们也很开心。冀望将来能为咱们的零碎拓展更多的应用场景,让其成长为一棵枝繁叶茂的大树。
5. 我的项目成绩
截止到文章公布工夫,我的项目上线靠近一年的工夫了。总共有 5 个利用接入应用,录制和回放次数累计差不多四五百次。应用数据上看起来有点寒碜,次要是因为公司业务是 toB 的,对压测的需要并没那么多。只管应用数据比拟低,然而作为压测系统,还是施展了相应价值。次要蕴含两方面:
- 性能问题发现:压测平台共为业务线发现了十几个性能问题,帮忙中间件团队发现了 6 个重大的根底组件问题
- 应用效率晋升:新的压测系统性能简略易用,仅需 10 分钟就能实现一次线上流量录制。相较于以往单人半天能力实现的事件,效率至多晋升了 20 倍,用户体验大幅晋升。一个佐证就是目前 90% 以上的压测工作都是在新平台上实现的。
可能大家对效率晋升数据有所狐疑,大家能够思考一下没有录制工具如何获取线上流量。传统的做法是业务开发批改接口代码,加一些日志,这要留神日志量问题。之后,把改变的代码公布到线上,对于一些比拟大的利用,一次公布波及到几十台机器,还是相当耗时的。接着,把接口参数数据从日志文件中荡涤进去。最初,还要把这些数据转换成压测脚本。这就是传统的流程,每个步骤都比拟耗时。当然,基建好的公司,能够基于全链路追踪平台拿到接口数据。但对于大多数公司来说,可能还是要应用传统的形式。而在咱们的平台上,只须要抉择指标利用和接口、录制时长、点击录制按钮就行了,用户操作仅限这些,所以效率晋升还是很显著的。
6. 展望未来
我的项目我的项目尽管曾经上线一年,但因为人手无限,目前根本只有我一个人在开发保护,所以迭代还是比较慢的。针对目前在实践中碰到的一些问题,这里把几个显著的问题,心愿将来可能一一解决掉。
1. 全链路节点压力图
目前在压测的时候,压测人员须要到监控平台上关上很多个利用的监控页面,压测期间须要在多个利用监控之间进行切换。心愿将来能够把全链路上各节点的压力图展现进去,同时能够把节点的报警信息发送给压测人员,升高压测的监督老本。
2. 压测工具状态收集与可视化
压测工具本身有一些很有用的状态信息,比方工作队列积压状况,以后的协程数等。这些信息在压测压力上不去时,能够帮忙咱们排查问题。比方工作队列工作数在增大,协程数也放弃高位。这时候能推断出什么起因吗?大概率是被压利用压力太大,导致 RT 变长,进而造成施压协程(数量固定)长时间被阻塞住,最终导致队列呈现积压状况。GoReplay 目前这些状态信息输入到管制台上的,查看起来还是很不不便。同时也没有告警性能,只能在出问题时被动去查看。所以冀望将来能把这些状态数据放到监控平台上,这样体验会好很多。
3. 压力感知与主动调节
目前压测系统更没有对业务利用的压力进行感知,不论压测利用处于什么状态,压测系统都会依照既定的设置进行压测。当然因为 GoReplay 并发模型的限度,这个问题目前不必放心。但将来不排除 GoReplay 的并发模型会发生变化,比方只有工作队列里有工作,就立刻起个协程发送申请,此时就会对业务利用造成很大的危险。
还有一些问题,因为重要水平不高,这里就不写了。总的来说,目前咱们的压测需要还是比拟少,压测的 QPS 也不高,导致很多优化都没法做。比方压测机性能调优,压测机器动静扩缩容。但想想咱们就 4 台压测机,默认配置齐全能够满足需要,所以这些问题都懒得去折腾🤪。当然从集体技术能力晋升的角度来说,这些优化还是很有价值的,有工夫能够玩玩。
7. 集体播种
7.1 技术播种
1. 入门 Go 语言
因为 GoReplay 是 Go 语言开发的,而且咱们在应用中的确也遇到了一些问题,不得不深刻源码排查。为了更好的掌控工具,不便排查问题和二次开发,所以专门学习了 Go 语言。目前的程度处于入门阶段,菜鸟程度。用 Java 用久了,刚开始学习 Go 语言还是很懵逼的。比方 Go 的 办法 定义:
type Rectangle struct {
Length uint32
Width uint32
}
// 计算面积
func (r *Rectangle) Area() uint32 {return r.Length * r.Width}
过后感觉这个语法十分的奇怪,Area 办法名后面的申明是什么鬼。好在我还有点 C 语言的常识,转念一想,如果让 C 去实现面向对象又该如何做呢?
struct Rectangle {
uint32_t length;
uint32_t width;
// 成员函数申明
uint32_t (*Area) (struct Rectangle *rect);
};
uint32_t Area(struct Rectangle *rect) {return rect->length * rect->width;}
struct Rectangle *newRect(uint32_t length, uint32_t width)
{struct Rectangle *rp = (struct Rectangle *) malloc(sizeof(struct Rectangle));
rp->length = length;
rp->width = width;
// 绑定函数
rp->Area = Area;
return rp;
}
int main()
{struct Rectangle *rp = newRect(5, 8);
uint32_t area = rp->Area(rectptr);
printf("area: %u\n", area);
free(pr);
return 0;
}
搞懂了下面的代码,就晓得 Go 的办法为什么要那么定义了。
随着学习的深刻,发现 Go 的语法个性和 C 还真的很像,竟然也有指针的概念,21 世纪的 C 语言果然名不副实。于是在学习过程中,会情不自禁的比照两者的个性,依照 C 的教训去学习 Go。所以当我看到上面的代码时,十分的惊恐。
func NewRectangle(length, width uint32) *Rectangle {var rect Rectangle = Rectangle{length, width}
return &rect
}
func main() {fmt.Println(NewRectangle(4, 5).Area())
}
过后预期操作系统会有情的抛个 segmentation fault 谬误给我,然而编译运行竟然没有问题 … 问.. 题..。难道是我错了?再看一遍,心想没问题啊,C 语言里不能返回栈空间的指针,Go 语言也不应该这么操作吧。这里就体现出两个语言的区别了,下面的 Rectangle 看起来像是在栈空间里调配到,实际上是在堆空间里调配的,这个和 Java 倒是一样的。
总的来说,Go 语法和 C 比拟像,加之 C 语言是我的启蒙编程语言。多以对于 Go 语言,也是感觉十分亲切和喜爱的。其语法简略,规范库丰盛易用,应用体验不错。当然,因为我目前还在新手村混,没有用 Go 写过较大的工程,所以对这个语言的意识还比拟肤浅。以上有什么不对的中央,也请大家见谅。
2. 较为熟练掌握了 GoReplay 原理
GoReplay 录制和回放外围的逻辑根本都看了一遍,并且在内网也写过文章分享,这里简略和大家聊聊这个工具。GoReplay 在设计上,形象出了一些概念,比方用 输出 和输入 来示意数据起源与去向,用介于输出和输出模块之间的 中间件 实现拓展机制。同时,输出和输入能够很灵便的组合应用,甚至能够组成一个集群。
图 16:GoReplay 集群示意图
录制阶段,每个 tcp 报文段被形象为 packet。当数据量较大,须要分拆成多个报文段发送时,收端须要把这些报文段按程序序组合起来,同时还要解决乱序、反复报文等问题,保障向下一个模块传递的是一个残缺无误的 HTTP 数据。这些逻辑统封装在了 tcp_message 中,tcp_message 与 packet 是一对多的关系。前面的逻辑会将 tcp_message 中的数据取出,打上标记,传递给中间件(可选)或者是输出模块。
回放阶段流程绝对简略,但依然会依照 输出 → [中间件] → 输入 流程执行。通常输出模块是 input-file,输出模块是 output-http。回放阶段一个有意思的点是倍速回放的原理,通过按倍数缩短申请间的距离实现减速性能,实现代码也很简略。
总的来说,这个工具的外围代码并不多,然而性能还是比拟丰盛的,能够体验一下。
3. 对 Dubbo 框架和类加载机制有了更多的认知
在实现 Dubbo 流量录制时,基本上把解码相干的逻辑看了一遍。当然这块逻辑以前也看过,还写过文章。只不过这次要去定制代码,还是会比单纯的看源码写文章理解的更深刻一些,毕竟要去解决一些理论的问题。在此过程中,因为须要自定义类加载器,所以对类加载机制也有了更多的意识,尤其是那个领路由性能的类加载器,还是挺好玩的。当然,学会这些技术也没什么大不了的,重点还是可能发现问题,解决问题。
4. 其余播种
其余的播种都是一些比拟小的点,这里就不多说了,以问题的模式留给大家思考吧。
- TCP 协定会保障向下层 有序 交付数据,为何工作在应用层的 GoReplay 还要解决乱序数据?
- HTTP 1.1 协定通信过程是怎么的?如果在一个 TCP 连贯上间断发送两个 HTTP 申请会造成什么问题?
7.2 教训和感想
1. 技术选型要谨慎
开始搞选型没什么教训,考查维度很少,不够全面。这就导致了几个问题,首先在验证阶段工具始终达不到预期,耽搁了不少工夫。其次在后续的迭代期间,发现 GoReplay 的小问题比拟多,感觉谨严水平不够。比方 1.1.0 版本应用文档和代码有很多处差别,应用时要小心。再比方应用过程中,发现 1.3.0-RC1 版本中存在资源泄露问题 #926,棘手帮忙修复了一下 #927。当然 RC 版本有问题也很失常,然而这么显著的问题说实话不应该出。不过思考到这个我的项目是集体保护的,也不能要求太多。然而对于使用者来说,还是要当心。这种要在生产上运行的程序,不靠谱是很闹心的事件。所以对于我集体而言,当前选型成熟度肯定会排在第一位。对于集体保护的我的项目,尽量不作为靠前的候选项。
2. 技术验证要全面
初期的选型没有进行性能测试和极限测试,这就导致问题在线上验证时才发现。这么显著的问题,拖到这么晚才发现,搞的挺难堪的。所以对于技术验证,要从不同的角度进行性能测试,极限测试。更严格一点,能够向李运华大佬在 如何正确的应用开源我的项目 文章中提的那样,搞搞故障测试,比方杀过程,断电等。把前期工作做足,防止前期被动。
3. 磨刀不误砍柴工
这个我的项目波及到不同的技术,公司现有的开发平台无奈反对这种我的项目,所以打包和公布是个麻烦事。在开发和测试阶段会频繁的批改代码,如果手动进行打包,而后上传的 FTP 服务器上(无奈间接拜访线上机器),最初再部署到具体的录制机器上,这是一件非常机械低效的事件。于是我写了一个自动化构建脚本,来晋升构建和部署效率,实践证明成果挺好。从此心态稳固多了😀,很少进入火暴模式了。
图 17:自动化构建脚本效果图
非常难堪的是,我在我的项目上线后才把脚本写好,后期没有享受到自动化的福利。不过好在后续的迭代中,自动化脚本还是帮了很大的忙。尽早实现编译和打包自动化工具,有助于进步工作效率。只管咱们会感觉写工具也要花不少工夫,但如果能够预料到很多事件会反复很屡次,那么这些工具带来的收益将会远超付出。
8. 写在最初
十分侥幸可能参加并主导这个我的项目,总的来说,我集体还是从中学到了很多货色。这算是我职业生涯中第一个深度参加和继续迭代的我的项目,看着它的性能逐步欠缺起来,稳固不间断给大家提供服务,施展出其价值。作为我的项目负责人,我还是十分开心自豪的。但同时也有些遗憾的,因为公司的业务是 toB 的,对压测系统的要求并不高。零碎目前算是进入了稳定期,没有太多可做的需要或者大的问题。我尽管能够私下做一些技术上的优化,但很难看出成果,毕竟现有的应用需要还没达到零碎瓶颈,提前优化并不是一个好主见。冀望将来公司的业务能有大的倒退,对压测系统提出更高的要求,我也非常乐意持续优化这个零碎。另外,要感激一起参加我的项目的共事,他们的强力输入得以让我的项目在缓和的工期内保质保量上线,如期为业务线提供服务。
本篇文章到此结束,感激浏览。个好主见。冀望将来公司的业务能有大的倒退,对压测系统提出更高的要求,我也非常乐意持续优化这个零碎。另外,要感激一起参加我的项目的共事,他们的强力输入得以让我的项目在缓和的工期内保质保量上线,如期为业务线提供服务。
本篇文章到此结束,感激浏览。
本文在常识共享许可协定 4.0 下公布,转载请注明出处
作者:田小波
原创文章优先公布到集体网站,欢送拜访:https://www.tianxiaobo.com
本作品采纳常识共享署名 - 非商业性应用 - 禁止演绎 4.0 国内许可协定进行许可。