用k6进行性能测试

575 阅读6分钟

在软件行业有一句老话--过早优化是万恶之源。这句话的一个推论应该是--没有优化和过早优化一样糟糕

通常情况下,在功能正确实现、正确和稳定之前,花费在优化系统上的努力是浪费的,但如果不考虑性能,即使是一个完全实现的系统也会提供一个糟糕的用户体验。不幸的是,当交付时间被压缩时,性能往往会被排在更多系统功能的后面--但如果用户因为性能不佳而难以与之互动,那么即使是世界上最好的功能也毫无用处。在功能和性能之间找到一个平衡点是关键。

什么是负载测试?

虽然有许多性能测试工具可以满足各种需求,但在这篇文章中,我们将专注于使用一个特别的工具对HTTP服务进行性能测试:K6

为什么是k6而不是其他负载测试工具?

如果你在2020年从事软件工程工作,你或你认识的人有可能正在使用TypeScript--或至少是ES6--编写一个现代Web应用程序。该Web应用程序也可能通过HTTP与后端服务进行通信,这些服务实现了应用程序的大部分业务逻辑和数据管理。

k6允许你将这些JavaScript技能转移到为你的应用程序的HTTP后端编写性能测试脚本上,减少了开始看到你的应用程序中有意义的性能洞察力所需的时间和投资。

虽然k6支持JavaScript,但它不在Node.js上运行--它使用自己的Go语言编写的运行时引擎。这样做有两个主要原因:减少其标准库的表面积,只关注与性能测试相关的API,并帮助使脚本执行性能接近裸机,而不是基于更通用的运行时引擎的性能测试工具所能实现的(警告:该文章中的k6偏向 - 尽管它不是最好的性能!)。

总的来说,k6在脚本灵活性和执行性能之间取得了良好的平衡。这包括关于其功能的广泛文档,以及可以转换从Web浏览器记录的用户旅程(HAR文件)的工具,或者转换你的团队可能已经有的其他预先存在的性能测试脚本,如果他们使用JMeter(一种流行的基于Java的后端性能测试工具)。K6也有一个基于云的性能脚本执行环境的商业产品,包括预先配置的分析仪表板,让你专注于编写测试脚本和在应用程序中实施性能改进。

如何开始使用k6

使用k6团队提供的docker-compose设置是启动和运行有意义的性能测试和分析的最快捷方式。这使你有能力:

假设你已经安装了docker,你的第一个性能测试例子可以通过以下命令运行:

git clone --depth 1 'https://github.com/loadimpact/k6'
cd k6
docker-compose up -d
docker-compose run -v $PWD/samples:/scripts k6 run --no-usage-report -w /scripts/es6sample.js

分析k6输出

k6 run 命令的输出显示了一系列打勾或交叉的验证步骤,类似于传统的功能测试工具。这些检查允许k6脚本根据预期的阈值断言观察到的性能指标,以及验证类似于常规功能测试的响应值。当测试有状态应用程序中更复杂的工作流时,检查响应往往包括提取出需要作为参数提供给后来的端点的值。

k6命令行输出的最后一部分是一个性能指标的汇总表,主要与HTTP数据协商和传输的各个阶段有关。特别值得注意的是http_req_duration ,它代表了发送请求、等待后端响应以及最终接收响应数据所花费的时间。这个指标不包括客户端查找DNS条目和执行TLS握手等所花费的时间,因此更接近于代表应用程序的HTTP后端执行其工作的实际时间。

其余的HTTP时间仍然与整体性能分析有关,因为它们可以代表应用程序的用户在给定请求中的体验,但这些值与k6的运行环境更具体相关,所以可能不完全代表真实世界的用户体验。

k6性能脚本的结构

那么k6在这里实际测试的是什么?对于上面的例子,打开./samples/es6sample.js file. 你会对默认的函数输出最感兴趣--这就是k6为一个虚拟用户(VU)运行的性能测试迭代。还有一些其他的k6脚本的顶层部分,用于更细化的定制。

