You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

261 lines
21 KiB
Markdown

2 years ago
# 07 | 工具进化:如何实现一个分布式压测平台
你好,我是吴骏龙。工欲善其事必先利其器,今天我将与你分享如何自己实现一个分布式压测平台。
现在只要是规模大一些的互联网公司,都在不遗余力地开发自己的压测平台,比如[京东](https://mp.weixin.qq.com/s?__biz=MzAwMDU1MTE1OQ%3D%3D&mid=2653548195&idx=1&sn=7668528e8521196f6ef122e85f6028e9&chksm=813a7f3bb64df62dfabba5e9de1ac8373ece36c792a11e8255b57abdaeba8697236c6361f926&scene=0&key=a226a081696afed0b5c63e7489f9cdcbaeac6955294907c42)、[美团](https://tech.meituan.com/2018/09/27/quake-introduction.html)、[阿里](https://www.aliyun.com/product/pts)、[360](http://test.360.cn/dabai/)。可能你会问市面上已经有无数的开源压测工具和平台比如JMeter、Locust、nGrinder、Gatling等为什么要自己做呢我在和一些大厂的同行交流经验时发现对于常见开源压测工具的诟病不外乎有以下几点。
![](https://static001.geekbang.org/resource/image/ee/yy/ee79af8c76bf32a18642f67590e4edyy.png)
从开源压测工具和平台的这些缺点中,我们可以看出,对于企业来说,自研压测平台就是要满足以下三点需求:
**1\. 平台化:** 企业需要一个平台化的压测工具每个团队都可以在这个平台上协作而开源工具大多是C/S类型 客户端/服务器体系结构),缺乏平台化支持。
**2\. 标准化:** 企业需要一个统一的标准化压测平台,最好能够和公司的审批流程、管理平台等集成,而开源工具在这方面的扩展性一般不强。
**3\. 控制成本:** 企业需要控制压测平台的维护成本,对于规模大的公司,自研优于使用开源。虽然开源压测工具由社区维护,但反馈较慢,自己维护的成本又比较高,不如重写一个或者二次开发。
在这一讲我会介绍一套由我设计的基于JMeter的分布式压测平台实现方案这套方案也兼顾了开源工具的一些成熟功能目前已经在阿里本地生活团队使用了超过4年能够支撑近百万压测并发量累计输出了近4000亿次请求量管理近600台压测机依然没有出现明显瓶颈。这些成绩到底是怎么做到的我们来一探究竟吧。
## 架构设计思路
首先,实现一个分布式压测平台,要能解决上面提到的绝大部分问题,需要实现的功能可以细化为以下几个方面:
* **用例管理:** 用户建立测试用例,包含脚本文件、数据文件和插件,平台进行分类管理并持久化。
* **压测执行:** 一键触发测试用例,可指定各种运行参数,可以指定多台压测机分布式执行,可以批量执行多个测试用例,压测过程中能够动态调节压测量。
* **实时结果(热数据):** 压测过程中,实时展示响应时间、吞吐量、错误率等概要数据。由于这些数据都是在压测时需要高频关注的,我们将其称之为“热数据”。
* **压测结果(冷数据):** 在压测结束后展示平均响应时间、平均吞吐量90/95/99线等更详尽的数据。这部分数据主要是供压测后的分析工作使用不需要实时获取因此被称之为“冷数据”。
* **压测机管理:** 平台能够与压测机进行交互,调度压测机完成压测执行工作。
* **安全保障:** 平台应具备一定的监控机制,对压测过程中的一些异常情况进行干预。
我们建设分布式压测平台的理念是“取其精华去其糟粕”,**尽可能复用已有开源工具的成熟功能,因为这部分功能相对稳定,不需要重复造轮子;对开源工具不成熟的功能应当规避,在压测平台中进行实现;对开源工具不具备的功能,则完全在压测平台中实现。**
下图是分布式压测平台的顶层设计图它是典型的Java Web项目平台本身不执行测试只做调度避免成为施压的瓶颈后台均使用JMeter执行测试。平台会对挂载的压测机进行心跳检测确保压测机是可用的。用例和数据文件可以存储在服务器本地或者采用[MinIO](https://min.io/)进行高可用的文件对象存储。压测期间产生的冷数据持久化至数据库热数据持久化至时序数据库InfluxDB并定期清理。平台允许挂载外部监控模块对压测过程进行干预。
![](https://static001.geekbang.org/resource/image/d0/ca/d023a5e15yy03a0e18dbdc1d8e898bca.png)
这里你需要注意在使用JMeter进行压测时如果并发量比较大单机的资源配置可能无法支撑这时需要联合多机进行分布式压测然而JMeter自身的分布式压测功能是有一定缺陷的
* JMeter的分布式执行和单机执行方式的差异较大需要做很多额外配置由此产生大量运维工作。
* JMeter分布式执行模式master节点通常不参与压测而是收集slave节点的压测信息这会造成一定程度上的资源浪费。
* JMeter分布式执行模式slave节点会将每个请求打点都实时回传给master节点造成大量的带宽消耗。
这时候怎么办呢上面我提到过对开源工具不成熟的功能应当规避因此我们可以在每台压测机中植入一个Agent它能够与压测平台服务器通过长连接的方式建立通信这样平台就可以直接对压测机进行调度。这种方案相当于我们在平台层重新实现了JMeter的分布式调度功能两者的实现对比见下表。
![](https://static001.geekbang.org/resource/image/c2/ff/c213f874bb301a7d75472bb5f0f704ff.png)
这个方案也一并实现了**压测过程中的冷热数据分离**冷数据在测试完成后才会传输如果不需要压测端数据甚至可以配置不存储冷数据因此该方案的扩展性是非常友好的理论上支持的TPS没有上限。
## 主要功能实现方案和原理
说完了需要实现的功能和架构设计思路相信你对我们要实现的压测平台已经有了初步认识下面我就从平台的6大功能实现方案和原理进行展开沿着基础功能到高阶功能的顺序进行讲解展开的粒度大约控制在让你稍加思考就能上手实现的程度你可以选择擅长的编程语言及前后端框架去实现具体功能。
如果你希望能更直观地看到整个平台的功能全貌,我也制作了一个预览视频,在这一讲的最后我会提供给你。
### 1.如何实现测试状态流转
测试状态流转是压测平台管理测试工作的核心,所以我放在第一个讲。和人类的生老病死一样,每一轮成功的测试工作会经历一个完整的生命周期,可以描绘成下面这条主线。
![](https://static001.geekbang.org/resource/image/58/ed/58bc9067cb030e1e17751ac896090fed.png)
其中,我将**配置、触发、运行、收集、清理定义为五大内部行为**(平台内部逻辑管控),这五大内部行为都会改变测试的状态;同时,我们还会允许一些外部行为去干预测试工作,比如在运行过程中需要人为停止测试,外挂的监控组件判断异常后主动熔断测试等,这些外部行为也会改变测试的状态。你可以通过下面的表格,理解各个行为对测试状态的影响,以及对应的后续行为是什么。
![](https://static001.geekbang.org/resource/image/54/1b/5451373e0210ee0f9210951410263f1b.png)
将这些状态变换的触发条件和转换过程绘制成状态流转图,就是下面这个样子。
![](https://static001.geekbang.org/resource/image/ay/7e/ayya5dd98b0c2a7b8c22c2aa6bbc8a7e.png)
说白了,压测平台对测试状态的管理,就是通过代码实现出这张图的所有逻辑。其中的关键是,**无论测试流程出现何种分支(正常或异常),最后都要能形成闭环(即起点一定最终要达到终点)**,这对系统的健壮性非常重要,因为如果测试状态卡在任何中间状态,本质都是平台对其失去了管理,测试的信息都丢失了。
### 2.如何获取和展示实时数据(热数据)
上面我讲到了实现测试状态流转的整体思路,其中在“运行中”状态下,我们需要获取压测时的实时数据,以便实时观察压测情况。
我们这套分布式压测平台实现方案是基于JMeter的但遗憾的是JMeter本身并不提供图形化的实时数据展示功能以往我们只能通过输出日志看到一些粗略的信息。在压测平台中我们就对实时数据展示功能进行了实现主要原理是通过JMeter的Backend Listener将测试结果实时发往InfluxDB同时平台向InfluxDB轮询查询数据得到实时曲线并展示给用户。
![](https://static001.geekbang.org/resource/image/5c/00/5cc95439e957c0a654fa0313969a2500.png)
当然,你也可以直接基于流行的开源数据可视化系统[Grafana](https://grafana.com/)进行数据展示。
另外补充一句我在2017年为JMeter贡献了基于UDP协议与InfluxDB传输数据的Backend Listener比起当时官方支持的HTTP协议传输效率更高被列为JMeter 3.3的核心改进项如果你也想使用这个UDP协议的Backend Listener的话请确保JMeter版本 ≥ 3.3,欢迎你尝试。
![](https://static001.geekbang.org/resource/image/e2/4a/e23f94d143a2c6482d239ea817064b4a.png)
### 3.如何处理结果数据(冷数据)
实时数据要讲究快,能实时观察压测结果,比如,我们只要看到响应时间和错误率就可以基本了解压测当时的状态了。但对结果数据要更讲究全,目的是在压测结束后对压测结果做详细分析时,能精细到看到每个报错信息是什么。
由于压测平台自己实现了分布式压测模式因此在拿到每台压测机的结果文件后也需要自行对这些结果文件的内容进行合并和解析并持久化记录下来。这里所谓的结果文件其实就是压测生成的JTL文件我们先来看下JTL文件的一个片段。
```
timeStamp,elapsed,label,responseCode,responseMessage,threadName,dataType,success,failureMessage,bytes,sentBytes,grpThreads,allThreads,URL,Latency,IdleTime,Connect
1617696530005,81,HTTP请求,200,OK,线程组 1-1,text,true,,2497,118,1,1,http://www.baidu.com/,78,0,43
1617696530088,32,HTTP请求,200,OK,线程组 1-1,text,true,,2497,118,1,1,http://www.baidu.com/,32,0,0
1617696530120,32,HTTP请求,200,OK,线程组 1-1,text,true,,2497,118,1,1,http://www.baidu.com/,32,0,0
1617696530152,32,HTTP请求,200,OK,线程组 1-1,text,true,,2497,118,1,1,http://www.baidu.com/,32,0,0
1617696530184,31,HTTP请求,200,OK,线程组 1-1,text,true,,2497,118,1,1,http://www.baidu.com/,31,0,0
1617696530216,31,HTTP请求,200,OK,线程组 1-1,text,true,,2497,118,1,1,http://www.baidu.com/,31,0,0
1617696530248,31,HTTP请求,200,OK,线程组 1-1,text,true,,2497,118,1,1,http://www.baidu.com/,31,0,0
1617696530280,31,HTTP请求,200,OK,线程组 1-1,text,true,,2497,118,1,1,http://www.baidu.com/,31,0,0
1617696530312,31,HTTP请求,200,OK,线程组 1-1,text,true,,2497,118,1,1,http://www.baidu.com/,31,0,0
1617696530343,32,HTTP请求,200,OK,线程组 1-1,text,true,,2497,118,1,1,http://www.baidu.com/,31,0,0
1617696530375,32,HTTP请求,200,OK,线程组 1-1,text,true,,2497,118,1,1,http://www.baidu.com/,32,0,0
```
JTL文件的特点很鲜明可以看到相对应每一行的单条结果数据的大小非常小大约只有100多个字节但总量很大上面的例子只是片段实际可能有几万到几百万条。如果我们只是简单的将所有数据存储起来将会占用大量的存储空间因此结果数据需要做预聚合再存入。
预聚合是怎么做的呢下面代码展示了聚合存储的数据结构核心是以labelJTL中的label作大分类维度errorMsg、errorCode等作小分类以时间作为聚合标准interval固定固定聚合为60个点从而保证存储大小不会过大。
```
{
"label": "upload", -- 大分类比如这条记录只针对upload label
"totalCount": 428,
"totalErrorCount": 12,
"errorMsg": [ -- 维度字段,作小分类
{
"msg": "io.exception",
"count": 12
},... -- 固定聚合成60个点
],
"errorCode": [
{
"code": "404",
"count": 12
},...
],
"count": [
12, …, 15
],
"error": [
12, …, 15
],
"rt": [
12, …, 15
],
"minRt": [
12, …, 15
],
"maxRt": [
12, …, 15
]
}
```
结果数据的这种存储方式,既保证了不会占用太大的存储空间,又能够汇总出丰富全面的数据。
### 4.如何进行吞吐量限制与动态调节
测试状态流转、冷热数据的获取和处理是压测平台最基本的功能,下面我来介绍一些更高阶的功能,先从吞吐量控制与动态调节开始吧。在压测时,**“控量”是非常重要的**JMeter是根据线程数大小来控制压力强弱的但我们制定的压测目标中的指标往往是吞吐量QPS/TPS这就给测试人员带来了不便之处必须一边调整线程数一边观察QPS/TPS达到什么量级了。
为了解决这个问题JMeter提供了吞吐量控制器的插件我们可以通过设定吞吐量上限来限制QPS/TPS达到控量的效果。
![](https://static001.geekbang.org/resource/image/0a/c2/0ac99b79e3753b70b37b36bb907f0ac2.png)
上面的做法能够确保将吞吐量控制在一个固定值上,但这样还远远不够,实际工作中我们希望在每次压测执行时能够随时调节吞吐量,比如,在某个压力下服务容量没有问题,我们希望在不停止压测的情况下,再加一些压力,这样的功能该如何实现呢?
我提供的方案也很简单,依然是基于吞吐量控制器,基本的实现原理是将吞吐量限制值设为占位符(如下图中的${\_\_P(throughput, 99999999)}throughput就是占位符利用JMeter的BeanShell功能通过执行外部命令的方式在运行时注入具体值达到动态调节吞吐量的目的。
![](https://static001.geekbang.org/resource/image/f2/47/f26afba15da8ccf95488020982a81947.png)
上面提到的外部命令具体为:
```
java -jar <jmeter_path>/lib/bshclient.jar localhost 9000 update.bsh <qps>
```
其中update.bsh文件的内容为
```
import org.apache.jmeter.util.JMeterUtils;
getprop(p){ // get a JMeter property
return JMeterUtils.getPropDefault(p,"");
}
setprop(p,v){ // set a JMeter property
print("Setting property '"+p+"' to '"+v+"'.");
JMeterUtils.getJMeterProperties().setProperty(p, v);
}
setprop("throughput", args[0]);
```
压测平台将上述这些工作统一封装后提供出接口,前端界面只要留出输入框供用户填写吞吐量的参数,就可以方便的使用了。
### 5.如何实现配置集功能
我们再来聊一个和压测运行相关的重要功能,称之为“配置集”。这个功能其实当初并不在平台设计的考虑范围内,但随着全链路压测规模的日益壮大,需要同时执行的用例数量越来越多,每次执行时都得一个一个去触发,手忙脚乱,有了这个痛点,引发了我们对配置集功能的探索。
**配置集功能的本质是“批量运行多个测试用例”**,如下图所示,用户只需提前在配置集中添加需要执行的测试用例,以及每个执行轮次的配置信息,比如线程数、持续时间等。配置集设定完成后,选择某个轮次,就能基于相应的配置一键触发所有测试用例。
![](https://static001.geekbang.org/resource/image/48/94/48ac6d200a5c1c0e9792014ef38cef94.png)
从实现的角度来讲,配置集是映射多个用例的数据结构,而刚我们提到的“轮次”的概念,是指对同一个配置集设定多轮不同的配置项,每个配置项还是作用在测试用例上,目的是进一步提高可复用性。简而言之,**记住这个公式:“配置集 1N 测试用例;测试用例 1N 轮次配置”**,即一个配置集对应多个测试用例,一个测试用例对应多个轮次配置。再直观一些,你可以直接通过下面的代码理解这个逻辑。
```
{
"_id" : ObjectId("603c7f8f587ca226d257b144"),
"testPlanName" : "addTestPlan",
"testPlanUnitList" : [ -- 一个配置集对应多个测试用例
{
"testcaseId" : "5fc4dd4ee9d7d7b19d0f5152",
……
"testRoundList" : [ -- 一个测试用例对应多个轮次配置
{
"threadCount" : 1,
"duration" : NumberLong(60),
"rampUp" : 0,
"detailLog" : true,
"realTimeLog" : true,
"throughput" : 100,
"name" : "配置1",
"qpsStep" : 0
} ……
]
}
],
……
}
```
由于在配置集中同时管理着多个用例的所有信息因此还可以实现一些高级操作比如某个用例先执行一段时间后其他用例再启动其本质就是单独先触发一个用例等待固定时间后再触发其余用例或是运行时临时改变其中几个用例的QPS上限其他用例保持不变其本质就是对单个用例进行吞吐量调节等等。
### 6\. 监控模块
我已经介绍了很多关于压测运行和压测数据的功能模块,最后我们来学习一下监控模块,它同样也是压测平台非常重要的组成部分,也是**几乎所有开源压测工具都缺乏的功能模块**。监控模块可以分为两类,分别是内部监控模块和外部监控模块。
内部监控模块主要针对压测平台自身运行过程进行监控和干预,观察压测是否处于正常进行中,如果遇到异常情况,如线程异常终止、没有持续的测试数据流出、磁盘打满等,则立刻终止测试,反馈异常结果并记录日志供排查。
内部监控模块实现起来也很简单,在触发压测后,我们也同时启动一个任务对使用的压测机进行监控,监控的内容可以是磁盘使用量、压测数据流的状态等等,如果识别到异常,则触发相应的异常逻辑即可。
第二类监控为外部监控模块,主要用来对接外部监控系统,这个模块很重要,除了方便观察系统指标以外,其最关键的作用是能够反向干预压测工作,协助用户规避风险,尤其是针对线上压测这类高风险工作。比如,当监控到服务端的错误率达到一定阈值时,立刻停止当次测试。
外部监控模块的实现,需要与外部监控系统提供的接口对接,如果有些监控系统自带报警功能,那么就更好了,压测平台在获取到报警信息后可以立刻停止测试。
以对接Grafana监控为例最简单的方式莫过于采用Webhook的方式我们只需要指定一个接口并配置到Grafana中在监控告警事件发生时Grafana就会回调这个接口触发相应的停止测试的动作。当然也可以触发其他动作这取决于接口的逻辑。在[Grafana使用手册](https://grafana.com/docs/grafana/latest/alerting/notifications/#webhook)中提供了详细的对接案例,你可以进一步阅读,加深理解。
## 总结
工欲善其事必先利其器,压测平台作为容量保障的工具枢纽,其地位不言而喻。一个扩展性好、设计健壮、体验优秀的压测平台,能够对容量保障工作带来巨大帮助。
这一讲中我介绍了一个基于JMeter的分布式压测平台的实现方案它解决了企业对于压测工具的三个重要诉求平台化、标准化和控制成本。如果你的团队已经习惯于使用流行的JMeter进行压测那么上手这个平台几乎是没有什么成本的因为它100%兼容JMeter。
平台的主要功能可以分为几大部分去看,首先是测试状态,我们明确了测试行为和测试状态之间的流转关系,确保无论测试过程是正常执行还是异常终止,整个流程都能闭环完结。
其次是测试数据,我们将数据分为热数据和冷数据,分别对应实时数据和结果数据,并采用了完全不同的思路去实现,确保各自的特点能够充分发挥。
接下来我们聊到了测试运行过程中的两个重要功能吞吐量动态调节和配置集。其中吞吐量动态调节利用了JMeter BeanShell的动态传参功能我们只需要暴露吞吐量作为参数即可而配置集则是平台对多测试用例运行的一种实现能够方便我们批量执行大量测试用例。
最后,监控模块是压测工作安全性的重要保证,内部监控模块对压测本身的状态进行检查,如有异常及时反馈;外部监控模块则是对接外部监控系统,在服务出现异常时主动终止测试。
通过今天的学习,希望能够让你了解分布式压测平台实现的重点和难点,也期待你能够创造更多方便友好的功能,为容量保障工作的降本提效贡献一份力。
## 课后讨论
这里,我给出一个分布式压测平台的预览视频,包含这一讲提到的所有功能,它可以作为你的实现蓝本。如果观看了视频后,你有了什么新的思路或启发,欢迎分享给我,我们共同探讨。