如何用Artillery为Node.js应用程序设置负载测试工作流程

1,148 阅读5分钟

使用Artillery对Node.js APIs进行负载测试的指南

Artillery是一个开源的命令行工具,专门用于负载测试和烟雾测试网络应用。它是用JavaScript编写的,支持测试HTTP、Socket.io和WebSockets APIs。

本文将让你开始使用Artillery对你的Node.js APIs进行负载测试。在你将代码部署到生产中之前,你将能够检测并修复关键的性能问题。

不过,在我们深入研究并为Node.js应用程序设置Artillery之前,让我们首先回答这个问题:什么是负载测试,为什么它很重要?

为什么要在Node.js中进行负载测试?

负载测试对于量化系统性能和确定应用程序开始失败的突破点至关重要。负载测试通常涉及模拟用户对远程服务器的查询。

负载测试再现了真实世界的工作负载,以衡量系统在一段时间内对指定负载量的反应。你可以确定一个系统在其设计处理的负载下是否表现正确,以及它对流量高峰的适应性如何。它与压力测试密切相关,压力测试评估一个系统在极端负载下的表现,以及一旦流量恢复到正常水平,它是否能够恢复。

负载测试可以帮助验证一个应用程序是否可以承受现实的负载情况,而不会出现性能下降的情况。它还可以帮助发现一些问题,如增加的响应时间、内存泄漏、各种系统组件在负载下的不良性能,以及其他导致次优用户体验的设计问题。

在这篇文章中,我们将专注于免费和开源版本的Artillery来探索负载测试。然而,请记住,Artillery的专业版本也可用于那些需求超过免费版本的用户。它为大规模测试提供了额外的功能,即使你之前没有DevOps经验,也可以使用。

安装Artillery for Node.js

Artillery是一个npm包,所以你可以通过npmyarn 来安装它。

$ yarn global add artillery

如果这样做成功了,artillery 程序应该可以从命令行中访问。

$ artillery -V
        ___         __  _ ____                  _
  _____/   |  _____/ /_(_) / /__  _______  __  (_)___  _____
 /____/ /| | / ___/ __/ / / / _ \/ ___/ / / / / / __ \/____/
/____/ ___ |/ /  / /_/ / / /  __/ /  / /_/ / / / /_/ /____/
    /_/  |_/_/   \__/_/_/_/\___/_/   \__, (_)_/\____/
                                    /____/

