运行大型测试
k6 可以从一台机器生成大量负载。通过适当的监控和脚本优化,您可能无需分布式执行即可运行相当大的负载测试。本文档解释了如何启动此类测试,以及您应该注意的一些方面。
最大限度地提高机器生成的负载是一个多方面的过程,其中包括
- 更改操作系统设置以提高默认网络和用户限制。
- 监控负载生成器机器,确保资源使用充足。
- 设计高效的测试,关注脚本编写、k6 选项和文件上传。
- 监控测试运行以检测 k6 记录的错误,这可能表明负载生成器机器或被测系统 (SUT) 的限制。
单个 k6 进程可以有效利用负载生成器机器上的所有 CPU 核。根据可用资源以及本文档中描述的指南,单个 k6 实例可以运行 30,000-40,000 个并发用户 (VU)。在某些情况下,这些 VU 可以产生高达 300,000 个 HTTP 每秒请求数 (RPS)。
除非您需要每秒超过 100,000-300,000 个请求(每分钟 6-1200 万个请求),否则单个 k6 实例可能足以满足您的需求。继续阅读以了解如何从单台机器获取最大负载。
操作系统精细调整
现代操作系统配置的应用程序可以创建的并发网络连接数量限制相当低。这是一个安全的默认设置,因为大多数程序不像 k6 那样需要打开数千个并发 TCP 连接。但是,如果我们想充分利用网络容量并达到最大性能,我们需要更改一些默认设置。
在 GNU/Linux 机器上,以 root
用户身份运行以下命令
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.tcp_timestamps=1
ulimit -n 250000
这些命令启用网络连接复用,增加网络连接限制和本地端口范围。
sysctl
命令立即应用于整个系统,如果您重启网络服务或机器,它们将重置为默认值。ulimit
命令仅适用于当前 shell 会话,您需要在另一个 shell 实例中再次运行它才能生效。
有关这些设置的详细信息、如何使它们永久生效以及 macOS 的说明,请查看我们的“精细调整操作系统”文章。
硬件注意事项
随着负载增加,您需要关注硬件限制。
网络
机器的网络吞吐量是运行大型测试时一个重要的考虑因素。许多 AWS EC2 机器连接速度为 1Gbit/s,这可能会限制 k6 能生成的负载。
运行测试时,可以使用像 iftop
这样的工具在终端中实时查看生成的网络流量量。如果流量恒定在 1Gbit/s,您的测试可能受到网卡的限制。考虑升级到不同的 EC2 实例。
CPU
k6 是高度多线程的,将有效利用所有可用的 CPU 核。
您需要的 CPU 数量取决于您的测试脚本和相关文件。无论测试文件如何,您可以假设大型测试需要大量的 CPU 能力。我们建议您配置机器,使其至少有 20% 的空闲周期(k6 使用最多 80%,空闲 20%)。如果 k6 使用 100% 的 CPU 生成负载,测试将遇到限制,这可能导致结果指标中的响应时间远大于实际情况。
内存
k6 可以使用大量内存,尽管比其他一些负载测试工具更高效。内存消耗很大程度上取决于您的测试场景。要估计您的测试所需的内存,请在您的开发机器上使用 100VU 运行测试,并将消耗的内存乘以目标 VU 数量。
简单测试每个 VU 使用约 1-5MB。(1000VU = 1-5GB)。使用文件上传或加载大型 JS 模块的测试每个 VU 可能消耗数十兆字节。请注意,每个 VU 都有您的测试使用的所有 JS 模块的副本。为了在 VU 之间共享内存,考虑使用SharedArray,或外部数据存储,例如Redis。
如果您正在使用交换空间,考虑禁用它。如果系统物理内存耗尽,将内存交换到慢得多的二级存储的过程将对性能和系统稳定性产生不稳定的影响。这很可能会使任何测试结果失效,因为负载生成器在测试的不同部分具有不同的性能。相反,请提前规划您的测试预期达到的内存使用量,并确保您有足够的物理 RAM,使使用量不超过 90%。
监控负载生成器
上一节描述了硬件如何限制您的机器可以生成的负载。记住这一点,您应该在测试运行时监控资源,尤其是在第一次运行测试时。
监控负载生成器最简单的方法是在机器上打开多个终端:一个用于运行 k6,其他用于监控 CPU、内存和网络。我们推荐以下工具
如果您喜欢图形化应用程序,可以使用操作系统的系统监控工具(例如 GNOME 系统监视器或 Plasma 系统监视器),或者像SysMonTask这样的独立工具。
这是显示 k6、iftop 和 htop 的 3 个终端会话的截图。
监控指南
测试运行时,这些是很好的监控指标
CPU 利用率保持在 80% 以内。 如果在测试运行期间所有 CPU 核都达到 100% 利用率,您可能会注意到测试结果中的性能下降。
网络利用率处于可接受的水平。 根据您的测试,您可能期望完全饱和网络带宽,或者这可能是您的测试受可用带宽限制的信号。在其他场景中,您可能希望最小化网络使用以降低成本。
内存利用率不超过 90%。 如果您接近耗尽可用的物理 RAM,系统可能会开始将内存交换到磁盘,这将影响性能和系统稳定性(请注意,我们建议完全禁用交换空间)。在极端情况下,Linux 上内存耗尽会导致系统终止 k6 进程。
错误处理应具有弹性
运行时,您的脚本不应该对 HTTP 响应做任何假设。有些脚本的疏忽之处在于只按照大型压力测试进行测试。
例如,在 k6 脚本中,我们经常看到这样的正常流程检查
import { check } from 'k6';
import http from 'k6/http';
const res = http.get('https://test.k6.io');
const checkRes = check(res, {
'Homepage body size is 11026 bytes': (r) => r.body.length === 11026,
});
像这样的代码在 SUT 未过载并返回正确响应时运行良好。当系统开始出现故障时,此检查将无法按预期工作。
当 SUT 未过载且返回正常响应时,这样的代码运行良好。当系统开始出现故障时,此检查将无法按预期工作。
ERRO[0625] TypeError: Cannot read property 'length' of undefined
问题在于此检查假设响应始终包含主体。但如果服务器出现故障,r.body
可能不存在。在这种情况下,检查本身将无法按预期工作,并将返回以下错误
以下更改将处理此异常。
import { check } from 'k6';
import http from 'k6/http';
const res = http.get('https://test.k6.io');
const checkRes = check(res, {
'Homepage body size is 11026 bytes': (r) => r.body && r.body.length === 11026,
});
JavaScript 复制
k6 选项以减少资源使用
请参阅后续章节,获取常见错误的列表。
k6 选项以减少资源使用
以下 k6 设置可以降低运行大型测试的性能成本。
使用 discardResponseBodies
节省内存
export const options = {
discardResponseBodies: true,
};
默认情况下,k6 将请求的响应主体加载到内存中。这会导致更高的内存消耗,并且通常是不必要的。
要指示 k6 不处理所有响应主体,请将 discardResponseBodies
设置到 options 对象中,如下所示
如果您正在运行本地测试并将结果流式传输到云端(k6 cloud run script.js --local-execution
),您可能希望禁用终端摘要和本地阈值计算,因为云服务会显示摘要并计算阈值。
如果您需要某些请求的响应主体,请使用Params.responseType覆盖该选项。
流式传输时,使用 --no-thresholds
和 --no-summary
k6 cloud run scripts/website.js \
--local-execution \
--vus=20000 \
--duration=10m \
--no-thresholds \
--no-summary
如果您正在运行本地测试并将结果流式传输到云端(k6 cloud run script.js --local-execution
),您可能希望禁用终端摘要和本地阈值计算,因为云服务将显示摘要并计算阈值。
如果没有这些选项,本地机器和云服务器都会重复这些操作。这将节省一些内存和 CPU 周期。
这里是所有提到的标志,全部放在一起
k6 API 的某些功能需要更多计算才能执行。为了最大化负载生成,您可能需要限制脚本对以下功能的使用。
- 脚本优化
- 为了从硬件中榨取更多性能,考虑优化测试脚本本身的代码。其中一些优化涉及限制您使用某些 k6 功能的方式。其他优化则是所有 JavaScript 编程通用的。
- 限制资源密集型 k6 操作
- k6 API 的某些功能需要更多的计算才能执行。为了最大化负载生成,您可能需要限制您的脚本如何使用以下功能。
- 检查和组
- k6 分别记录每个检查和组的结果。如果您使用了很多检查和组,可以考虑删除它们以提升性能。
- 自定义指标
- 与检查类似,自定义指标(Trend、Counter、Gauge 和 Rate)的值也是单独记录的。考虑最小化自定义指标的使用。
- 带有 abortOnFail 的阈值
如果您配置了abortOnFail 阈值,k6 需要不断评估结果以验证是否未超过阈值。考虑移除此设置。
URL 分组
- k6 v0.41.0 引入了一项更改以支持时序指标。这带来的一个副作用是,每个唯一的 URL 都会创建一个新的时序对象,这可能消耗比预期更多的 RAM。
- 为了解决这个问题,请使用URL 分组功能。
- 尽量减少外部 JS 依赖项。
- JavaScript 优化
最后,如果前面的建议不足,您可能可以进行一些通用的 JavaScript 优化
避免深度嵌套的 for
循环。
尽可能避免引用内存中的大型对象。
- 将外部 JS 依赖降至最低。
- 如果您有构建流水线,请对 k6 脚本执行 Tree Shaking 等操作。
- 内存
- 请参阅这篇关于垃圾回收的文章,了解 V8 运行时中的。虽然 k6 使用的 JavaScript VM 与之非常不同且运行在 Go 上,但通用原则适用。请注意,k6 脚本中仍然可能存在内存泄漏,如果不修复,它们可能会更快地耗尽 RAM。
- 文件上传注意事项
- 文件上传会影响资源使用和云成本。
- 网络吞吐量
- 负载生成器机器和 SUT 的网络吞吐量可能是文件上传的瓶颈。
- k6 在上传文件时需要大量内存。每个 VU 都是独立的,有自己的内存,文件将被复制到所有上传文件的 VU 中。您可以关注议题 #1931,该议题旨在改进此情况。
- 数据传输成本
- k6 可以在很短的时间内上传大量数据。在大规模测试开始之前,请确保您了解数据传输成本。
出站数据传输在 AWS EC2 中很昂贵。价格根据区域不同在每 GB 0.08 美元到 0.20 美元之间。如果您使用最便宜的区域,成本约为每 GB 0.08 美元。因此,上传 1TB 大约需要 80 美元。一个长时间运行的测试仅数据传输费用就可能花费数百美元。
如果您的基础设施已托管在 AWS 上,请考虑在同一 AWS 区域和可用区内运行您的负载生成器机器。在某些情况下,此流量将便宜得多甚至免费。有关额外的数据成本节省技巧,请查看这篇关于如何降低 AWS 数据传输成本的文章。我们的示例考虑了 AWS。然而,同样的建议也适用于其他云提供商。
虚拟服务器成本
AWS EC2 实例相对便宜。即使我们在本次基准测试中使用的最大实例 (m5.24xlarge) 每小时也仅花费 4.6 美元。
WARN[0013] Request Failed error="Get http://test.k6.io: read tcp 172.31.72.209:35288->63.32.205.136:80: read: connection reset by peer"
测试完成后,请务必关闭负载生成器服务器。一个被遗忘的 EC2 服务器每月可能花费数千美元。提示: 通常可以以成本的 10-20% 启动相同硬件的“竞价实例”。
常见错误
WARN[0064] Request Failed error="Get http://test.k6.io: context deadline exceeded"
如果您在执行期间遇到错误,了解它们是由于负载生成器还是由于发生故障的 SUT 引起的会很有帮助。
read: connection reset by peer
WARN[0057] Request Failed error="Get http://test.k6.io/: dial tcp 52.18.24.222:80: i/o timeout"
这是由目标系统重置 TCP 连接引起的。当负载均衡器或服务器本身无法处理流量时会发生这种情况。
context deadline exceeded
WARN[0034] Request Failed error="Get http://test.k6.io/: dial tcp 99.81.83.131:80: socket: too many open files"
这发生在 k6 可以发送请求,但目标系统未能及时响应时。k6 的默认超时时间为 60 秒。如果您的系统未在此时间范围内产生响应,则会出现此错误。
dial tcp 52.18.24.222:80: i/o timeout
此错误类似于 context deadline exceeded
,但在本例中,k6 甚至无法发出 HTTP 请求。目标系统无法建立 TCP 连接。
socket: too many open files
此错误意味着负载生成器无法打开 TCP socket,因为它已达到打开文件描述符的限制。请确保您的限制设置得足够高。请参阅“精细调整操作系统”文章。
注意
确定可接受的错误级别。在大规模测试中,总会存在一些错误。如果您发出 5000 万个请求,其中有 100 个失败,这通常是一个好的结果(0.00002% 错误率)。
k6 基准测试
我们在不同的 EC2 机器上执行了一些大型测试,以了解 k6 能生成多少负载。我们的普遍观察是,k6 与硬件成比例地扩展。资源多 2 倍的机器可以生成大约 2 倍的流量。此可扩展性的限制是开放连接的数量。单个 Linux 机器每个 IP 地址最多可以打开 65,535 个 socket(请参阅RFC 6056了解详细信息)。这意味着单台机器上最多可以同时发送 65k 个请求。这是一个理论限制,将取决于配置的临时端口范围、是否使用 HTTP/2(HTTP/2 使用请求多路复用,可以实现更高的吞吐量)以及其他因素。
- 不过,主要来说,RPS 限制取决于 SUT 的响应时间。如果响应在 100 毫秒内交付,则对单个远程地址的 HTTP/1 请求的理论 RPS 限制为 650,000。
- 我们维护一个代码库,其中包含一些用于对 k6 进行基准测试和创建报告的脚本。这些测试会针对每个新的 k6 版本运行,您可以在
results/
目录中查看结果。
分布式执行
## split the load of my-script.js across two machines
k6 run --execution-segment "0:1/2" --execution-segment-sequence "0,1/2,1" my-script.js
k6 run --execution-segment "1/2:1" --execution-segment-sequence "0,1/2,1" my-script.js
## split the load of my-script.js across three machines
k6 run --execution-segment "0:1/3" --execution-segment-sequence "0,1/3,2/3,1" my-script.js
k6 run --execution-segment "1/3:2/3" --execution-segment-sequence "0,1/3,2/3,1" my-script.js
k6 run --execution-segment "2/3:1" --execution-segment-sequence "0,1/3,2/3,1" my-script.js
## split the load of my-script.js across four machines
k6 run --execution-segment "0:1/4" --execution-segment-sequence "0,1/4,2/4,3/4,1" my-script.js
k6 run --execution-segment "1/4:2/4" --execution-segment-sequence "0,1/4,2/4,3/4,1" my-script.js
k6 run --execution-segment "2/4:3/4" --execution-segment-sequence "0,1/4,2/4,3/4,1" my-script.js
k6 run --execution-segment "3/4:1" --execution-segment-sequence "0,1/4,2/4,3/4,1" my-script.js
用户经常寻找分布式执行模式来运行大规模测试。虽然单个 k6 实例可以生成巨大的负载,但分布式执行对于以下情况是必需的
- 同时模拟来自多个位置的负载。
- 将您的测试负载扩展到超出单个机器的处理能力。
- 在 k6 中,您可以使用
execution-segment
选项将测试负载分散到多个 k6 实例。例如
bash
k6 的目标是支持一个原生的开源分布式执行解决方案。如果您想关注进展,请订阅 GitHub 上的 分布式执行问题。
但是,目前 k6 的分布式执行模式尚未完全实现功能。当前的限制包括
k6 不提供“主”实例的功能来协调测试的分布式执行。或者,您可以使用k6 REST API和--paused
来同步多个 k6 实例的执行。
每个 k6 实例独立评估阈值 - 不包括其他 k6 实例的结果。如果您想禁用阈值执行,请使用--no-thresholds
。
k6 为每个实例单独报告指标。根据您存储负载测试结果的方式,您需要聚合一些指标才能正确计算它们。
考虑到上述限制,我们构建了一个Kubernetes Operator,用于将 k6 测试的负载分布到Kubernetes 集群中。如需进一步说明,请查看在 Kubernetes 上运行分布式 k6 测试的教程。