配置执行

脚本顶部的options 输出允许你预先配置某些k6运行时参数。在编写你的第一个性能测试脚本时,你可以省略这个变量,因为你很可能会从命令行执行你的脚本,并可以从那里进行自定义。在脚本中嵌入选项在寻求自动化性能测试时变得非常有用,例如在与功能测试一起集成到你的构建管道时。

默认情况下,当options 变量不存在时,k6 run [perfscript.js] 将使用一个虚拟用户来执行测试脚本的单一迭代。这种类型的执行在最初编写和调试性能测试脚本时很有用。然而,在任何有意义的用户负载下,它不会给你带来太多的性能洞察力(尽管它对你的应用程序的新版本的性能测试很有用)。

你可以分别使用-u-i 命令行参数来增加虚拟用户的数量和总迭代次数。如果你想通过时间而不是迭代次数来限制总的脚本持续时间,你也可以使用-d 参数。

测试性能

如前所述,该脚本的重点是默认函数。这个函数应该包括一个或多个HTTP请求,用于与你的应用程序的服务层的相关方面进行交互。每个HTTP请求都会有自己的性能指标被记录下来,表明请求完成的时间--这个计时的一部分将表明后端服务完成其工作的时间。

POST 应用程序在访问其他端点之前通常需要某种形式的认证,例如对/login 端点的请求。k6隐含地处理单个虚拟用户的多个HTTP请求的cookie管理,这意味着除了调用你的认证端点之外没有更多的事情要做,假设你的应用程序依赖于典型的授权或会话cookie。

当测试多个端点作为用户通过你的应用程序的部分凝聚力的一部分时,你很可能想开始对你的脚本的区域进行分组,以提供一个更接近用户特征的结构,而不是一系列孤立的HTTP请求。这种按用户特征分组的方式是group() 功能发挥作用的地方(它也可能被嵌套)--尽管k6也支持标签,以增加你的测试结构的维度。

当涉及到写脚本的大部分时,有几种不同的方法可以采取。基本上,你可以从一张白纸开始,手动添加你感兴趣的测试端点的请求,或者你可以从自动生成的脚本开始,基于HAR记录你的应用程序的用户界面中的典型用户旅程。从HAR开始,对于更复杂的旅程是有用的,因为它将更准确地反映用户在使用你的应用程序时需要做的一切--包括步骤之间的任何人工处理延迟。

当提高性能脚本产生的负载时,你可能不想在每次测试迭代中包括你的应用程序的某些方面,例如你的Web应用程序可能已经在缓存的不变的静态数据。你可以使用k6执行生命周期的附加设置和拆分阶段来处理这个问题。

使用动态数据

从HAR中启动k6脚本将需要修改,因为初始旅程将被硬编码为只有那些作为记录的一部分使用的参数值。这种硬编码不会提供对各种不同数据集的应用程序性能的广泛覆盖。

在你的测试中使用动态数据可能需要参数化的HTTP请求,包括从以前的响应中提取的数据。为了帮助生成动态数据,你可以使用k6的模块支持来导入一个库,如faker.js

如果这些动态数据出现在一个静态端点URL的POST请求体中,那么在分析这个单一端点的性能时就没有太大问题,但如果端点名称本身需要得到参数化,这就需要更多的努力。默认情况下,k6在对其记录的指标进行标记时,会对每个独特的端点名称进行区分。如果你的目标是查看GET /product/aGET /product/b 之间的性能差异,这种区分是有帮助的,但是如果你要寻找带有各种参数的GET /product/{productId} 的整体性能分析,这种区分就不那么有用了。为了解决这个问题,你可以为一个给定的请求覆盖name 标签 - 所有使用相同的name 别名的请求都会被分组。

把事情放在一起:k6脚本示例

下面的脚本强调了上面描述的许多动态数据概念:

import { group, sleep, check } from 'k6';
import http from 'k6/http';
import faker from 'https://cdnjs.cloudflare.com/ajax/libs/Faker/3.1.0/faker.min.js';

