背景
近期我们有个号称会有很高很高并发的系统要上线,为了测试一下自己开发的系统的负载能力,准备了点海克斯科技,来看看抗不抗的住。
之前笔者写过用Apache JMeter进行压力测试的文章(传送门👉:https://xie.infoq.cn/article/9c156894b7374e1bd9bc6271f),这次换成了K6,实在不是因为爱折腾,而因为我们是小团队,没那么多工种,很多事儿基本都是开发自己来,K6在配置上比Jmeter更加简单,而且支持JavaScript脚本,更适合我们的测试场景。
部署环境
安装K6
这部分,其实没什么可多说的,参照官网的手册即可(传送门👉:https://grafana.com/docs/k6/latest/),我是在Ubuntu 22.04系统进行的安装。
*安装vscode
这一步其实可有可无,只是写脚本的话,Linux自带的vi,或者安装一下vim,nano等都完全足够,但我个人还是比较推荐结合vscode的remote-ssh插件,来编写远程服务器上的文件。
尤其是K6的测试脚本是JavaScript脚本,搭配vscode来写的话会更方便一些。
这里安装步骤也不细说了,在vscode插件市场搜索remote-ssh安装即可
如果使用的是wsl2,那会更加方便,进入编写k6脚本的路径下,直接输入
code .
即可在宿主机编写脚本。
测试条件
我这里要测试的对象,是一个在线考试系统,为了尽可能真实的模拟考试行为,我这里准备了几个接口,分别模拟了考试信息加载,身份验证,保存试卷草稿3个使用频率高的接口。
这里我就放一个最主要的,模拟用户保存草稿的接口
/// <summary>
/// 模拟保存草稿
/// </summary>
/// <param name="answer">提交一个长字符传</param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> SaveDraft(SimulateSaveDto dto)
{//模拟验证header的耗时await Task.Delay(new Random().Next(10, 300));int rd = new Random().Next(0, 100);var randomOne = (await _simulation1Repo.getListAsync(u => u.Id > 0))[rd];dto.sid=randomOne.Id;dto.answer = Utils.GenerateRandomCodeFast(new Random().Next(2, 8000));if (dto.answer.Length > CapConsts.CapMsgMaxLength){await _redisCachingProvider.StringSetAsync("tmpSimulate" + dto.sid, dto.answer, TimeSpan.FromMinutes(1));dto.answer = "";}await _capPublisher.PublishAsync(CapConsts.ClientPrefix + "SimulateSaveDraft", dto);Logger.Warning($"{DateTime.Now}:发布事务---模拟提交答案");return Json(_resp.success(dto.sid));
}
简单解释下这段代码,前半部分就是模拟构造了一个包含大字段的参数,因为用户提交的草稿可能会很大,我们目前的业务,光单选题就有100道,题目的id类型都是雪花算法生成的id,所以拼接完也是一个很大的字符,如果包含主观题,就更大了。
这里其实可以增量保存,但因为有其他业务关联,而且增量提交也有问题,比如用户第一份草稿提交的1-10题,到第三份草稿提交的25-30题,第四份又回去改动了第1题或者前面的题,这样又引入了新的复杂逻辑,所以这部分不多说,暂时就是这样全量保存草稿的场景。
后半部分是我使用消息队列的方式,把保存草稿的动作提交到队列里,这里在提交队列的时候做了一个前置处理,就是当答案的字符过长,就把答案暂存到Redis里,保证队列里传输的消息都不大。
一个小坑
之所以这样做,也是压测环节中发现的,因为我是使用PostgreSql来持久化存储的队列消息,EventBus组件用的CAP,当CAP在Pg里创建对列表的时候,会创建对应的索引,而当队列传输的消息过大,会报一个索引超长的错误,当然存储改为SQL Server或者手动把索引删掉就不会有这个问题,但为了确保更高的可用性,还是选择了规避。
事实上,正式接口里这里是采用的分段保存,思路就是分解答案,当字符串长度超过设定的最大长度,我们可以把字符串转成char数组,然后利用linq里对操作数组的chunk方法,分段保存用户答案,就可以避免这个问题了,也不用担心大字段临时存在Redis造成的性能问题,这篇的要点不在这里,就不细聊了。
测试脚本
测试脚本其实写完了回去看,还是挺简单的,但磨人的地方主要是在调整参数的过程,我们要根据系统的实际情况,编写压测的递进参数,尽可能的贴近实际情况,该快的时候快,该慢的时候慢,请求密度要尽可能的大,等等。
好了,直接看下这个脚本吧
import http from 'k6/http';
import { check, sleep } from 'k6';export const options = {thresholds: {checks: ['rate>0.95'], // 至少 95% 的请求必须成功http_req_duration: ['p(95)<500'] // 95% 的请求响应时间小于 500 毫秒}// 设置并发用户数,stages: [{ duration: '10s', target: 10 }, // 起始阶段,10个线程运行30秒{ duration: '30s', target: 100 }, // 在接下来的30秒内将线程数增加到100{ duration: '30s', target: 300 }, // 再接下来的30秒内将线程数增加到300{ duration: '30s', target: 500 }, // 再接下来的30秒内将线程数增加到500{ duration: '30s', target: 800 }, // 再接下来的30秒内将线程数增加到800{ duration: '60s', target: 1000 }, // 再接下来的60秒内将线程数增加到1000{ duration: '120s', target: 1000 } // 保持1000个程运行120秒],
}export default function () {const url = '压测接口';let params = {headers: {'Content-Type': 'application/json'}};let payLoad ={answer:"[]",}let res = http.post(url,payLoad, params);// 解析响应体let response = JSON.parse(res.body);// 定义检查点let checks = {'status code is 200': (r) => r.status === 200,'API code is 0': (r) => response.code === 0};// 执行检查点check(res, checks);// 记录非成功的响应if (response.code !== 0) {console.log(response);//console.log(`Error: code=${response.code}, msg=${response.msg}, data=${JSON.stringify(response.data)}`);}sleep(2)}
控制台打印出来的结果
事实上,控制台打印的这些数据已经足够我们分析问题了,如果我们想更直观的观看压测过程的数据情况,可以开启webdashboard
K6_WEB_DASHBOARD=true k6 run simulatescript.js
压测时,被测系统打印的日志
这样最后生成的报告会像这样👇
最后,其实不论是任何一种压测工具给出的报告,大家其实都不用有什么心智负担,因为现在是AI的时代,当我们想深入了解报告中所有参数的含义时,可以把报告直接扔给大模型,让大模型帮我们解读,这样我们也不用担心我们辛苦写的报告领导看不懂,当然我们自己也能起到促进作用,毕竟谁也不想完全被大模型替代,还是要有自己的见解才好。
好了,基本就这些内容了,下篇再细聊聊系统性能优化过程遇到的问题。