k6压测
https://age9przup0.feishu.cn/docx/RG8GdkcDSow6kKx97Mnc35qonvj
官网
主页:https://k6.io/
文档:https://k6.io/docs/
博客:https://k6.io/blog/
安装
Windows
有以下几种安装方式
- winget install k6
- choco install k6
- 官方安装程序
安装完成后可在命令窗口输入:k6,检测是否安装成功
[图片]
MacOS
brew install k6
Linux和Docker
可参考官方安装文档
运行
复制下面的代码,另存为script.js
import http from 'k6/http';
import { sleep } from 'k6';
export default function () {
http.get('https://test.k6.io');
sleep(1);
}
当前目录下命令行运行:k6 run script.js
编写脚本
生命周期
// 1. init code
export function setup() {
// 2. setup code
}
export default function (data) {
// 3. VU code
}
export function teardown(data) {
// 4. teardown code
}
调用顺序:init -- setup -- vu code -- teardown
测试阶段
用途
例子
调用次数
是否必须
init
加载本地文件,导入模块,声明全局变量
打开 JSON 文件,导入模块
每个 VU 一次*
必须
Setup
设置要处理的数据,在 VU 之间共享数据
调用 API 启动测试环境
每个脚本一次
自选
VU code
运行测试函数
发出请求,验证响应
每次迭代一次,根据测试选项的要求多次
必填
Teardown
设置代码的处理结果,停止测试环境
验证设置是否具有特定结果,发送 Webhook 通知测试已完成
每个脚本一次
自选
*在云脚本中,init 代码可能会被更频繁地调用。
import { sleep } from "k6";
export let options = {
vus: 1,
duration: "1s",
};
export function setup() {
console.log("setup");
return { data: "test" };
}
export default function (data) {
console.log(data);
console.log("default");
sleep(0.25);
}
export function teardown(data) {
console.log("teardown");
console.log(data);
}
[图片]
更多信息可参考https://k6.io/docs/using-k6/test-life-cycle/
HTTP 请求
需要先导入import http from "k6/http";
Get 请求
get( url, [params] )
import http from "k6/http";
export default function () {
const params = {
headers: { "Content-Type": "application/json" },
};
const res = http.get("https://test.k6.io", params);
}
Post 请求
post( url, [body], [params] )
import http from "k6/http";
export default function () {
const url = "http://test.k6.io/login";
const payload = JSON.stringify({
email: "aaa",
password: "bbb",
});
const params = {
headers: {
"Content-Type": "application/json",
},
};
http.post(url, payload, params);
}
Put 请求
put( url, [body], [params] )
import http from 'k6/http';
const url = 'https://httpbin.test.k6.io/put';
export default function () {
const headers = { 'Content-Type': 'application/json' };
const data = { name: 'Bert' };
const res = http.put(url, JSON.stringify(data), { headers: headers });
console.log(JSON.parse(res.body).json.name);
}
Batch 批处理
batch( requests )
有两种使用方式,批量请求可能按任何顺序执行,也可能同时执行
import http from 'k6/http';
import { check } from 'k6';
export default function () {
const responses = http.batch([
['GET', 'https://test.k6.io', null, { tags: { ctype: 'html' } }],
['GET', 'https://test.k6.io/style.css', null, { tags: { ctype: 'css' } }],
['GET', 'https://test.k6.io/images/logo.png', null, { tags: { ctype: 'images' } }],
]);
check(responses[0], {
'main page status was 200': (res) => res.status === 200,
});
}
import http from 'k6/http';
import { check } from 'k6';
export default function () {
const req1 = {
method: 'GET',
url: 'https://httpbin.test.k6.io/get',
};
const req2 = {
method: 'GET',
url: 'https://test.k6.io',
};
const req3 = {
method: 'POST',
url: 'https://httpbin.test.k6.io/post',
body: {
hello: 'world!',
},
params: {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
},
};
const responses = http.batch([req1, req2, req3]);
check(responses[2], {
'form data OK': (res) => JSON.parse(res.body)['form']['hello'] == 'world!',
});
}
Response 对象属性
属性
类型
Response.body
HTTP响应正文
Response.cookies
响应cookies,属性是cookie名称,值是cookie对象数组
Response.error
发送请求失败后的错误信息
Response.error_code
错误码
Response.headers
标头,键值对
Response.status
从服务器收到的HTTP响应代码
Response.timings
耗时(以毫秒为单位)
Response.timings.blocked
= http_req_blocked
Response.timings.connecting
= http_req_connecting
Response.timings.tls_handshaking
= http_req_tls_handshaking
Response.timings.sending
= http_req_sending
Response.timings.waiting
= http_req_waiting
Response.timings.receiving
= http_req_receiving
Response.timings.duration
= http_req_duration
Checks 检查
Checks 类似断言,不同在于 Checks 不会停止当前的脚本。指标都可以作为检查的项目。
需要导入import { check } from 'k6';
import http from 'k6/http';
import { check } from 'k6';
export let options = {
vus: 100,
duration: '10s',
};
export default function () {
let res = http.get('http://test.k6.io/');
check(res, {
'状态码为200': (r) => r.status === 200,
'响应时间小于200ms': (r) => r.timings.duration < 200,
'等待远程主机响应时间小于200ms': (r) => r.timings.waiting < 200,
});
}
[图片]
官方建议:
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,
});
如果服务器出现故障,则 r.body 可能不存在,这种情况下,check无法按预期工作,并且将返回类似于以下的错误ERRO[0625] TypeError: Cannot read property 'length' of undefined
可更改为:
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,
});
Fail 错误
可以自定义失败信息,立即引发错误,中止当前脚本迭代
需要导入import { fail } from "k6";
import http from "k6/http";
import { check, fail } from "k6";
export default function () {
const res = http.get("http://httpbin.org");
const checkOutput = check(
res,
{
"response code was 200": (res) => res.status == 200,
"body size was 1234 bytes": (res) => res.body.length == 1234,
},
{ myTag: "I'm a tag" }
);
if (!checkOutput) {
fail("unexpected response");
}
}
[图片]
Options 配置
Vus 虚拟用户数
指定要并发运行的虚拟用户数量,必须是一个整数,和 duration 搭配使用。默认值:1
export let options = {
vus: 10,
duration: '10s',
};
Duration 运行时间
一个字符串,指定测试运行的总持续时间,与 vus 选项一起使用。默认值:null
export let options = {
vus: 10,
duration: '10s',
};
与在命令行直接输入下面命令效果相同
k6 run -u 10 --d 20s test.js
k6 run --vus 10 --duration 20s test.js
Stage 阶段
可在运行过程中设置用户数的增长和降低,通过配置 options.stages 属性实现用户数的变化
//在前3分钟内虚拟用户将从1 VU上升到10 VU,
//然后在10 VU的水平下保持5分钟
//然后在10分钟内逐渐从10 VU上升到35 VU
//最后3分钟内慢慢降至0 VU
export let options = {
stages: [
{ duration: '3m', target: 10 },
{ duration: '5m', target: 10 },
{ duration: '10m', target: 35 },
{ duration: '3m', target: 0 },
],
};
RPS 每秒最大请求数
每秒发出的最大请求数。 默认值:0
export let options = {
rps: 500,
};
与在命令行直接输入下面命令效果相同
k6 run --rps 500 test.js
Throw 抛出异常
一个布尔值,true or false ,指定是否在失败的 HTTP 请求上抛出异常。 默认值:false
export let options = {
throw: true,
};
与在命令行直接输入下面命令效果相同
k6 run --throw test.js
k6 run -w test.js
Thresholds 阈值
一组阈值规范,用于根据指标数据配置在何种条件下测试成功与否,测试通过或失败。默认值:null
名称
描述
Counter
计数器,对值进行累加
Gauge
最小值、最大值和最后一个值
Rate
百分比
Trend
最小值、最大值、平均值和百分位数的统计数据指标
import http from "k6/http";
import { Trend, Rate, Counter, Gauge } from "k6/metrics";
export let GaugeContentSize = new Gauge("ContentSize");
export let TrendRTT = new Trend("RTT");
export let options = {
vus: 10,
duration: "10s",
thresholds: {
// 发出的请求数量需要大于1000
http_reqs: ["count>1000"],
// 错误率应该效率 0.01%
http_req_failed: ["rate<0.01"],
// 返回的内容必须小于 4000 字节。
ContentSize: ["value<4000"],
// p(N) 其中 N 是一个介于 0.0 和 100.0 之间的数字,表示要查看的百分位值,例如p(99.99) 表示第 99.99 个百分位数。这些值的单位是毫秒。
// 90% 的请求必须在 400 毫秒内完成,95% 必须在 800 毫秒内完成,99.9% 必须在 2 秒内完成
http_req_duration: ["p(90) < 400", "p(95) < 800", "p(99.9) < 2000"],
// 99% 响应时间必须低于 300 毫秒,70% 响应时间必须低于 250 毫秒,
// 平均响应时间必须低于 200 毫秒,中位响应时间必须低于 150 毫秒,最小响应时间必须低于 100 毫秒
RTT: ["p(99)<300", "p(70)<250", "avg<200", "med<150", "min<100"],
},
};
export default function () {
let res = http.get("http://www.baidu.com");
TrendRTT.add(res.timings.duration);
GaugeContentSize.add(res.body.length);
}
未通过的阈值会在指标左侧显示✗
[图片]
阈值标签,测试中可以给指定的 url 或者特定标签上使用阈值。
import http from "k6/http";
export let options = {
vus: 10,
duration: "10s",
thresholds: {
// type 为 qq 使用
"http_req_duration{type:qq}": ["p(95)<500"],
// type 为 bing 使用
"http_req_duration{type:bing}": ["p(95)<200"],
},
};
export default function () {
let res1 = http.get("https://www.qq.com", {
tags: { type: "qq" },
});
let res2 = http.get("https://cn.bing.com", {
tags: { type: "bing" },
});
}
[图片]
Tags 标签
指定应在所有指标中设置为测试范围的标签。如果在请求、检查或自定义指标上指定了同名标签,它将优先于测试范围的标签。 默认值:null
export let options = {
tags: {
name: 'value',
},
};
与在命令行直接输入下面命令效果相同
k6 run --tag NAME=VALUE test.js
更多情况可参考 https://k6.io/docs/using-k6/k6-options/reference/
Group 组
在组内运行代码,测试的结果将按组分别显示group( name, fn )
需要导入import { group } from 'k6';
import { group } from 'k6';
export default function () {
group('visit product listing page', function () {
// ...
});
group('add several products to the shopping cart', function () {
// ...
});
group('visit login page', function () {
// ...
});
group('authenticate', function () {
// ...
});
group('checkout process', function () {
// ...
});
}
随机种子
设置种子以获得可重现的伪随机数randomSeed( int )
下面这个例子中,所有迭代中将获得相同的随机数
import { randomSeed } from 'k6';
export const options = {
vus: 10,
duration: '5s',
};
export default function () {
randomSeed(123456789);
const rnd = Math.random();
console.log(rnd);
}
执行脚本
日志输出
输出到控制台
import http from "k6/http";
export let options = {
vus: 10,
duration: "2s",
};
export default function () {
console.log("log");
console.info("info");
console.error("err");
console.debug("debug");
console.warn("warn");
}
输出到文件,输出到文件的同时控制台不在输出
k6 run test.js --console-output=test.log
报告输出
插件
使用方法
Amazon CloudWatch
k6 run --out statsd
Apache Kafka
k6 run --out kafka
Cloud
k6 run --out cloud
CSV
k6 run --out csv
Datadog
k6 run --out datadog
InfluxDB
k6 run --out influxdb
JSON
k6 run --out json
New Relic
k6 run --out statsd
StatsD
k6 run --out statsd
控制台报告
指定要统计的数据
定义要计算和显示的trend度量统计信息--summary-trend-stats
export const options = {
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(95)', 'p(99)', 'p(99.99)', 'count'],
};
k6 run --summary-trend-stats="avg,min,med,max,p(90),p(99.9),p(99.99),count" test.js
[图片]
指定单位
强制k6对报告中的所有时间值使用固定的时间单位--summary-time-unit
export const options = {
summaryTimeUnit: 'ms',
};
k6 run --summary-time-unit=ms test.js
不输出聚合报告
完全禁用报告生成--no-summary
k6 run --no-summary test.js
更多参数
可以通过k6 run --help 查询
其他操作
在k6执行测试任务时,可以再次打开一个新的命令窗口来控制测试的暂停测试等操作
- k6 pause -暂停测试执行,暂停所有VU
- k6 resume -取消暂停k6,导致所有活动的VU恢复执行
- k6 scale -更改活动VU的数量(或允许的最大VU数量)
- k6 stats -报告当前已收集的统计信息
- k6 status -报告测试的一般状态
结果分析
指标说明
指标类型
名称
描述
Counter
计数器,对值进行累加
Gauge
最小值、最大值和最后一个值
Rate
百分比
Trend
最小值、最大值、平均值和百分位数的统计数据指标
常见指标
名称
类型
作用
checks
Rate
checks项的成功率
data_received
Counter
收到的数据量和速率
data_snet
Counter
发送的数据量和速率
http_req_blocked
Trend
在启动请求之前,被阻塞(等待空闲的 TCP 连接槽位)所花费的时间
http_req_connecting
Trend
建立与远程主机的 TCP 连接所花费的时间
http_req_duration
Trend
请求的总时间。它等于http_req_sending + http_req_waiting + http_req_receiving(即,在没有初始 DNS 查找/连接时间的情况下,远程服务器处理请求和响应所需的时间)
http_req_failed
Rate
失败的请求比例和数量
http_req_receiving
Trend
从远程主机接收响应数据所花费的时间
http_req_sending
Trend
将数据发送到远程主机所花费的时间
http_req_tls_handshaking
Trend
与远程主机握手 TLS 会话所花费的时间
http_req_waiting
Trend
等待来自远程主机的响应所花费的时间(也称为“第一个字节的时间”或“TTFB”)
http_reqs
Counter
生成的 HTTP 请求总数和速率
iteration_duration
Trend
完成一次完整迭代所花费的时间,包含 setup 和 teardown.要计算特定场景的迭代函数的持续时间
iterations
Counter
VU 执行 JS 脚本的总次数(违约函数)
vus
Gauge
当前活动虚拟用户数
vus_max
Gauge
最大可能数量的虚拟用户(VU 资源是预先分配的,确保在纵向扩展负载级别时不会影响性能)
对于如上的指标,比较重要的是 http_req_duration(Time)、http_reqs(TPS)。
自定义指标
需要导入import { Trend, Rate, Counter, Gauge } from 'k6/metrics';
import http from "k6/http";
import { Trend } from "k6/metrics";
export let options = {
vus: 100,
duration: "10s",
};
// 新建一个类型为 Trend 名为 sending_time 的自定制指标
let sendingTime = new Trend("sending_time");
export default function () {
let res = http.get("http://www.baidu.com");
sendingTime.add(res.timings.sending);
}
k6 Cloud
登陆 https://app.k6.io/ 后,点击左上角查看账号信息,复制API token
[图片]
在命令行运行:k6 login cloud --token <YOUR_K6_CLOUD_API_TOKEN> 即可成功绑定云端。
然后在执行脚本时修改命令为:k6 run --out cloud test.js 测试数据将实时发送到云端,可在命令行输出的output 后看到对应云端网址,或直接在 https://app.k6.io/ 找到。
执行完成后,可点击右上角生成测试报告:
[图片]
注意:
K6的Cloud端功能很强大,但是对于免费账号有一些限制,例如一次测试并发VU不能超过50,而且只能免费测试50次。
其他报告输出方式
k6内置了许多输出方式:
[图片]
只需要在执行脚本时,添加额外的参数,如:
k6 run --out json=test.json --out influxdb=http://localhost:8086/k6 test.js
[图片]
例子
随机用户名
将下面的代码保存为randomUser.js 在其他文件中导入
import { randomUser } from "./randomUser.js";
// 第一个参数为你想生成的固定的文字开头比如: 微信用户xxxxx
// 第二个为你想生成出固定开头文字外的随机长度
export function random(prefix, randomLength) {
// 兼容更低版本的默认值写法
prefix === undefined ? prefix = "" : prefix;
randomLength === undefined ? randomLength = 8 : randomLength;
// 设置随机用户名
// 用户名随机词典数组
let nameArr = [
[1, 2, 3, 4, 5, 6, 7, 8, 9, 0],
["a", "b", "c", "d", "e", "f", "g", "h", "i", "g", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
]
// 随机名字字符串
let name = prefix;
// 循环遍历从用户词典中随机抽出一个
for (var i = 0; i < randomLength; i++) {
// 随机生成index
let index = Math.floor(Math.random() * 2);
let zm = nameArr[index][Math.floor(Math.random() * nameArr[index].length)];
// 如果随机出的是英文字母
if (index === 1) {
// 则百分之50的概率变为大写
if (Math.floor(Math.random() * 2) === 1) {
zm = zm.toUpperCase();
}
}
// 拼接进名字变量中
name += zm;
}
// 将随机生成的名字返回
return name;
}
官方用户流测试
import http from 'k6/http';
import { check, group, sleep } from 'k6';
const options = {
vus: 1000,
duration: '600s',
};
const SLEEP_DURATION = 0.1;
export default function () {
let body = JSON.stringify({
username: 'user_' + __ITER,
password: 'PASSWORD',
});
const params = {
headers: {
'Content-Type': 'application/json',
},
tags: {
name: 'login', // first request
},
};
group('simple user journey', (_) => {
// Login request
const login_response = http.post('http://api.yourplatform.com/v2/login', body, params);
check(login_response, {
'is status 200': (r) => r.status === 200,
'is api key present': (r) => r.json().hasOwnProperty('api_key'),
});
params.headers['api-key'] = login_response.json()['api_key'];
sleep(SLEEP_DURATION);
// Get user profile request
params.tags.name = 'get-user-profile';
const user_profile_response = http.get(
'http://api.yourplatform.com/v2/users/user_' + __ITER + '/profile',
params
);
sleep(SLEEP_DURATION);
// Update user profile request
body = JSON.stringify({
first_name: 'user_' + __ITER,
});
params.tags.name = 'update-user-profile';
const update_profile_response = http.post(
'http://api.yourplatform.com/v2/users/user_' + __ITER + '/profile',
body,
params
);
sleep(SLEEP_DURATION);
// Logout request
params.tags.name = 'logout';
const logout_response = http.get('http://api.yourplatform.com/v2/logout', params);
sleep(SLEEP_DURATION);
});
}
test