const BASE_URL = `https://myapp.example.com`;

const COMMON_REQUEST_HEADERS = {
   dnt: '1',
   'user-agent': 'Mozilla/5.0',
   'content-type': 'application/json',
   accept: '*/*',
   origin: BASE_URL,
   referer: BASE_URL
};

function simulateUserInteractionDelay() {
   sleep(1 + Math.random(3));
}

export default function() {
   group('myapp performance test', function() {
       group('authenticate', function() {
           let response = http.post(
               `${BASE_URL}/login`,
               '{username:"whoami",password:"verysecure"}',
               {
                   tags: { name: '/login' },
                   headers: COMMON_REQUEST_HEADERS
               }
           );
           check(response, {
               'can login': (res) => res.status === 201
           });
       });

       simulateUserInteractionDelay();

       let productId;
       group('add product', function() {
           let productName = faker.commerce.productName();

           let response = http.post(`${BASE_URL}/product`, `{productName: "${productName}"}`, {
               tags: { name: '/product' },
               headers: COMMON_REQUEST_HEADERS
           });
           check(response, {
               'can add product': (res) => res.status === 201,
               'can obtain product ID': (res) => {
                   let productResponse = JSON.parse(res.body);
                   productId = productResponse && productResponse.id;
                   return productId !== undefined;
               }
           });
       });

       simulateUserInteractionDelay();

       group('fetch product', function() {
           let response = http.get(`${BASE_URL}/product/${productId}`, {
               tags: { name: '/product/{productId}' },
               headers: COMMON_REQUEST_HEADERS
           });
           check(response, {
               'can get product': (res) => res.status === 200
           });
       });
   });
}

使用k6获得更详细的性能分析

k6的命令行输出在很大程度上是对整个脚本执行的总结。当你的脚本在测试更复杂的用户旅程时,涉及到几个不相关的后端点时,它并没有什么帮助。在Grafana中对完整的指标集进行更细化的分析,对每个端点名称进行标记,是更好的做法。

docker-compose设置使Grafana在.NET上可用。然而,这是一个原始安装,没有预设仪表盘。你可以根据你的分析需求创建自己的仪表盘,但在刚开始的时候,使用预制的仪表盘并进一步定制往往会更快。要做到这一点,你需要通过dashboard/import导入一个dashboard--一个好的开始是dashboard ID#2587 --或者4411 ,也值得探索。

在进行分析和准确理解你的应用程序的性能方面--不幸的是,这是个比这里要大得多的话题!Brendan Gregg提供了大量的关于分析的信息。Brendan Gregg提供了大量的信息,帮助指导你使用合理的方法,避免最常见的指标解释陷阱,还有作者的书,可以对这个主题进行更彻底的回顾。

结论

k6提供了一个全面的性能测试生态系统,可以为你的应用程序的响应性增加重要的价值,无论你是为特别慢的区域寻找临时的手动性能调查,还是你想把性能测试整合为整个自动化测试套件的一部分。虽然它不是工作的唯一工具,但它很适合现代全栈应用开发。

性能测试不像功能测试那样是二进制的,一个功能要么工作要么不工作。在权衡了有多少用户会与一个性能不佳的功能进行交互,以及为提高其性能所需的投资之后,往往需要达成妥协。要实现 "100%的完美性能 "是不可能的,因为这样的概念并不存在;你可以花无限多的时间来优化你的应用程序,而不利于常规功能的开发。因此,必须收集有用的性能测量数据,让你专注于最重要的几个瓶颈,这些瓶颈如果被修复,会给终端用户带来最明显的感知改善。瓶颈列表中也会有一个分界点,在这个分界点上,如果任何其他的瓶颈得到改善,它们只会提供越来越少的、难以察觉的回报。

无论你是否选择使用k6,你必须正确分析和考虑你的应用程序的性能。性能是一块经常被遗忘的拼图,是为你的用户提供最好的体验。请确保你能看到完整的拼图图片!