تست بار API با هوش مصنوعی و k6؛ آموزش Load Testing و تحلیل Performance
در این آموزش یک سیستم واقعی Load Testing با k6 میسازیم، نرخ درخواست و p95 را اندازه میگیریم و نتایج تست کارایی API را با هوش مصنوعی درواره تحلیل میکنیم.
یک API ممکن است در تستهای معمولی کاملاً درست کار کند اما هنگام دریافت ترافیک همزمان با مشکلات جدی مواجه شود. افزایش زمان پاسخ، Timeout، مصرف بیشازحد منابع، خطاهای دیتابیس، محدودیت Connection Pool و ایجاد صف درخواستها معمولاً در Unit Test مشخص نمیشوند.
برای پیداکردن این مشکلات باید رفتار API را تحت بار کنترلشده اندازهگیری کنیم.
Load Testing یا تست بار به ما کمک میکند به پرسشهای زیر پاسخ دهیم:
- API در هر ثانیه چند درخواست را پاسخ میدهد؟
- زمان پاسخ p95 و p99 چقدر است؟
- از چه نقطهای نرخ خطا افزایش پیدا میکند؟
- آیا عملکرد سیستم با افزایش تدریجی بار افت میکند؟
- آیا بعد از پایان بار، سیستم به وضعیت عادی بازمیگردد؟
- کدام Endpoint بیشترین تأخیر را دارد؟
- آیا عملیات خواندن و نوشتن رفتار یکسانی دارند؟
- چند کاربر یا درخواست همزمان قابل پشتیبانی است؟
- آیا نسخه جدید نسبت به Baseline قبلی کندتر شده است؟
هوش مصنوعی میتواند نتایج تست را خلاصه، تغییرها را مقایسه و فرضیههای تشخیصی پیشنهاد کند؛ اما اندازهگیری باید توسط ابزار واقعی Load Testing انجام شود.
در این مقاله با k6 یک پروژه عملی میسازیم و نتیجه آن را با API هوش مصنوعی درواره تحلیل میکنیم.
Load Testing چیست؟
Load Testing نوعی Performance Testing است که رفتار سیستم را زیر بار مورد انتظار بررسی میکند.
فرض کنید API شما در ساعت پرترافیک بهطور متوسط 50 درخواست در ثانیه دریافت میکند. یک Load Test مناسب میتواند بار را بهتدریج از 5 به 50 درخواست در ثانیه برساند و وضعیت سیستم را در این محدوده اندازهگیری کند.
خروجی تست ممکن است نشان دهد:
request_rate = 50 requests/second
error_rate = 0.4%
p50 = 120 ms
p95 = 480 ms
p99 = 920 ms
این اطلاعات بسیار مفیدتر از عبارت کلی «API سریع است» هستند.
تفاوت Load Test، Stress Test، Spike Test و Soak Test
Smoke Test
یک تست بسیار کوچک برای اطمینان از درستبودن Script و دردسترسبودن سیستم است.
1 VU
5 iterations
Average Load Test
بار عادی و مورد انتظار سیستم را شبیهسازی میکند.
20 requests/second
5 minutes
Stress Test
بار را از سطح عادی بالاتر میبرد تا محدودیت سیستم مشخص شود.
20 → 50 → 100 requests/second
Spike Test
افزایش ناگهانی ترافیک را شبیهسازی میکند.
5 → 150 requests/second
Soak Test
بار ثابت را برای مدت طولانی اجرا میکند تا مشکلاتی مانند افزایش تدریجی Memory، پرشدن Pool یا افت Performance مشخص شوند.
30 requests/second
2 hours
Breakpoint Test
بار را تا زمان عبور سیستم از محدوده قابلقبول افزایش میدهد.
راهنمای رسمی k6 نیز تستهای Smoke، Average Load، Stress، Spike و Soak را بهعنوان الگوهای متفاوت تست Performance معرفی میکند. راهنمای تست خودکار k6
k6 چیست؟
k6 یک ابزار Load Testing است که سناریوهای تست آن با JavaScript نوشته میشوند.
نمونه ساده:
import http from "k6/http";
import { check, sleep } from "k6";
export const options = {
vus: 5,
duration: "30s"
};
export default function () {
const response = http.get(
"https://api.example.com/health"
);
check(response, {
"status is 200": (result) =>
result.status === 200
});
sleep(1);
}
ماژول k6/http امکان ارسال درخواستهای HTTP مانند GET، POST، PUT و DELETE را فراهم میکند. مستندات رسمی k6/http
قبل از Load Test چه چیزی باید مشخص باشد؟
قبل از نوشتن Script، یک Test Plan بسازید.
حداقل اطلاعات موردنیاز:
- محیط هدف
- Base URL
- Endpointهای مورد آزمایش
- نسبت عملیات خواندن و نوشتن
- نرخ بار مورد انتظار
- مدت تست
- داده آزمایشی
- روش احراز هویت
- معیار موفقیت
- Thresholdهای Performance
- روش پاکسازی داده تست
- Baseline نسخه قبلی
- Metrics زیرساختی قابل مشاهده
Load Test را فقط روی سامانهای اجرا کنید که برای آزمایش آن مجوز و کنترل لازم را دارید. محیط هدف باید برای بار برنامهریزیشده آماده باشد.
پروژه عملی
فرض میکنیم یک API فروشگاه داریم:
GET /health
GET /api/products
GET /api/products/{id}
POST /api/orders
هدف تست:
- 90 درصد درخواستها خواندنی باشند.
- 10 درصد درخواستها سفارش آزمایشی بسازند.
- نرخ معمول 25 درخواست در ثانیه باشد.
- p95 عملیات خواندن کمتر از 500 میلیثانیه باشد.
- p95 ساخت سفارش کمتر از 900 میلیثانیه باشد.
- نرخ خطای HTTP کمتر از یک درصد باشد.
- نرخ Check موفق بیشتر از 99 درصد باشد.
ساختار پروژه:
api-load-testing/
├── test-plan.json
├── tests/
│ ├── api.js
│ └── helpers.js
├── results/
├── scripts/
│ ├── analyze_results.py
│ └── compare_results.py
├── requirements.txt
└── .env
نصب k6
در macOS:
brew install k6
در Windows با Winget:
winget install k6 --source winget
اجرای Docker:
docker pull grafana/k6
روشهای نصب بهروز در مستندات رسمی نصب k6 قرار دارند.
بررسی نصب:
k6 version
ساخت Test Plan
فایل test-plan.json:
{
"name": "catalog-api-load-test",
"environment": "staging",
"baseUrlEnvironmentVariable": "BASE_URL",
"endpoints": [
{
"name": "list_products",
"method": "GET",
"path": "/api/products",
"trafficWeight": 0.7,
"expectedStatuses": [
200
],
"thresholds": {
"p95Milliseconds": 500,
"errorRate": 0.01
}
},
{
"name": "get_product",
"method": "GET",
"path": "/api/products/{id}",
"trafficWeight": 0.2,
"expectedStatuses": [
200
],
"thresholds": {
"p95Milliseconds": 400,
"errorRate": 0.01
}
},
{
"name": "create_order",
"method": "POST",
"path": "/api/orders",
"trafficWeight": 0.1,
"expectedStatuses": [
201
],
"thresholds": {
"p95Milliseconds": 900,
"errorRate": 0.01
}
}
],
"profiles": {
"smoke": {
"virtualUsers": 1,
"iterations": 5
},
"load": {
"startRate": 5,
"peakRate": 25,
"durationMinutes": 5
},
"stress": {
"startRate": 10,
"peakRate": 100,
"durationMinutes": 8
}
},
"rules": {
"runSmokeBeforeLoad": true,
"runLoadBeforeStress": true,
"doNotTestProductionByDefault": true,
"doNotInventEndpoints": true,
"doNotInventThresholds": true
}
}
این فایل منبع حقیقت ابزار هوش مصنوعی است. مدل حق ندارد Endpoint یا Threshold جدیدی خارج از آن تعریف کند.
VU چیست؟
VU مخفف Virtual User است. هر VU منطق تعریفشده در Script را اجرا میکند.
در مدل بسته یا Closed Model، تعداد VUها مشخص است:
export const options = {
vus: 20,
duration: "5m"
};
اگر API کند شود، هر VU زمان بیشتری منتظر میماند و نرخ درخواست کاهش پیدا میکند.
Arrival Rate چیست؟
در مدل Open، هدف نرخ شروع Iterationها است:
25 iterations/second
برای APIهایی که باید نرخ مشخصی از ترافیک را تحمل کنند، Arrival Rate اغلب مدل مناسبی است.
در k6 میتوان از Executorهایی مانند موارد زیر استفاده کرد:
constant-vusramping-vusconstant-arrival-rateramping-arrival-rateshared-iterationsper-vu-iterations
مستندات Scenarioهای k6 توضیح میدهد که Scenarioها میتوانند بار را براساس VU، تعداد Iteration یا Arrival Rate مدل کنند.
رابطه تقریبی نرخ ورود و همزمانی
برای تخمین تعداد عملیات همزمان میتوان از رابطه تقریبی زیر استفاده کرد:
concurrency ≈ arrival_rate × average_iteration_time
اگر:
arrival_rate = 50 iterations/second
average_iteration_time = 0.4 seconds
آنگاه:
concurrency ≈ 50 × 0.4 = 20
این یک تخمین است. رفتار واقعی به زمان پاسخ، Think Time، تعداد درخواستهای هر Iteration و توزیع Latency بستگی دارد.
چرا Average کافی نیست؟
فرض کنید زمان پاسخها چنین باشد:
90 درخواست = 100 ms
9 درخواست = 700 ms
1 درخواست = 5000 ms
Average میتواند بخشی از کندی انتهای توزیع را پنهان کند.
Percentileها مفیدترند:
- p50: نیمی از درخواستها سریعتر از این مقدارند.
- p90: نود درصد درخواستها سریعترند.
- p95: نودوپنج درصد درخواستها سریعترند.
- p99: نودونه درصد درخواستها سریعترند.
برای تجربه کاربر، p95 و p99 اغلب اطلاعات مهمتری از Average ارائه میکنند.
ساخت Helperهای تست
فایل tests/helpers.js:
export function buildHeaders() {
const headers = {
Accept: "application/json",
"Content-Type": "application/json"
};
if (__ENV.API_TOKEN) {
headers.Authorization =
`Bearer ${__ENV.API_TOKEN}`;
}
return headers;
}
export function requireEnvironment(
variableName
) {
const value = __ENV[variableName];
if (!value) {
throw new Error(
`Missing environment variable: ${variableName}`
);
}
return value.replace(/\/+$/, "");
}
export function selectOperation() {
const randomValue = Math.random();
if (randomValue < 0.7) {
return "list_products";
}
if (randomValue < 0.9) {
return "get_product";
}
return "create_order";
}
ساخت Script کامل k6
فایل tests/api.js:
import http from "k6/http";
import {
check,
group,
sleep
} from "k6";
import {
Rate,
Trend
} from "k6/metrics";
import {
buildHeaders,
requireEnvironment,
selectOperation
} from "./helpers.js";
const BASE_URL = requireEnvironment(
"BASE_URL"
);
const PRODUCT_ID =
__ENV.PRODUCT_ID || "product-test-1";
const TEST_CUSTOMER_ID =
__ENV.TEST_CUSTOMER_ID ||
"customer-load-test";
const listProductsDuration = new Trend(
"list_products_duration",
true
);
const getProductDuration = new Trend(
"get_product_duration",
true
);
const createOrderDuration = new Trend(
"create_order_duration",
true
);
const listProductsFailed = new Rate(
"list_products_failed"
);
const getProductFailed = new Rate(
"get_product_failed"
);
const createOrderFailed = new Rate(
"create_order_failed"
);
const profiles = {
smoke: {
scenarios: {
api_smoke: {
executor: "shared-iterations",
exec: "runApiScenario",
vus: 1,
iterations: 5,
maxDuration: "1m"
}
}
},
load: {
scenarios: {
api_load: {
executor: "ramping-arrival-rate",
exec: "runApiScenario",
startRate: 5,
timeUnit: "1s",
preAllocatedVUs: 30,
maxVUs: 100,
stages: [
{
target: 10,
duration: "1m"
},
{
target: 25,
duration: "3m"
},
{
target: 0,
duration: "1m"
}
]
}
}
},
stress: {
scenarios: {
api_stress: {
executor: "ramping-arrival-rate",
exec: "runApiScenario",
startRate: 10,
timeUnit: "1s",
preAllocatedVUs: 80,
maxVUs: 250,
stages: [
{
target: 25,
duration: "1m"
},
{
target: 50,
duration: "2m"
},
{
target: 75,
duration: "2m"
},
{
target: 100,
duration: "2m"
},
{
target: 0,
duration: "1m"
}
]
}
}
}
};
const selectedProfile =
__ENV.TEST_PROFILE || "smoke";
if (!profiles[selectedProfile]) {
throw new Error(
`Unknown TEST_PROFILE: ${selectedProfile}`
);
}
export const options = {
...profiles[selectedProfile],
thresholds: {
http_req_failed: [
"rate<0.01"
],
checks: [
"rate>0.99"
],
list_products_duration: [
"p(95)<500"
],
get_product_duration: [
"p(95)<400"
],
create_order_duration: [
"p(95)<900"
],
list_products_failed: [
"rate<0.01"
],
get_product_failed: [
"rate<0.01"
],
create_order_failed: [
"rate<0.01"
]
},
summaryTrendStats: [
"avg",
"med",
"p(90)",
"p(95)",
"p(99)",
"max"
]
};
export function setup() {
const response = http.get(
`${BASE_URL}/health`,
{
headers: buildHeaders(),
tags: {
endpoint: "health"
},
timeout: "5s"
}
);
const healthy = check(response, {
"health status is 200": (result) =>
result.status === 200
});
if (!healthy) {
throw new Error(
"Target API did not pass health check"
);
}
return {
startedAt: new Date().toISOString(),
profile: selectedProfile
};
}
function listProducts() {
const response = http.get(
`${BASE_URL}/api/products?limit=20`,
{
headers: buildHeaders(),
tags: {
endpoint: "list_products"
},
timeout: "10s"
}
);
listProductsDuration.add(
response.timings.duration
);
const successful = check(response, {
"list products status is 200": (
result
) => result.status === 200,
"list products returns JSON": (
result
) => {
const contentType =
result.headers["Content-Type"] || "";
return contentType.includes(
"application/json"
);
}
});
listProductsFailed.add(!successful);
}
function getProduct() {
const response = http.get(
`${BASE_URL}/api/products/${PRODUCT_ID}`,
{
headers: buildHeaders(),
tags: {
endpoint: "get_product"
},
timeout: "10s"
}
);
getProductDuration.add(
response.timings.duration
);
const successful = check(response, {
"get product status is 200": (
result
) => result.status === 200,
"product id matches": (result) => {
if (result.status !== 200) {
return false;
}
try {
return (
result.json("id") === PRODUCT_ID
);
} catch {
return false;
}
}
});
getProductFailed.add(!successful);
}
function createOrder() {
const idempotencyKey = [
"load-test",
__VU,
__ITER,
Date.now()
].join("-");
const payload = JSON.stringify({
customerId: TEST_CUSTOMER_ID,
items: [
{
productId: PRODUCT_ID,
quantity: 1
}
],
idempotencyKey
});
const response = http.post(
`${BASE_URL}/api/orders`,
payload,
{
headers: buildHeaders(),
tags: {
endpoint: "create_order"
},
timeout: "15s"
}
);
createOrderDuration.add(
response.timings.duration
);
const successful = check(response, {
"create order status is 201": (
result
) => result.status === 201,
"create order returns id": (
result
) => {
if (result.status !== 201) {
return false;
}
try {
return Boolean(
result.json("id")
);
} catch {
return false;
}
}
});
createOrderFailed.add(!successful);
}
export function runApiScenario() {
const operation = selectOperation();
group(operation, () => {
if (operation === "list_products") {
listProducts();
return;
}
if (operation === "get_product") {
getProduct();
return;
}
createOrder();
});
sleep(0.2);
}
تفاوت Check و Threshold
Check یک Assertion روی پاسخ اجرا میکند:
check(response, {
"status is 200": (result) =>
result.status === 200
});
اما Check بهتنهایی لزوماً باعث شکست کل اجرای k6 نمیشود.
Threshold معیار Pass یا Fail تست است:
thresholds: {
checks: [
"rate>0.99"
]
}
طبق مستندات Thresholdهای k6، در صورت شکست Threshold، اجرای k6 با وضعیت ناموفق پایان مییابد. این ویژگی برای CI/CD اهمیت دارد.
چرا Custom Metric ساختهایم؟
Metric کلی زیر تمام Endpointها را با هم ترکیب میکند:
http_req_duration
ممکن است Endpointهای خواندن سریع و عملیات ساخت سفارش کند باشند. Average کلی این تفاوت را پنهان میکند.
Metricهای جدا:
list_products_duration
get_product_duration
create_order_duration
به ما اجازه میدهند برای هر عملیات Threshold متفاوت تعریف کنیم.
اجرای Smoke Test
TEST_PROFILE=smoke \
BASE_URL=https://staging.example.com \
PRODUCT_ID=product-test-1 \
TEST_CUSTOMER_ID=customer-load-test \
k6 run tests/api.js
ابتدا همیشه Smoke Test اجرا کنید. اگر Script، داده تست یا Environment Variable اشتباه باشد، اجرای Load Test بزرگ فقط خروجی نامعتبر تولید میکند.
اجرای Load Test
TEST_PROFILE=load \
BASE_URL=https://staging.example.com \
PRODUCT_ID=product-test-1 \
TEST_CUSTOMER_ID=customer-load-test \
k6 run tests/api.js
اجرای Stress Test
TEST_PROFILE=stress \
BASE_URL=https://staging.example.com \
PRODUCT_ID=product-test-1 \
TEST_CUSTOMER_ID=customer-load-test \
k6 run tests/api.js
Stress Test را فقط پس از موفقیت Smoke و Average Load اجرا کنید. همچنین بار تعریفشده باید با ظرفیت و هدف محیط آزمایشی هماهنگ باشد.
اجرای k6 با Docker
در Linux و macOS:
docker run \
--rm \
--volume "$PWD:/work" \
--workdir /work \
--env TEST_PROFILE=smoke \
--env BASE_URL=https://staging.example.com \
--env PRODUCT_ID=product-test-1 \
--env TEST_CUSTOMER_ID=customer-load-test \
grafana/k6 \
run tests/api.js
برای تکرارپذیری بهتر، نسخه مشخص Image را انتخاب کنید و قبل از استفاده با نسخه Scriptهای پروژه آزمایش کنید.
ذخیره نتیجه JSON
k6 میتواند Metrics زمانی را در JSON ذخیره کند:
k6 run \
--out json=results/metrics.json \
tests/api.js
این فایل ممکن است بزرگ شود؛ زیرا شامل دادههای زمانی اجرای تست است.
برای تحلیل خلاصه، بهتر است یک Summary کوچک تولید کنیم.
تولید Summary اختصاصی
این تابع را به انتهای tests/api.js اضافه کنید:
function metricValues(
data,
metricName
) {
const metric = data.metrics[
metricName
];
if (!metric) {
return null;
}
return metric.values || null;
}
function thresholdStatus(
data,
metricName
) {
const metric = data.metrics[
metricName
];
if (!metric || !metric.thresholds) {
return {};
}
const result = {};
for (
const [
thresholdName,
threshold
] of Object.entries(
metric.thresholds
)
) {
result[thresholdName] = {
ok: threshold.ok
};
}
return result;
}
export function handleSummary(data) {
const summary = {
schemaVersion: 1,
test: {
profile: selectedProfile,
baseUrl: BASE_URL,
durationMilliseconds:
data.state.testRunDurationMs
},
metrics: {
http_req_duration: metricValues(
data,
"http_req_duration"
),
http_req_failed: metricValues(
data,
"http_req_failed"
),
checks: metricValues(
data,
"checks"
),
iterations: metricValues(
data,
"iterations"
),
dropped_iterations: metricValues(
data,
"dropped_iterations"
),
list_products_duration:
metricValues(
data,
"list_products_duration"
),
get_product_duration:
metricValues(
data,
"get_product_duration"
),
create_order_duration:
metricValues(
data,
"create_order_duration"
),
list_products_failed:
metricValues(
data,
"list_products_failed"
),
get_product_failed:
metricValues(
data,
"get_product_failed"
),
create_order_failed:
metricValues(
data,
"create_order_failed"
)
},
thresholds: {
http_req_failed: thresholdStatus(
data,
"http_req_failed"
),
checks: thresholdStatus(
data,
"checks"
),
list_products_duration:
thresholdStatus(
data,
"list_products_duration"
),
get_product_duration:
thresholdStatus(
data,
"get_product_duration"
),
create_order_duration:
thresholdStatus(
data,
"create_order_duration"
)
}
};
return {
"results/summary.json":
JSON.stringify(
summary,
null,
2
)
};
}
طبق مستندات Custom Summary در k6، تابع handleSummary در پایان تست به دادههای تجمیعشده دسترسی دارد و میتواند خروجی JSON، متن یا فرمتهای دیگر تولید کند.
نمونه Summary
{
"schemaVersion": 1,
"test": {
"profile": "load",
"baseUrl": "https://staging.example.com",
"durationMilliseconds": 300214
},
"metrics": {
"http_req_duration": {
"avg": 182.4,
"med": 132.1,
"p(90)": 310.6,
"p(95)": 472.9,
"p(99)": 886.3,
"max": 2401.7
},
"http_req_failed": {
"rate": 0.004
},
"checks": {
"rate": 0.995
},
"list_products_duration": {
"avg": 160.2,
"p(95)": 410.5,
"p(99)": 702.4
},
"create_order_duration": {
"avg": 380.7,
"p(95)": 840.2,
"p(99)": 1510.6
},
"dropped_iterations": {
"count": 0,
"rate": 0
}
}
}
اعداد این نمونه فرضیاند و فقط برای توضیح ساختار استفاده شدهاند.
Dropped Iterations چیست؟
در تست Arrival Rate، k6 تلاش میکند Iterationها را با نرخ مشخص آغاز کند. اگر تعداد VUهای آماده کافی نباشد، بعضی Iterationها ممکن است Drop شوند.
دلایل احتمالی:
preAllocatedVUsکم است.maxVUsکم است.- زمان Iteration زیاد شده است.
- سیستم هدف کند شده است.
- بار برنامهریزیشده از ظرفیت Load Generator بیشتر است.
وجود Dropped Iteration باید در تحلیل نتیجه در نظر گرفته شود؛ زیرا ممکن است بار هدف واقعاً به سیستم اعمال نشده باشد.
تعیین Threshold درست
Threshold را از روی حدس انتخاب نکنید. منابع مناسب:
- SLO محصول
- نیاز تجربه کاربر
- Baseline نسخه فعلی
- قرارداد داخلی API
- رفتار واقعی ترافیک
- ظرفیت زیرساخت
- اهمیت Endpoint
- Timeout Client
مثال:
thresholds: {
http_req_failed: [
"rate<0.01"
],
list_products_duration: [
"p(95)<500",
"p(99)<1000"
],
create_order_duration: [
"p(95)<900",
"p(99)<1800"
]
}
Thresholdهای بیشازحد سخت میتوانند Pipeline را دائماً ناموفق کنند. Thresholdهای بسیار آسان نیز مشکل Performance را پنهان میکنند.
نرخ درخواست هدف را چگونه تعیین کنیم؟
اگر Peak واقعی را میدانید:
target_rate = observed_peak_rate × safety_factor
مثال:
observed_peak_rate = 80 requests/second
safety_factor = 1.25
target_rate = 100 requests/second
این مقدار برای Average Load یا Capacity Planning قابل استفاده است. Stress Test معمولاً فراتر از نرخ هدف حرکت میکند.
چرا Think Time مهم است؟
کاربر واقعی دائماً و بدون توقف درخواست ارسال نمیکند.
در تست مبتنی بر VU:
sleep(1);
Think Time میتواند رفتار کاربر را واقعیتر کند.
در تست Arrival Rate، نرخ Iteration مستقیماً کنترل میشود و Sleep باید با مدل بار هماهنگ باشد. افزودن Sleep طولانی ممکن است تعداد VU موردنیاز را افزایش دهد.
تست عملیات نوشتن
عملیات زیر داده ایجاد میکند:
POST /api/orders
برای جلوگیری از آلودگی داده:
- از Environment آزمایشی استفاده کنید.
- مشتری و محصول تستی جدا داشته باشید.
- شناسه درخواست یکتا ایجاد کنید.
- فرایند پاکسازی داده تعریف کنید.
- عملیات نوشتن را با نسبت واقعی اجرا کنید.
- اثر Queue، Worker و Database را اندازه بگیرید.
- اجرای تکراری را در طراحی Test Data در نظر بگیرید.
Load Test فقط آزمایش Endpoint نیست؛ کل مسیر پردازش درخواست را تحت بار قرار میدهد.
Metrics مهم k6
http_req_duration
زمان کامل درخواست HTTP.
http_req_waiting
زمان انتظار برای اولین Byte پاسخ که معمولاً بخش مهمی از زمان پردازش سمت سرور را منعکس میکند.
http_req_connecting
زمان ایجاد اتصال TCP.
http_req_tls_handshaking
زمان TLS Handshake.
http_req_sending
زمان ارسال درخواست.
http_req_receiving
زمان دریافت پاسخ.
http_req_failed
نرخ درخواستهای HTTP ناموفق.
iterations
تعداد Iterationهای کاملشده.
dropped_iterations
Iterationهایی که در زمان برنامهریزیشده شروع نشدهاند.
vus و vus_max
تعداد Virtual Userهای فعال و حداکثر قابل استفاده.
k6 در Summary پایان تست Metricهای تجمیعشده، Checkها و Thresholdها را نمایش میدهد. راهنمای خروجی نتایج k6
اتصال نتایج به API هوش مصنوعی درواره
ابتدا در درواره ثبتنام و کلید API دریافت کنید.
فایل .env:
DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL=MODEL_ID_DARVAREH
Base URL:
https://api.darvareh.ir/v1
مدل مناسب و قیمت بهروز را از صفحه مدلهای درواره انتخاب کنید.
ساخت تحلیلگر نتیجه Load Test
فایل requirements.txt:
openai
python-dotenv
pydantic
فایل scripts/analyze_results.py:
from __future__ import annotations
import json
import os
from pathlib import Path
from typing import Literal
from dotenv import load_dotenv
from openai import OpenAI
from pydantic import BaseModel, Field
load_dotenv()
class Finding(BaseModel):
metric: str
severity: Literal[
"low",
"medium",
"high",
]
observation: str
evidence: list[str] = Field(
default_factory=list
)
possible_causes: list[str] = Field(
default_factory=list
)
required_measurements: list[str] = Field(
default_factory=list
)
next_test: str | None = None
class PerformanceAnalysis(BaseModel):
test_valid: bool
overall_result: Literal[
"pass",
"fail",
"inconclusive",
]
summary: str
findings: list[Finding] = Field(
default_factory=list
)
missing_information: list[str] = Field(
default_factory=list
)
recommended_next_profile: str | None = None
ALLOWED_METRICS = {
"http_req_duration",
"http_req_failed",
"checks",
"iterations",
"dropped_iterations",
"list_products_duration",
"get_product_duration",
"create_order_duration",
"list_products_failed",
"get_product_failed",
"create_order_failed",
}
def read_json(path: str) -> dict:
return json.loads(
Path(path).read_text(
encoding="utf-8"
)
)
def strip_code_fence(value: str) -> str:
text = value.strip()
if not text.startswith("```"):
return text
lines = text.splitlines()
content = "\n".join(
lines[1:-1]
).strip()
if content.startswith("json"):
content = content[4:].lstrip()
return content
def main() -> None:
test_plan = read_json(
"test-plan.json"
)
summary = read_json(
"results/summary.json"
)
client = OpenAI(
api_key=os.environ[
"DARVAREH_API_KEY"
],
base_url="https://api.darvareh.ir/v1",
)
model_id = os.environ.get(
"DARVAREH_MODEL",
"MODEL_ID_DARVAREH",
)
response = client.chat.completions.create(
model=model_id,
temperature=0.1,
messages=[
{
"role": "system",
"content": (
"You are an API performance test "
"analyst. Use only the supplied test "
"plan and measured summary. Do not "
"invent metrics, infrastructure state, "
"database behavior, code paths, root "
"causes, or test results. Separate "
"observations from hypotheses. Return "
"valid JSON only."
),
},
{
"role": "user",
"content": json.dumps(
{
"task": (
"Evaluate whether the load test "
"was valid, identify failed or "
"near-limit metrics, explain "
"measured evidence, list possible "
"causes only as hypotheses, and "
"recommend the next controlled "
"test."
),
"rules": [
(
"Use only metrics present "
"in the summary."
),
(
"Do not claim a database, "
"CPU, memory, network, or "
"application bottleneck "
"without corresponding "
"measurements."
),
(
"Mark the test inconclusive "
"if dropped iterations or "
"missing metrics make the "
"target load uncertain."
),
(
"Do not propose a larger "
"test when the smoke profile "
"has failed."
),
],
"requiredOutput": {
"test_valid": "boolean",
"overall_result": (
"pass | fail | inconclusive"
),
"summary": "string",
"findings": [
{
"metric": "string",
"severity": (
"low | medium | high"
),
"observation": "string",
"evidence": ["string"],
"possible_causes": [
"string"
],
"required_measurements": [
"string"
],
"next_test": (
"string or null"
)
}
],
"missing_information": [
"string"
],
"recommended_next_profile": (
"string or null"
)
},
"testPlan": test_plan,
"measuredSummary": summary,
},
ensure_ascii=False,
),
},
],
)
content = response.choices[0].message.content
if not content:
raise RuntimeError(
"Model returned an empty response"
)
parsed = json.loads(
strip_code_fence(content)
)
analysis = (
PerformanceAnalysis.model_validate(
parsed
)
)
unknown_metrics = {
finding.metric
for finding in analysis.findings
if finding.metric
not in ALLOWED_METRICS
}
if unknown_metrics:
raise ValueError(
"Model referenced unknown metrics: "
+ ", ".join(
sorted(unknown_metrics)
)
)
output_path = Path(
"results/ai-analysis.json"
)
output_path.write_text(
json.dumps(
analysis.model_dump(),
ensure_ascii=False,
indent=2,
),
encoding="utf-8",
)
print(
json.dumps(
analysis.model_dump(),
ensure_ascii=False,
indent=2,
)
)
if __name__ == "__main__":
main()
اجرا:
python scripts/analyze_results.py
نمونه تحلیل هوش مصنوعی
{
"test_valid": true,
"overall_result": "pass",
"summary": "The load profile met all declared thresholds, but create_order p99 is substantially higher than its p95.",
"findings": [
{
"metric": "create_order_duration",
"severity": "medium",
"observation": "The measured p95 passed, while p99 shows a long-tail latency increase.",
"evidence": [
"p95 = 840.2 ms",
"p99 = 1510.6 ms",
"declared p95 threshold = 900 ms"
],
"possible_causes": [
"A subset of order requests may follow a slower execution path.",
"Shared downstream capacity may vary during the test."
],
"required_measurements": [
"Endpoint-level server traces for create_order",
"Database query duration during the test window",
"Worker or queue wait time if the operation is asynchronous"
],
"next_test": "Repeat the same load profile while collecting endpoint traces and database timings."
}
],
"missing_information": [
"Server-side CPU and memory metrics",
"Database latency",
"Trace data"
],
"recommended_next_profile": "load"
}
مدل بهدرستی بین Observation و Possible Cause تفاوت گذاشته است. p99 بالا اندازهگیری شده، اما علت دیتابیس یا CPU بدون Metric واقعی قابل اثبات نیست.
مقایسه Baseline و نسخه جدید
فایلهای نتیجه:
results/
├── baseline-summary.json
└── candidate-summary.json
فایل scripts/compare_results.py:
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
METRICS = [
"list_products_duration",
"get_product_duration",
"create_order_duration",
]
def read_json(path: str) -> dict[str, Any]:
return json.loads(
Path(path).read_text(
encoding="utf-8"
)
)
def percentage_change(
baseline: float,
candidate: float,
) -> float | None:
if baseline == 0:
return None
return (
(candidate - baseline)
/ baseline
* 100
)
def main() -> None:
baseline = read_json(
"results/baseline-summary.json"
)
candidate = read_json(
"results/candidate-summary.json"
)
comparison = []
for metric_name in METRICS:
baseline_metric = (
baseline.get("metrics", {})
.get(metric_name)
)
candidate_metric = (
candidate.get("metrics", {})
.get(metric_name)
)
if (
not baseline_metric
or not candidate_metric
):
continue
baseline_p95 = baseline_metric.get(
"p(95)"
)
candidate_p95 = candidate_metric.get(
"p(95)"
)
if (
baseline_p95 is None
or candidate_p95 is None
):
continue
comparison.append(
{
"metric": metric_name,
"baselineP95": baseline_p95,
"candidateP95": candidate_p95,
"changePercent": (
percentage_change(
baseline_p95,
candidate_p95,
)
),
}
)
print(
json.dumps(
{
"comparison": comparison
},
ensure_ascii=False,
indent=2,
)
)
if __name__ == "__main__":
main()
اگر:
baseline_p95 = 400 ms
candidate_p95 = 500 ms
تغییر:
change_percent = ((500 - 400) / 400) × 100
change_percent = 25%
هر دو نسخه ممکن است Threshold نهایی را پاس کنند، اما نسخه جدید 25 درصد کندتر شده باشد. به همین دلیل مقایسه با Baseline اهمیت دارد.
تعیین Regression Budget
میتوان علاوه بر Threshold مطلق، محدودیت Regression تعریف کرد:
candidate_p95 <= absolute_threshold
candidate_p95 <= baseline_p95 × 1.10
یعنی:
- p95 باید کمتر از حد مطلق باشد.
- افزایش آن نسبت به Baseline بیشتر از 10 درصد نباشد.
برای جلوگیری از حساسیت به نوسان، تستها باید:
- روی محیط مشابه اجرا شوند.
- از Profile یکسان استفاده کنند.
- داده مشابه داشته باشند.
- چند بار تکرار شوند.
- Warm-up یکسان داشته باشند.
- همزمان با Jobهای نامرتبط سنگین اجرا نشوند.
تشخیص Saturation
نشانههای احتمالی نزدیکشدن به Saturation:
- p95 و p99 با افزایش بار سریع رشد میکنند.
- نرخ خطا بالا میرود.
- Dropped Iteration ظاهر میشود.
- Throughput دیگر متناسب با بار افزایش پیدا نمیکند.
- Queue Time رشد میکند.
- CPU یا Memory به محدوده بالا میرسد.
- Connection Pool پر میشود.
- Timeout افزایش پیدا میکند.
فقط Metrics k6 برای تشخیص دقیق علت کافی نیستند. باید آنها را با Metrics سمت سرور همزمان کنید.
Metrics سمت سرور
هنگام Load Test بهتر است این موارد ثبت شوند:
برنامه
- Request Rate
- Error Rate
- Latency
- Active Requests
- Worker Utilization
- Queue Time
- Timeout Count
دیتابیس
- Query Duration
- Active Connections
- Waiting Connections
- Lock Wait
- Slow Query Count
زیرساخت
- CPU
- Memory
- Network
- Disk I/O
- Container Restart
- Pod Count
- Autoscaling Events
Cache
- Hit Rate
- Miss Rate
- Eviction
- Connection Count
- Command Latency
برای تحلیل مدل، همه Metrics باید دارای بازه زمانی یکسان باشند.
تحلیل همبستگی با هوش مصنوعی
ورودی:
{
"testWindow": {
"start": "2026-07-21T10:00:00Z",
"end": "2026-07-21T10:05:00Z"
},
"load": {
"targetRate": 25,
"achievedRate": 24.8
},
"api": {
"p95Milliseconds": 720,
"errorRate": 0.008
},
"infrastructure": {
"cpuPercentP95": 82,
"memoryPercentP95": 61
},
"database": {
"queryP95Milliseconds": 390,
"activeConnectionsP95": 48,
"connectionLimit": 50
}
}
از مدل بخواهید:
- همزمانی تغییرها را بررسی کند.
- علت قطعی اعلام نکند.
- شواهد مخالف را نیز بیان کند.
- آزمایش بعدی برای تفکیک فرضیهها پیشنهاد دهد.
- Metric جدید اختراع نکند.
اجرای Load Test در CI
Smoke Test برای Pull Request مناسبتر از تست سنگین است.
name: API performance smoke test
on:
workflow_dispatch:
pull_request:
paths:
- "backend/**"
- "tests/**"
permissions:
contents: read
jobs:
performance-smoke:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up k6
uses: grafana/setup-k6-action@v1
- name: Run smoke test
env:
TEST_PROFILE: smoke
BASE_URL: >-
${{ vars.STAGING_BASE_URL }}
API_TOKEN: >-
${{ secrets.STAGING_API_TOKEN }}
PRODUCT_ID: product-test-1
TEST_CUSTOMER_ID: customer-load-test
run: |
mkdir -p results
k6 run tests/api.js
- name: Upload performance summary
if: always()
uses: actions/upload-artifact@v6
with:
name: k6-smoke-summary
path: results/summary.json
if-no-files-found: warn
retention-days: 14
Load و Stress Test بهتر است بهصورت زمانبندیشده یا دستی در محیط مخصوص اجرا شوند.
اجرای AI Analysis در CI
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
cache: pip
- name: Install analysis dependencies
run: |
python -m pip install \
-r requirements.txt
- name: Analyze performance result
if: always()
env:
DARVAREH_API_KEY: >-
${{ secrets.DARVAREH_API_KEY }}
DARVAREH_MODEL: MODEL_ID_DARVAREH
run: |
if test -f results/summary.json
then
python scripts/analyze_results.py
fi
- name: Upload AI analysis
if: always()
uses: actions/upload-artifact@v6
with:
name: k6-ai-analysis
path: results/ai-analysis.json
if-no-files-found: warn
retention-days: 14
موفقیت تحلیل مدل نباید جای Exit Code مربوط به Thresholdهای k6 را بگیرد.
الگوی مناسب اجرای تستها
Unit Test
↓
Integration Test
↓
Build
↓
Deploy Test Environment
↓
Smoke Performance Test
↓
Average Load Test
↓
Stress یا Spike Test
↓
Analyze Results
↓
Compare with Baseline
اگر Smoke Test شکست خورد، اجرای Stress Test منطقی نیست.
اشتباهات رایج در Load Testing
شروع با بار بسیار زیاد
ابتدا Smoke Test و سپس بار مرحلهای اجرا کنید.
استفاده از محیط اشتباه
محیط Load Test باید مشخص و آماده باشد. تفاوت ظرفیت Staging و Production نیز در تفسیر نتایج لحاظ شود.
نداشتن Threshold
بدون معیار Pass و Fail، نتیجه فقط مجموعهای از عددها است.
تمرکز فقط روی Average
p95، p99، Error Rate و Dropped Iteration را بررسی کنید.
تست فقط یک Endpoint
ترکیب ترافیک باید تا حد امکان به رفتار واقعی نزدیک باشد.
استفاده از داده تکراری
داده تکراری میتواند رفتار Cache یا Constraintهای دیتابیس را غیرواقعی کند.
ایجاد داده بدون Cleanup
عملیات POST ممکن است حجم زیادی داده آزمایشی بسازد.
نادیدهگرفتن Load Generator
ممکن است محدودیت از ماشین اجرای k6 باشد، نه سیستم هدف. CPU، Memory و Network مولد بار را نیز بررسی کنید.
افزایش preAllocatedVUs برای پنهانکردن مشکل
Dropped Iteration میتواند از کمبود VU یا کندی Iteration ناشی شود. ابتدا علت را اندازهگیری کنید.
اجرای تست بدون Baseline
Threshold مطلق کافی نیست. نسخه جدید را با نسخه قبلی مقایسه کنید.
نتیجهگیری علت از روی یک Metric
افزایش Latency بهتنهایی ثابت نمیکند دیتابیس، CPU یا شبکه عامل اصلی است.
ارسال فایل کامل Metrics به مدل
فایل Time-series ممکن است بسیار بزرگ باشد. ابتدا Summary و بخشهای مرتبط را استخراج کنید.
اعتماد به Threshold تولیدشده مدل
Threshold باید از نیاز محصول، SLO و Baseline واقعی گرفته شود.
چکلیست طراحی Load Test
- هدف تست مشخص است.
- محیط هدف مشخص است.
- مجوز اجرای تست وجود دارد.
- Endpointها از قرارداد واقعی API گرفته شدهاند.
- نسبت ترافیک عملیات مشخص است.
- داده آزمایشی آماده است.
- Cleanup تعریف شده است.
- Smoke Test وجود دارد.
- Average Load مشخص است.
- Peak Rate براساس داده واقعی تعیین شده است.
- Think Time بررسی شده است.
- نوع Executor آگاهانه انتخاب شده است.
- Threshold هر Endpoint مشخص است.
- p95 و p99 ثبت میشوند.
- Error Rate اندازهگیری میشود.
- Dropped Iteration کنترل میشود.
- Baseline وجود دارد.
- Metrics سمت سرور جمعآوری میشوند.
- زمان تمام Metrics هماهنگ است.
- Load Generator نیز مانیتور میشود.
چکلیست تحلیل نتیجه
- آیا بار هدف واقعاً ایجاد شده است؟
- آیا Dropped Iteration وجود دارد؟
- آیا Checkها موفقاند؟
- آیا Thresholdها پاس شدهاند؟
- p50، p95 و p99 چقدرند؟
- کدام Endpoint کندتر است؟
- Error Rate در چه مرحلهای افزایش یافته است؟
- آیا Throughput همراه بار افزایش یافته است؟
- آیا نتیجه با Baseline قابل مقایسه است؟
- آیا محیط دو تست یکسان بوده است؟
- آیا داده تست مشابه بوده است؟
- آیا Metrics زیرساخت موجودند؟
- آیا فرضیه مدل با شواهد اندازهگیریشده پشتیبانی میشود؟
- آزمایش بعدی برای تفکیک علتها چیست؟
انتخاب مدل مناسب تحلیل Performance
مدل مناسب باید:
- JSON و Metrics را دقیق درک کند.
- Percentileها را درست تفسیر کند.
- Observation را از Hypothesis جدا کند.
- بدون Metric زیرساختی علت قطعی اعلام نکند.
- بتواند Baseline و Candidate را مقایسه کند.
- خروجی ساختاریافته تولید کند.
- تست بعدی کنترلشده پیشنهاد دهد.
- Context کافی برای Summary و Test Plan داشته باشد.
- برای اجرای مداوم هزینه مناسبی داشته باشد.
برای Summaryهای کوچک، مدل سریع و اقتصادی کافی است. برای تحلیل چند اجرای متوالی و Metrics چند سرویس، مدل قویتری ممکن است نتیجه بهتری بدهد.
فهرست مدلها، Model ID و قیمت بهروز را در صفحه مدلهای درواره مشاهده کنید.
چرا API درواره برای این پروژه مناسب است؟
تحلیل Performance باید از داخل Script، CI/CD یا داشبورد داخلی انجام شود.
با API درواره میتوانید:
- نتیجه k6 را تحلیل کنید.
- Test Plan را با Summary مقایسه کنید.
- Regression را توضیح دهید.
- یافتهها را به JSON تبدیل کنید.
- خروجی را به Artifact تبدیل کنید.
- مدل مناسب تحلیل فنی را انتخاب کنید.
- مدل را بدون تغییر معماری ابزار عوض کنید.
- تحلیل را وارد فرایند Release کنید.
برای شروع:
- در درواره ثبتنام کنید.
- کلید API بگیرید.
- مدل مناسب را از صفحه مدلها انتخاب کنید.
- Base URL زیر را در کلاینت قرار دهید:
https://api.darvareh.ir/v1
پرسشهای متداول
Load Testing چیست؟
Load Testing رفتار سیستم را زیر بار مورد انتظار اندازهگیری میکند و Latency، Throughput و Error Rate را نشان میدهد.
k6 چیست؟
k6 ابزار Performance Testing است که سناریوهای آن با JavaScript نوشته میشوند و برای تست API و سرویسهای مختلف استفاده میشود.
p95 چیست؟
p95 مقداری است که 95 درصد نمونهها کمتر یا مساوی آن هستند. اگر p95 برابر 500 میلیثانیه باشد، 95 درصد درخواستها حداکثر حدود 500 میلیثانیه زمان داشتهاند.
تفاوت Load Test و Stress Test چیست؟
Load Test بار معمول و مورد انتظار را بررسی میکند. Stress Test بار را بالاتر میبرد تا محدودیتها و رفتار سیستم تحت فشار مشخص شوند.
چند کاربر مجازی برای تست لازم است؟
تعداد VU به مدل بار، زمان Iteration، نرخ هدف و رفتار کاربر بستگی دارد. عدد ثابتی برای همه APIها وجود ندارد.
Threshold در k6 چیست؟
Threshold معیار Pass یا Fail تست است؛ مانند نرخ خطای کمتر از یک درصد یا p95 کمتر از 500 میلیثانیه.
آیا Check باعث شکست اجرای k6 میشود؟
Check نتیجه Assertion را ثبت میکند، اما برای شکست کل Test بهتر است Threshold مربوط به نرخ Check تعریف شود.
آیا هوش مصنوعی میتواند علت کندی API را پیدا کند؟
هوش مصنوعی میتواند براساس Metrics فرضیه پیشنهاد دهد؛ اما بدون Metrics برنامه، دیتابیس و زیرساخت نباید علت قطعی اعلام کند.
آیا Load Test را در هر Pull Request اجرا کنیم؟
Smoke Test کوتاه میتواند مناسب باشد. تستهای Load، Stress و Soak معمولاً بهتر است در Environment اختصاصی و بهصورت زمانبندیشده یا دستی اجرا شوند.
آیا درواره خودش Load Test اجرا میکند؟
درواره دسترسی API به مدلهای هوش مصنوعی را فراهم میکند. اجرای بار توسط k6 و تحلیل هوشمند Summary از طریق درواره انجام میشود.
Model ID درواره را از کجا دریافت کنیم؟
Model ID، مشخصات و قیمت بهروز مدلها را در صفحه مدلهای درواره ببینید.
جمعبندی
تست بار API با هوش مصنوعی زمانی ارزشمند است که اندازهگیری قطعی k6 با تحلیل کنترلشده مدل ترکیب شود.
فرایند مناسب:
- Test Plan واقعی بسازید.
- Endpoint و نسبت ترافیک را مشخص کنید.
- Thresholdها را از SLO و Baseline استخراج کنید.
- ابتدا Smoke Test اجرا کنید.
- بار را بهتدریج افزایش دهید.
- p95، p99، Error Rate و Dropped Iteration را ثبت کنید.
- Summary کوچک و ساختاریافته بسازید.
- Metrics سمت سرور را در همان بازه زمانی جمعآوری کنید.
- نتیجه را برای تحلیل به مدل بدهید.
- Observation را از Hypothesis جدا کنید.
- نسخه جدید را با Baseline مقایسه کنید.
- آزمایش بعدی را برای رد یا تأیید فرضیهها طراحی کنید.
برای ساخت تحلیلگر هوشمند نتایج k6، در درواره ثبتنام کنید، کلید API بگیرید و مدل مناسب تحلیل داده و کد را از صفحه مدلها انتخاب کنید.
مقالات مرتبط
- تولید تست نرمافزار با هوش مصنوعی
- ساخت API هوش مصنوعی آماده Production
- مانیتورینگ و Observability در هوش مصنوعی
- تحلیل Log با هوش مصنوعی
- دیباگ کد و رفع خطا با هوش مصنوعی
- ارزیابی مدلهای هوش مصنوعی و Evals
- آموزش تست API با Postman
- آموزش API درواره با cURL
- اتصال API هوش مصنوعی به اپلیکیشن
- راهنمای انتخاب بهترین API هوش مصنوعی
برای مطالعه شرایط استفاده و محدودیتهای مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.