Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save tuannguyen29/462587e38f85f8c29eb238fe1f6cd45a to your computer and use it in GitHub Desktop.
Save tuannguyen29/462587e38f85f8c29eb238fe1f6cd45a to your computer and use it in GitHub Desktop.
ài liệu kịch bản Load Test (cơ bản + nâng cao) với k6 trên Ubuntu Server 22.04

Tài liệu kịch bản Load Test (cơ bản + nâng cao) với k6 trên Ubuntu Server 22.04

Lưu ý: Trong tài liệu này mình sử dụng k6 (một số người đôi khi viết nhầm thành “k8”). Nếu bạn muốn dùng công cụ khác, hãy nói rõ.

Mục tiêu:

  • Cung cấp kịch bản test cơ bản và nâng cao cho API/Web.
  • Cách cài đặt k6 trên Ubuntu 22.04, cách chạy, và cách đọc kết quả.
  • Mẫu code k6 sẵn dùng, có thể tùy biến nhanh.

Nội dung:

  1. Cài đặt và chuẩn bị môi trường
  2. Quy ước chung sử dụng trong mã k6
  3. Kịch bản cơ bản
  4. Kịch bản nâng cao
  5. Cách chạy test và đọc kết quả
  6. Gợi ý theo dõi/giám sát
  7. Mẹo tối ưu và tránh sai lầm phổ biến

1) Cài đặt và chuẩn bị môi trường

  • Yêu cầu:

    • Ubuntu Server 22.04
    • Quyền sudo
    • Cổng mạng/Firewall cho phép outbound HTTP/HTTPS để client test gọi tới hệ thống mục tiêu
  • Cài đặt k6 (qua apt):

    bash scripts/install-k6-ubuntu-22.04.sh

    Kiểm tra:

    k6 version
  • Cấu trúc thư mục mẫu

    docs/
      load-testing-k6-ubuntu-22.04.md
    loadtests/
      basic-smoke.js
      closed-model-vus.js
      constant-arrival-rate.js
      spike.js
      stress.js
      soak.js
      multi-scenario.js
      login-and-use-token.js
      data/
        users.csv
    scripts/
      install-k6-ubuntu-22.04.sh
    

2) Quy ước chung trong mã k6

  • Biến môi trường:
    • BASE_URL: URL gốc API, ví dụ https://api.example.com
    • AUTH_PATH: Đường dẫn login (nếu có), ví dụ /auth/login
  • Thresholds (ngưỡng SLO mẫu):
    • p(95) < 300ms, tỉ lệ lỗi < 0.1%
  • Mô hình tải:
    • Closed model: theo số VU (virtual users), dùng executor ramping-vus hoặc constant-vus
    • Open model: theo RPS (arrival rate), dùng executor constant-arrival-rate hoặc ramping-arrival-rate
  • Think-time (sleep) chỉ có ý nghĩa trong closed model; với open model, tốc độ đến được điều khiển bởi executor.

3) Kịch bản cơ bản

  • Smoke Test: kiểm tra nhanh tính sẵn sàng (1-2 VU, thời gian ngắn)
  • Closed model (ramping-vus): mô phỏng số user tăng/giảm
  • Open model (constant-arrival-rate): cố định RPS
  • Spike test: tải tăng đột biến rồi giảm nhanh

Các file tương ứng:

  • loadtests/basic-smoke.js
  • loadtests/closed-model-vus.js
  • loadtests/constant-arrival-rate.js
  • loadtests/spike.js

Xem nội dung trong các file ở phần “Mẫu mã” bên dưới.

4) Kịch bản nâng cao

  • Stress test: tăng tải theo bậc đến khi hệ thống vỡ ngưỡng
  • Soak/Endurance: tải vừa phải kéo dài (hàng giờ) để phát hiện rò rỉ tài nguyên
  • Multi-scenario: nhiều kịch bản song song với tỉ lệ khác nhau (read-heavy, write-light, v.v.)
  • Đăng nhập + tương quan (correlation): login lấy token rồi dùng cho các request tiếp theo (có tham số từ file CSV)

Các file tương ứng:

  • loadtests/stress.js
  • loadtests/soak.js
  • loadtests/multi-scenario.js
  • loadtests/login-and-use-token.js
  • loadtests/data/users.csv