------------ Version Info ------------
Artillery: 1.7.7
Artillery Pro: not installed (https://artillery.io/pro)
Node.js: v16.7.0
OS: linux/x64
--------------------------------------

Artillery的基本用法

一旦你安装了Artillery CLI,你就可以开始使用它来向Web服务器发送流量。它提供了一个quick 子命令,可以让你运行一个测试,而不用先写一个测试脚本。

你需要指定。

  • 一个端点
  • 每秒的虚拟用户率或固定数量的虚拟用户
  • 每个用户应该有多少个请求。
$ artillery quick --count 20 --num 10 http://localhost:4000/example

上面的--count 参数指定了虚拟用户的总数,而--num 表示每个用户应该发出的请求数。因此,200(20*10)个GET请求被发送到指定的端点。在成功完成测试后,一份报告被打印到控制台。

All virtual users finished
Summary report @ 14:46:26(+0100) 2021-08-29
  Scenarios launched:  20
  Scenarios completed: 20
  Requests completed:  200
  Mean response/sec: 136.99
  Response time (msec):
    min: 0
    max: 2
    median: 1
    p95: 1
    p99: 2
  Scenario counts:
    0: 20 (100%)
  Codes:
    200: 200

这显示了关于测试运行的几个细节,如完成的请求、响应时间、测试所需时间等。它还显示在每个请求上收到的响应代码,这样你就可以确定你的API在超载的情况下是否能优雅地处理失败。

虽然quick 子命令对于从命令行执行一次性测试很方便,但它所能实现的功能相当有限。这就是为什么Artillery提供了一种方法,通过YAML或JSON格式的测试定义文件来配置不同的负载测试场景。这允许非常灵活地模拟一个或多个应用程序端点的预期流量。

编写你的第一个火炮测试脚本

在本节中,我将演示一个基本的测试配置,你可以应用于任何应用程序。如果你想跟着做,你可以为你的项目建立一个测试环境,或者在本地运行测试,以便你的生产环境不受影响。确保你将Artillery安装为开发依赖,这样你使用的版本在所有部署中都是一致的。

$ yarn add -D artillery

Artillery测试脚本由两个主要部分组成:configscenariosconfig 包括测试的一般配置设置,如目标、响应超时、默认的HTTP头等。scenarios 包括虚拟用户在测试中应该做的各种请求。下面是一个测试端点的脚本,每秒钟发送10个虚拟用户,持续30秒。

config:
  target: "http://localhost:4000"
  phases:
    - duration: 30
      arrivalRate: 10

scenarios:
  - name: "Retrieve data"
    flow:
      - get:
          url: "/example"

在上面的脚本中,config 部分在target 属性中定义了被测试的应用程序的基本URL。脚本中后面定义的所有端点都将针对这个基本URL运行。

然后,phases 属性被用来设置在一段时间内产生的虚拟用户的数量,以及这些用户被发送到指定端点的频率。

在这个测试中,duration 决定了虚拟用户将在30秒内生成,arrivalRate 决定了每秒发送到端点的虚拟用户数量(10个用户)。

另一方面,scenarios 部分定义了一个虚拟用户应该执行的各种操作。这是通过flow 属性来控制的,它指定了应该按顺序执行的确切步骤。在这种情况下,我们有一个单一的步骤:向基本URL上的/example 端点发出一个GET请求。Artillery生成的每个虚拟用户都会发出这个请求。

现在我们已经写好了我们的第一个脚本,让我们深入了解如何运行一个负载测试。

在Artillery中运行一个负载测试

把你的测试脚本保存到一个文件中(比如load-test.yml ),然后通过下面的命令执行它。

$ artillery run path/to/script.yml

这个命令将开始以每秒10个请求的速度向指定的端点发送虚拟用户。每隔10秒就会有一份报告打印到控制台,告知你在该时间段内启动和完成的测试方案的数量,以及其他统计数据,如平均响应时间、HTTP响应代码和错误(如果有)。

一旦测试结束,在命令退出之前,会打印出一份总结报告(与我们前面检查的报告相同)。

All virtual users finished
Summary report @ 15:38:48(+0100) 2021-09-02
  Scenarios launched:  300
  Scenarios completed: 300
  Requests completed:  300
  Mean response/sec: 9.87
  Response time (msec):
    min: 0
    max: 1459
    median: 1
    p95: 549.5
    p99: 1370
  Scenario counts:
    Retrieve data: 300 (100%)
  Codes:
    200: 300

如何创建真实的用户流

我们在上一节执行的测试脚本与quick 的例子没有什么不同,因为它只向一个端点发出请求。然而,你可以使用Artillery来测试应用程序中更复杂的用户流。

例如,在一个SaaS产品中,一个用户流可能是:有人登陆你的主页,查看价格页面,然后注册免费试用。你肯定会想知道,如果成百上千的用户同时试图执行这些操作,这个流程在压力下的表现如何。

下面是你如何在Artillery测试脚本中定义这样一个用户流。

config:
  target: "http://localhost:4000"
  phases:
    - duration: 60
      arrivalRate: 20
      name: "Warming up"
    - duration: 240
      arrivalRate: 20
      rampTo: 100
      name: "Ramping up"
    - duration: 500
      arrivalRate: 100
      name: "Sustained load"
  processor: "./processor.js"

scenarios:
  - name: "Sign up flow"
    flow:
      - get:
          url: "/"
      - think: 1
      - get:
          url: "/pricing"
      - think: 2
      - get:
          url: "/signup"
      - think: 3
      - post:
          url: "/signup"
          beforeRequest: generateSignupData
          json:
            email: "{{ email }}"
            password: "{{ password }}"

在上面的脚本中,我们在config.phases 中定义了三个测试阶段。

  • 第一阶段每秒钟向应用程序发送20个虚拟用户,持续60秒。
  • 在第二阶段,负载将从每秒20个用户开始,在240秒内逐渐增加到每秒100个用户。
  • 第三和最后一个阶段模拟每秒100个用户的持续负载,持续500秒。

通过提供几个阶段,你可以准确地模拟真实世界的流量模式,并测试你的系统对突然出现的大量请求的适应性。

每个虚拟用户在应用中的步骤是在scenarios.flow 。第一个请求是GET / ,它导致了主页的出现。之后,在向/pricing 发出下一个GET请求之前,会有1秒钟的停顿(通过think 配置),以模拟用户的滚动或阅读。再延迟2秒后,虚拟用户向/signup 发出GET请求。最后一个请求是POST /signup ,它在请求体中发送了一个JSON有效载荷。

{{ email }}{{ password }} 占位符是通过generateSignupData 函数填充的,该函数在请求发出前执行。这个函数被定义在config.processor 中引用的processor.js 文件中。通过这种方式,Artillery可以让你指定自定义钩子,在测试运行中的特定点执行。下面是processor.js 的内容。

const Faker = require('faker');

function generateSignupData(requestParams, ctx, ee, next) {
  ctx.vars['email'] = Faker.internet.exampleEmail();
  ctx.vars['password'] = Faker.internet.password(10);

  return next();
}

module.exports = {
  generateSignupData,
};

generateSignupData 函数使用Faker.js提供的方法,在每次调用时生成一个随机的电子邮件地址和密码。然后将结果设置在虚拟用户的上下文上,并调用next() ,这样场景就可以继续执行。你可以使用这种方法在你的测试中注入动态的随机内容,使它们尽可能地接近真实世界的请求。

注意,除了beforeRequest ,还有其他钩子可用,包括以下内容。

  • afterResponse - 在收到端点的响应后执行一个或多个函数。
- post:
    url: "/login"
    afterResponse:
      - "logHeaders"
      - "logBody"
  • beforeScenario 和 - 用于在一个场景中每个请求之前或之后执行一个或多个函数。afterScenario
scenarios:
  - beforeScenario: "setData"
    afterScenario: "logResults"
    flow:
      - get:
          url: "/auth"
  • function - 可以在一个场景中的任何一点执行功能。
- post:
    url: "/login"
    function: "doSomething"

从有效载荷文件中注入数据

Artillery还允许你通过CSV格式的有效载荷文件注入自定义数据。例如,你可以在CSV文件中预定义此类数据的清单,而不是像我们在上一节中所做的那样,临时生成假的电子邮件地址和密码。

Dovie32@example.net,rwkWspKUKy
Allen.Fay@example.org,7BaFHbaWga
Jany30@example.org,CWvc6Bznnh
Dorris47@example.com,1vlT_02i6h
Imani.Spencer21@example.net,1N0PRraQU7

要访问这个文件中的数据,你需要在测试脚本中通过config.payload.path 属性引用它。其次,你需要通过config.payload.fields ,指定你想访问的字段的名称。config.payload 属性提供了其他几个选项来配置其行为,而且还可以在一个脚本中指定多个有效载荷文件。

config:
  target: "http://localhost:4000"
  phases:
    - duration: 60
      arrivalRate: 20
  payload:
    path: "./auth.csv"
    fields:
      - "email"
      - "password"

scenarios:
  - name: "Authenticating users"
    flow:
      - post:
          url: "/login"
          json:
            email: "{{ email }}"
            password: "{{ password }}"

从一个端点捕获响应数据

Artillery很容易捕获一个请求的响应,并在随后的请求中重复使用某些字段。如果你在模拟流程中的请求依赖于先前的行动的执行,这就很有帮助。

假设你提供一个地理编码API,接受一个地方的名称,并以下列格式返回其经度和纬度。

{
  "longitude": -73.935242,
  "latitude": 40.730610
}

你可以用一个CSV文件来填充一个城市列表。

Seattle
London
Paris
Monaco
Milan

以下是你如何配置Artillery以在另一个请求中使用每个城市的经度和纬度值。例如,你可以使用这些值来通过另一个端点检索当前的天气。

config:
  target: "http://localhost:4000"
  phases:
    - duration: 60
      arrivalRate: 20
  payload:
    path: "./cities.csv"
    fields:
      - "city"

scenarios:
  - flow:
      - get:
          url: "/geocode?city={{ city }}"
          capture:
            - json: "$.longitude"
              as: "lon"
            - json: "$.latitude"
              as: "lat"
      - get:
          url: "/weather?lon={{ lon }}&lat={{ lat }}"

上面的capture 属性是所有魔法发生的地方。在这里你可以访问一个请求的JSON响应,并将其存储在一个变量中,以便在随后的请求中重复使用。来自/geocode 响应体的longitudelatitude 属性(分别有别名lonlat )然后作为查询参数传递给/weather 端点。

在CI/CD环境中使用Artillery

运行你的负载测试脚本的一个明显的地方是在CI/CD管道中,这样你的应用程序在部署到生产之前就可以通过它的考验。

在这种环境中使用Artillery时,有必要设置失败条件,使程序以非零代码退出。如果性能目标没有得到满足,你的部署就会中止。Artillery通过其config.ensure 属性为这种用例提供了支持。

这里有一个例子,它使用ensure 设置来断言99%的所有请求的总响应时间为150毫秒或更少,并且允许1%或更少的请求失败。

config:
  target: "https://example.com"
  phases:
    - duration: 60
      arrivalRate: 20
  ensure:
    p99: 150
    maxErrorRate: 1

一旦你运行了这个测试,它就会像以前一样继续进行,除了在测试结束时验证断言,如果不满足要求,会导致程序以非零退出代码退出。测试失败的原因会打印在总结报告的底部。

All virtual users finished
Summary report @ 07:45:48(+0100) 2021-09-03
  Scenarios launched:  10
  Scenarios completed: 10
  Requests completed:  20
  Mean response/sec: 4
  Response time (msec):
    min: 1
    max: 487
    median: 2
    p95: 443.5
    p99: 487
  Scenario counts:
    0: 10 (100%)
  Codes:
    200: 20

ensure condition failed: ensure.p99 < 200

除了检查总的延迟,你还可以在min,max, 和median 上运行断言--分别是最小、最大和中间的响应时间。下面是如何断言在测试运行期间请求的完成时间不超过500毫秒。

config:
  ensure:
    max: 500

测试失败的报告将指出失败的原因。

All virtual users finished
Summary report @ 08:29:59(+0100) 2021-09-03
  Scenarios launched:  10
  Scenarios completed: 10
  Requests completed:  20
  Mean response/sec: 3.64
  Response time (msec):
    min: 1
    max: 603
    median: 305.5
    p95: 602.5
    p99: 603
  Scenario counts:
    0: 10 (100%)
  Codes:
    200: 20

ensure condition failed: ensure.max < 500

在Artillery中生成状态报告

Artillery为每个测试运行打印一份摘要报告到标准输出,但也可以通过利用--output 标志将测试运行的详细统计数据输出到JSON文件。

$ artillery run config.yml --output test.json

一旦测试完成,它的报告会被放在当前工作目录下的一个test.json 文件中。这个JSON文件可以通过[Artillery的在线报告查看器]进行可视化,或者通过report 子命令转换为HTML报告。

$ artillery report --output report.html test.json
Report generated: report.html

你可以在浏览器中打开report.html 文件,查看测试运行的完整报告。它包括表格和一些图表,应该可以让你很好地了解你的应用程序在负载下的表现。

Artillery HTML report

用插件扩展Artillery

Artillery用于测试HTTP、Socket.io和Websocket APIs的内置工具可以让你在负载测试过程中走得更远。

为Node.js应用程序使用Artillery以避免停机

在这篇文章中,我们描述了如何用Artillery为你的Node.js应用程序设置一个负载测试工作流程。这种设置将确保你的应用程序性能在各种流量条件下保持可预测性。你将能够很好地考虑到流量大的时期,即使面对突然涌入的用户,也能避免停机。