5) Cách chạy test và đọc kết quả

  • Thiết lập biến môi trường:

    export BASE_URL="https://api.example.com"
    export AUTH_PATH="/auth/login"
  • Chạy từng kịch bản:

    # Smoke
    k6 run -e BASE_URL="$BASE_URL" loadtests/basic-smoke.js
    
    # Closed model (VU ramping)
    k6 run -e BASE_URL="$BASE_URL" loadtests/closed-model-vus.js
    
    # Constant RPS (Open model)
    k6 run -e BASE_URL="$BASE_URL" loadtests/constant-arrival-rate.js
    
    # Spike
    k6 run -e BASE_URL="$BASE_URL" loadtests/spike.js
    
    # Stress
    k6 run -e BASE_URL="$BASE_URL" loadtests/stress.js
    
    # Soak (chạy dài, cân nhắc screen/tmux)
    k6 run -e BASE_URL="$BASE_URL" loadtests/soak.js
    
    # Login + Token (cần users.csv)
    k6 run -e BASE_URL="$BASE_URL" -e AUTH_PATH="$AUTH_PATH" loadtests/login-and-use-token.js
  • Ghi log kết quả ra file JSON:

    k6 run -e BASE_URL="$BASE_URL" --summary-export=summary.json loadtests/constant-arrival-rate.js
  • Cách đọc kết quả:

    • http_req_duration: xem p(50), p(90), p(95), p(99)
    • http_req_failed: tỉ lệ lỗi
    • iterations, data_received/sent: thông tin phụ
    • Khi p95/p99 tăng mạnh khi tăng tải nhẹ => có thể đụng hàng đợi hoặc nghẽn tài nguyên (DB/CPU/Pool)

6) Gợi ý theo dõi/giám sát

  • Tích hợp output:
    • InfluxDB + Grafana, hoặc Prometheus Remote Write (xem tài liệu k6 output)
  • Kết hợp APM/Observability:
    • Prometheus (exporters), Grafana, OpenTelemetry, Jaeger để theo dõi DB, cache, downstream
  • Gắn nhãn/note test: để phân tích dễ hơn sau này

7) Mẹo tối ưu và tránh sai lầm phổ biến

  • Tách generator ra máy riêng nếu server test yếu (tránh nghẽn ở client)
  • Dataset đa dạng, tránh ID lặp lại gây cache hit giả tạo
  • Test cả “cold cache” và “warm cache” nếu cần
  • Sử dụng open model để giảm “coordinated omission” khi đo latency
  • Theo dõi tài nguyên: CPU, RAM, IO, connection pool, thread/worker, DB locks
  • Đặt ngưỡng (thresholds) rõ ràng để CI/CD có thể fail sớm

Có thể điều chỉnh các kịch bản và tham số cho sát thực tế hơn (ví dụ mix đọc/ghi, payload kích thước, think-time…).


Script example:

loadtests/basic-smoke.js

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  vus: 1,
  duration: '30s',
  thresholds: {
    http_req_failed: ['rate<0.01'], // <1% lỗi
    http_req_duration: ['p(95)<500'], // p95 < 500ms
  },
};

const BASE_URL = __ENV.BASE_URL || 'https://example.com';

export default function () {
  const res = http.get(`${BASE_URL}/health`); // thay endpoint phù hợp
  check(res, {
    'status is 200': (r) => r.status === 200,
  });
  sleep(1);
}

loadtests/closed-model-vus.js

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  scenarios: {
    ramping_users: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '1m', target: 20 },
        { duration: '3m', target: 50 },
        { duration: '2m', target: 0 },
      ],
      gracefulRampDown: '30s',
    },
  },
  thresholds: {
    http_req_failed: ['rate<0.001'],
    http_req_duration: ['p(95)<300', 'p(99)<800'],
  },
};

const BASE_URL = __ENV.BASE_URL || 'https://example.com';

export default function () {
  const res = http.get(`${BASE_URL}/items?limit=20`);
  check(res, { '200 OK': (r) => r.status === 200 });
  // Think-time có ý nghĩa trong closed model
  sleep(1 + Math.random() * 2);
}

loadtests/constant-arrival-rate.js

import http from 'k6/http';
import { check } from 'k6';

export const options = {
  scenarios: {
    steady_rps: {
      executor: 'constant-arrival-rate',
      rate: 200, // 200 requests/giây
      timeUnit: '1s',
      duration: '10m',
      preAllocatedVUs: 50, // VU sơ bộ
      maxVUs: 200,         // trần VU khi hệ thống chậm
    },
  },
  thresholds: {
    http_req_failed: ['rate<0.001'],
    http_req_duration: ['p(95)<300'],
  },
};

const BASE_URL = __ENV.BASE_URL || 'https://example.com';

export default function () {
  const res = http.get(`${BASE_URL}/search?q=test`);
  check(res, { '200 OK': (r) => r.status === 200 });
}

loadtests/spike.js

import http from 'k6/http';
import { check } from 'k6';

export const options = {
  scenarios: {
    spike_rps: {
      executor: 'ramping-arrival-rate',
      startRate: 0,
      timeUnit: '1s',
      preAllocatedVUs: 50,
      maxVUs: 500,
      stages: [
        { target: 50, duration: '30s' },   // tăng nhẹ
        { target: 500, duration: '15s' },  // spike
        { target: 50, duration: '1m' },    // giảm về mức cũ
        { target: 0, duration: '30s' },
      ],
    },
  },
  thresholds: {
    http_req_failed: ['rate<0.01'],
    http_req_duration: ['p(95)<600'],
  },
};

const BASE_URL = __ENV.BASE_URL || 'https://example.com';

export default function () {
  const res = http.get(`${BASE_URL}/hot-items`);
  check(res, { '200 OK': (r) => r.status === 200 });
}

loadtests/stress.js

import http from 'k6/http';
import { check } from 'k6';

export const options = {
  scenarios: {
    stress_steps: {
      executor: 'ramping-arrival-rate',
      timeUnit: '1s',
      preAllocatedVUs: 100,
      maxVUs: 1000,
      stages: [
        { target: 100, duration: '1m' },
        { target: 200, duration: '2m' },
        { target: 300, duration: '2m' },
        { target: 400, duration: '2m' },
        { target: 500, duration: '2m' },
        { target: 0, duration: '1m' },
      ],
    },
  },
  thresholds: {
    http_req_failed: ['rate<0.02'],    // cho phép lỗi nhiều hơn khi stress
    http_req_duration: ['p(95)<800'],  // p95 có thể tăng
  },
};

const BASE_URL = __ENV.BASE_URL || 'https://example.com';

export default function () {
  const res = http.get(`${BASE_URL}/items?limit=50`);
  check(res, { '200 OK': (r) => r.status === 200 });
}

loadtests/soak.js

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  scenarios: {
    soak_test: {
      executor: 'constant-arrival-rate',
      rate: 50, // tải vừa phải
      timeUnit: '1s',
      duration: '2h', // endurance
      preAllocatedVUs: 20,
      maxVUs: 100,
    },
  },
  thresholds: {
    http_req_failed: ['rate<0.005'],
    http_req_duration: ['p(95)<400'],
  },
};

const BASE_URL = __ENV.BASE_URL || 'https://example.com';

export default function () {
  const res = http.get(`${BASE_URL}/profile?id=${Math.floor(Math.random()*100000)}`);
  check(res, { '200 OK': (r) => r.status === 200 });
  // Think-time không ảnh hưởng đến arrival-rate nhưng vẫn hợp lý mô phỏng hành vi
  sleep(Math.random());
}

loadtests/multi-scenario.js

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  scenarios: {
    // 70% đọc danh sách
    list_items: {
      executor: 'constant-arrival-rate',
      rate: 140,
      timeUnit: '1s',
      duration: '10m',
      preAllocatedVUs: 40,
      maxVUs: 200,
      exec: 'listItems',
    },
    // 25% xem chi tiết
    view_detail: {
      executor: 'constant-arrival-rate',
      rate: 50,
      timeUnit: '1s',
      duration: '10m',
      preAllocatedVUs: 20,
      maxVUs: 100,
      exec: 'viewDetail',
    },
    // 5% thao tác ghi
    create_item: {
      executor: 'constant-arrival-rate',
      rate: 10,
      timeUnit: '1s',
      duration: '10m',
      preAllocatedVUs: 10,
      maxVUs: 50,
      exec: 'createItem',
    },
  },
  thresholds: {
    'http_req_failed{scenario:list_items}': ['rate<0.001'],
    'http_req_failed{scenario:view_detail}': ['rate<0.001'],
    'http_req_failed{scenario:create_item}': ['rate<0.005'],
    'http_req_duration{scenario:list_items}': ['p(95)<300'],
    'http_req_duration{scenario:view_detail}': ['p(95)<350'],
    'http_req_duration{scenario:create_item}': ['p(95)<600'],
  },
};

const BASE_URL = __ENV.BASE_URL || 'https://example.com';

export function listItems() {
  const res = http.get(`${BASE_URL}/items?limit=20`);
  check(res, { '200 OK': (r) => r.status === 200 });
}

export function viewDetail() {
  const id = Math.floor(Math.random() * 100000) + 1;
  const res = http.get(`${BASE_URL}/items/${id}`);
  check(res, { '200 OK': (r) => r.status === 200 });
  sleep(Math.random() * 0.2);
}

export function createItem() {
  const payload = JSON.stringify({
    name: `item-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
    price: Math.floor(Math.random() * 10000) + 1000,
  });
  const headers = { 'Content-Type': 'application/json' };
  const res = http.post(`${BASE_URL}/items`, payload, { headers });
  check(res, {
    '201 Created or 200': (r) => r.status === 201 || r.status === 200,
  });
}

loadtests/login-and-use-token.js

import http from 'k6/http';
import { check, sleep } from 'k6';
import { SharedArray } from 'k6/data';

export const options = {
  scenarios: {
    auth_and_use: {
      executor: 'constant-arrival-rate',
      rate: 50,
      timeUnit: '1s',
      duration: '15m',
      preAllocatedVUs: 20,
      maxVUs: 200,
    },
  },
  thresholds: {
    http_req_failed: ['rate<0.005'],
    http_req_duration: ['p(95)<400'],
  },
};

const BASE_URL = __ENV.BASE_URL || 'https://example.com';
const AUTH_PATH = __ENV.AUTH_PATH || '/auth/login';

// Tải danh sách user từ CSV (header: username,password)
const users = new SharedArray('users', () => {
  const raw = open('./data/users.csv'); // relative to script
  const lines = raw.trim().split('\n');
  const header = lines.shift();
  return lines
    .map((line) => line.trim())
    .filter(Boolean)
    .map((line) => {
      const [username, password] = line.split(',');
      return { username, password };
    });
});

function pickRandomUser() {
  return users[Math.floor(Math.random() * users.length)];
}

export default function () {
  // 1) Đăng nhập
  const { username, password } = pickRandomUser();
  const loginRes = http.post(
    `${BASE_URL}${AUTH_PATH}`,
    JSON.stringify({ username, password }),
    { headers: { 'Content-Type': 'application/json' } }
  );

  check(loginRes, {
    'login success (200/201)': (r) => r.status === 200 || r.status === 201,
  });

  if (loginRes.status >= 400) {
    // Fail fast nếu login lỗi
    return;
  }

  let token;
  try {
    token = loginRes.json('token') || loginRes.json('access_token');
  } catch (_e) {
    token = null;
  }

  // 2) Gọi các API yêu cầu xác thực
  const authHeaders = token
    ? { Authorization: `Bearer ${token}` }
    : {};

  const listRes = http.get(`${BASE_URL}/me/orders?limit=10`, { headers: authHeaders });
  check(listRes, { 'orders 200': (r) => r.status === 200 });

  const detailRes = http.get(`${BASE_URL}/me/profile`, { headers: authHeaders });
  check(detailRes, { 'profile 200': (r) => r.status === 200 });

  // 3) Hành vi viết có kiểm soát
  if (Math.random() < 0.2) {
    const createRes = http.post(
      `${BASE_URL}/me/notes`,
      JSON.stringify({ body: `note ${Date.now()}` }),
      { headers: { ...authHeaders, 'Content-Type': 'application/json' } }
    );
    check(createRes, {
      'create note 2xx': (r) => r.status === 201 || r.status === 200,
    });
  }

  // Think-time nhẹ nhàng
  sleep(0.5 + Math.random() * 1.5);
}

loadtests/data/users.csv

username,password
user001@example.com,pass001
user002@example.com,pass002
user003@example.com,pass003
user004@example.com,pass004
user005@example.com,pass005
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment