Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 41 — Performance Testing trong CI

Đã đọc 0/14 mục · còn khoảng 13 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng một buổi tối thứ Sáu cuối tháng, đội sản phẩm của bạn vừa merge một tính năng mới lên nhánh chính. Tất cả unit test đều xanh, code review đã duyệt, pipeline chạy qua các bước lint, test, build một cách trơn tru. Bạn nhấn deploy với sự tự tin cao độ. Rồi sáng thứ Hai, khi lượng người dùng đổ vào, trang thanh toán bắt đầu treo. Response time từ 200ms nhảy vọt lên 4 giây. Bộ phận vận hành rối loạn, khách hàng bỏ giỏ hàng, và không ai biết chính xác commit nào đã gây ra thảm họa này.

Vấn đề nằm ở chỗ: pipeline của bạn kiểm tra được tính đúng (correctness) nhưng hoàn toàn mù trước tính nhanh (performance). Một hàm có thể trả về kết quả đúng nhưng chậm gấp mười lần phiên bản cũ mà không một test nào bắt được. Đây chính là lý do Performance Testing trong CI ra đời: biến hiệu năng thành một thuộc tính được kiểm chứng tự động ở mỗi lần thay đổi code, thay vì một điều bất ngờ được phát hiện trên production.

Trong bài này, chúng ta sẽ tập trung riêng vào chuyện đưa kiểm thử hiệu năng vào bên trong pipeline CI — cách chọn loại test phù hợp cho từng giai đoạn, cách đọc con số để đưa ra quyết định pass/fail, và cách tránh việc pipeline bị chậm hoặc "nhấp nháy" (flaky) vì chính những bài test này. Đây là kỹ năng phân biệt một kỹ sư DevOps chỉ biết dựng pipeline với một người thực sự bảo vệ được trải nghiệm người dùng.

Khái niệm cốt lõi

Performance testing trong CI khác gì so với chạy thủ công?

Chạy một công cụ load test trên máy cá nhân trước khi release là chuyện nhiều người làm. Nhưng "trong CI" nghĩa là bài test đó chạy tự động, có ngưỡng rõ ràng, và có khả năng chặn merge khi hiệu năng suy giảm. Sự khác biệt cốt lõi nằm ở ba yếu tố: tự động hóa (không phụ thuộc con người nhớ chạy), tính lặp lại (môi trường chuẩn hóa để so sánh được giữa các lần), và cơ chế gác cổng (fail pipeline khi vượt ngưỡng).

Điều này đặt ra một ràng buộc quan trọng mà người mới hay bỏ qua: pipeline CI cần chạy nhanh (thường dưới 10-15 phút) và cho kết quả ổn định. Một bài stress test kéo dài 45 phút với kết quả dao động 30% mỗi lần chạy sẽ phá hỏng trải nghiệm CI. Vì vậy, nghệ thuật ở đây không phải là "test càng nhiều càng tốt" mà là "chọn đúng loại test cho đúng giai đoạn".

Bốn loại performance test bạn cần phân biệt

Smoke performance test — tải tối thiểu, thường chỉ 1-5 người dùng ảo trong 30-60 giây. Mục đích không phải đo giới hạn hệ thống mà là kiểm tra "sanity": endpoint có phản hồi không, có lỗi 500 dưới tải nhẹ không, latency cơ bản có nằm trong khoảng chấp nhận được không. Đây là loại duy nhất đủ nhanh để chạy ở mỗi commit hoặc mỗi pull request.

Load test — mô phỏng lượng truy cập ở mức đỉnh dự kiến trong điều kiện bình thường. Ví dụ, nếu vào giờ cao điểm hệ thống của bạn nhận 500 request/giây, load test sẽ đẩy đúng mức đó và kiểm tra xem p95 latency, throughput, tỷ lệ lỗi có còn trong ngưỡng SLA không. Load test thường chạy trên nhánh chính sau merge, hoặc theo lịch (nightly), vì nó tốn 5-15 phút.

Stress test — đẩy hệ thống vượt qua mức đỉnh để tìm điểm gãy (breaking point). Bạn tăng dần tải cho đến khi hệ thống bắt đầu từ chối request hoặc sập, nhằm trả lời câu hỏi "chúng ta chịu được đến đâu và gãy như thế nào". Stress test hiếm khi nằm trong CI thường xuyên vì nó dài và tốn tài nguyên; thường được chạy trước những sự kiện lớn (ví dụ trước đợt sale) hoặc theo lịch tuần.

Soak test (endurance) — chạy tải vừa phải trong thời gian dài (vài giờ) để phát hiện memory leak, kết nối database bị rò rỉ, hoặc suy giảm hiệu năng theo thời gian. Loại này gần như không bao giờ nằm trong pipeline PR; nó thuộc về môi trường staging chạy nền.

Latency, throughput và percentile — đọc con số cho đúng

Điểm chết người nhất khi mới làm performance test là nhìn vào giá trị trung bình (average). Trung bình che giấu nỗi đau. Nếu 95 request trả về trong 100ms và 5 request trả về trong 5 giây, trung bình vẫn "đẹp" nhưng 5% người dùng của bạn đang có trải nghiệm tệ hại.

Vì vậy hãy luôn làm việc với percentile: p50 (median — nửa số request nhanh hơn con số này), p95 (95% request nhanh hơn), p99 (99% request nhanh hơn — thường phản ánh những trường hợp tệ nhất mà người dùng thực sự cảm nhận). Ngưỡng gác cổng trong CI nên đặt trên p95 hoặc p99, không phải average. Bên cạnh latency, hai chỉ số cần theo dõi là throughput (số request xử lý được mỗi giây) và error rate (tỷ lệ request thất bại dưới tải).

Gác cổng theo ngưỡng tuyệt đối và theo baseline

Có hai cách đặt điều kiện pass/fail. Ngưỡng tuyệt đối (absolute threshold): "p95 phải dưới 300ms, error rate dưới 1%". Đơn giản, dễ hiểu, phù hợp với smoke test. So với baseline (regression detection): so sánh kết quả lần này với kết quả trên nhánh chính, fail nếu hiệu năng suy giảm quá một tỷ lệ (ví dụ p95 tăng hơn 15%). Cách thứ hai mạnh hơn vì nó bắt được "regression tương đối" ngay cả khi con số tuyệt đối vẫn dưới ngưỡng — nhưng đòi hỏi bạn lưu và quản lý baseline một cách kỷ luật.

Tình huống thực tế

Ví dụ 1 — Tiki và cái bẫy latency trung bình trước đợt sale 12/12

Một đội backend giả định tại một sàn thương mại điện tử lớn ở Việt Nam (lấy bối cảnh tương tự Tiki) chuẩn bị cho đợt sale 12/12. Họ đã có load test chạy nightly, ngưỡng gác cổng đặt ở "average latency dưới 250ms". Suốt hai tuần pipeline đều xanh. Nhưng ba ngày trước sale, khi họ chạy thử một đợt stress test thủ công với 3.000 người dùng ảo, hệ thống giỏ hàng sập ở mức 1.800 concurrent user — thấp hơn nhiều so với lượng dự kiến 5.000.

Điều tra ra, một truy vấn tính phí vận chuyển có p99 lên tới 3,2 giây dưới tải cao, nhưng vì chỉ chiếm 2% tổng số request nên average vẫn đẹp ở mức 240ms. Baseline gác cổng bằng average đã che giấu hoàn toàn vấn đề. Đội chuyển ngưỡng CI sang p99 dưới 800ms và bổ sung một stress test hàng tuần vào lịch pipeline. Ở đợt sale sau, họ phát hiện regression tương tự ngay khi nó xuất hiện, trước khi lên production.

Bài học: average là kẻ nói dối. Gác cổng phải đặt trên percentile cao, và stress test — dù không chạy mỗi commit — vẫn phải có chỗ trong lịch pipeline định kỳ, đặc biệt trước các sự kiện tải cao.

Ví dụ 2 — Một startup fintech Đông Nam Á và pipeline bị "nhấp nháy"

Một startup fintech ở Singapore đưa k6 load test vào pipeline PR với ý định tốt: mỗi PR chạy 200 VU (virtual user) trong 3 phút, fail nếu p95 vượt 400ms. Vấn đề là họ chạy test này trên GitHub-hosted runner dùng chung, nơi tài nguyên CPU biến động mạnh. Kết quả: cùng một commit, lúc thì p95 = 320ms (pass), lúc thì p95 = 480ms (fail). Các kỹ sư mất niềm tin, bắt đầu "retry cho đến khi xanh", và bài test trở nên vô nghĩa.

Đội khắc phục bằng ba thay đổi. Thứ nhất, chuyển performance test nặng ra khỏi pipeline PR, chỉ giữ lại một smoke performance test (10 VU, 45 giây) ở mỗi PR để bắt lỗi thô. Thứ hai, chuyển load test đầy đủ sang self-hosted runner có tài nguyên cố định, chạy sau khi merge vào nhánh chính. Thứ ba, thay ngưỡng tuyệt đối cứng bằng so sánh baseline có biên độ cho phép (tolerance) 10%. Sau đó tỷ lệ flaky giảm từ khoảng 20% xuống dưới 2%.

Bài học: performance test cực kỳ nhạy với môi trường thực thi. Chạy nó trên runner dùng chung, tài nguyên biến động là công thức tạo ra flaky test. Hãy tách theo giai đoạn (smoke ở PR, load ở nhánh chính) và cố định môi trường đo.

Ví dụ 3 — Đội API nội bộ của một ngân hàng và regression 15% âm thầm

Một đội xây dựng API nội bộ tại một ngân hàng ở Việt Nam có ngưỡng tuyệt đối rất rộng rãi: "p95 dưới 1 giây". Endpoint xác thực của họ vốn chạy ở 180ms. Qua sáu tháng và hàng trăm commit, mỗi lần ai đó thêm một lời gọi service phụ, p95 nhích lên vài chục mili-giây. Không lần nào chạm ngưỡng 1 giây, nên pipeline luôn xanh. Đến khi có người thực sự nhìn vào dashboard, p95 đã là 620ms — chậm gấp ba lần so với sáu tháng trước, và không ai chịu trách nhiệm cho lần suy giảm cụ thể nào.

Đội bổ sung regression gate theo baseline: lưu kết quả p95 của nhánh chính, và fail bất kỳ PR nào làm p95 tăng hơn 8% so với baseline. Ngay tuần đầu, cơ chế này chặn một PR thêm cache sai cách làm p95 tăng 22%. Con số tuyệt đối vẫn dưới 1 giây, nhưng regression tương đối bị bắt ngay lập tức.

Bài học: ngưỡng tuyệt đối một mình không đủ. Hiệu năng suy giảm theo kiểu "luộc ếch" — mỗi lần một chút, không lần nào đủ để báo động. Chỉ có gác cổng theo baseline mới bắt được kiểu bào mòn dần này.

Hướng dẫn từng bước

Dưới đây là cách đưa performance testing vào pipeline một cách thực dụng, dùng k6 (một công cụ load test viết bằng Go, script bằng JavaScript, rất hợp với CI) làm ví dụ.

Bước 1 — Xác định các endpoint và kịch bản trọng yếu. Đừng test mọi thứ. Chọn 3-5 luồng quan trọng nhất về mặt kinh doanh: đăng nhập, tìm kiếm, thêm giỏ hàng, thanh toán. Đây là những nơi mà một regression hiệu năng gây thiệt hại thực sự.

Bước 2 — Viết một smoke performance test. Tạo script tối giản, tải nhẹ, thời gian ngắn. Với k6:

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

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

export default function () {
  const res = http.get('https://staging.example.com/api/products');
  check(res, { 'status is 200': (r) => r.status === 200 });
}

Điểm mấu chốt là khối thresholds: nếu vi phạm, k6 thoát với exit code khác 0, và pipeline tự động fail.

Bước 3 — Gắn vào pipeline như một stage riêng. Đặt performance test thành một job độc lập, chạy sau khi build và deploy lên môi trường test. Ví dụ với GitHub Actions:

perf-smoke:
  runs-on: ubuntu-latest
  needs: [deploy-staging]
  steps:
    - uses: actions/checkout@v4
    - name: Run k6 smoke test
      uses: grafana/k6-action@v0.3.1
      with:
        filename: tests/perf/smoke.js

Bước 4 — Phân tầng theo giai đoạn. Ở PR chỉ chạy smoke test (nhanh, gác lỗi thô). Sau khi merge vào nhánh chính, chạy load test đầy đủ với số VU cao hơn và thời gian dài hơn. Stress/soak test đưa vào lịch nightly hoặc weekly bằng schedule trigger.

Bước 5 — Cố định môi trường đo. Deploy phiên bản cần test lên một môi trường có tài nguyên ổn định và gần giống production (cùng loại instance, cùng cấu hình database). Nếu chạy trên runner, ưu tiên self-hosted runner có tài nguyên cố định cho các bài load/stress. Đừng bao giờ so sánh kết quả giữa hai môi trường có cấu hình khác nhau.

Bước 6 — Lưu baseline và so sánh regression. Sau mỗi lần chạy trên nhánh chính, lưu các chỉ số (p95, p99, throughput) vào một nơi bền vững (artifact, database chỉ số, hoặc dịch vụ như Grafana Cloud k6). Trong các lần chạy sau, so sánh với baseline và fail nếu suy giảm vượt biên độ cho phép (ví dụ 10%).

Bước 7 — Xuất báo cáo dễ đọc. Đẩy kết quả ra dạng bảng tóm tắt hoặc comment tự động trên PR: p95 lần này so với baseline, tăng/giảm bao nhiêu phần trăm. Con số hiện ngay trước mắt reviewer có sức mạnh gấp nhiều lần một log dài phải tự đào.

Lỗi thường gặp & mẹo

Lỗi 1 — Gác cổng bằng average. Như đã phân tích, average che giấu đuôi phân phối. Luôn dùng p95/p99 cho threshold.

Lỗi 2 — Chạy load test nặng ở mỗi PR. Điều này làm CI chậm khủng khiếp và dễ flaky. Nguyên tắc: PR chỉ chạy smoke, còn load/stress đẩy về nhánh chính hoặc lịch định kỳ.

Lỗi 3 — Test trên môi trường không ổn định. Runner dùng chung với tài nguyên biến động sẽ tạo kết quả dao động, khiến team mất niềm tin. Cố định môi trường là điều kiện tiên quyết để so sánh có ý nghĩa.

Lỗi 4 — Không warm-up. Nhiều hệ thống (JVM với JIT, connection pool, cache) chậm ở những request đầu tiên. Nếu đo ngay từ giây đầu, bạn đang đo giai đoạn khởi động chứ không phải trạng thái ổn định. Thêm một giai đoạn ramp-up ngắn và loại bỏ dữ liệu warm-up khỏi tính toán.

Lỗi 5 — Dữ liệu test không thực tế. Test với database rỗng cho kết quả đẹp giả tạo. Truy vấn quét bảng có 10 dòng rất khác với bảng có 10 triệu dòng. Hãy dùng bộ dữ liệu có kích thước gần production.

Lỗi 6 — Bỏ qua tài nguyên phía sinh tải. Nếu máy chạy k6 hết CPU trước khi máy chủ đích quá tải, bạn đang đo giới hạn của công cụ test chứ không phải của hệ thống. Luôn giám sát cả phía tạo tải.

Mẹo: Bắt đầu bằng cảnh báo, không phải chặn cứng. Trong 1-2 tuần đầu, để performance gate ở chế độ báo cáo (không fail pipeline) để hiểu độ dao động tự nhiên và hiệu chỉnh ngưỡng. Chỉ bật chế độ chặn khi bạn tin ngưỡng đã ổn định — nếu không, một gate quá nhạy sẽ nhanh chóng bị cả team học cách phớt lờ.

Bài tập thực hành

  • Viết smoke test đầu tiên. Chọn một API bất kỳ bạn đang làm (hoặc một API công khai). Viết một script k6 với 5 VU trong 45 giây, đặt threshold p(95)<500ms và error rate <1%. Chạy cục bộ, quan sát output và xác nhận exit code thay đổi khi bạn hạ ngưỡng xuống mức không thể đạt.
  • Gắn vào pipeline. Thêm một job perf-smoke vào file workflow CI (GitHub Actions hoặc GitLab CI), cho nó chạy sau bước deploy staging. Cố ý đặt một threshold quá nghiêm để thấy pipeline fail, rồi nới lại cho hợp lý.
  • Phân biệt bốn loại test. Với hệ thống bạn đang làm, viết một bảng ngắn: loại test nào (smoke/load/stress/soak) chạy ở giai đoạn nào (PR / nhánh chính / nightly / weekly), số VU và thời gian dự kiến, và lý do. Đây là bài tập tư duy thiết kế quan trọng hơn cả việc gõ code.
  • Thiết kế regression gate. Viết ra bằng lời cách bạn sẽ lưu baseline p95 và so sánh với lần chạy mới. Baseline lưu ở đâu? Biên độ cho phép bao nhiêu phần trăm? Điều gì xảy ra khi baseline cần cập nhật sau một cải tiến hiệu năng có chủ đích?

Tóm tắt

Performance testing trong CI biến hiệu năng từ một điều bất ngờ trên production thành một thuộc tính được kiểm chứng tự động ở mỗi thay đổi. Bốn loại test — smoke (tải tối thiểu, sanity check), load (mức đỉnh dự kiến), stress (vượt đỉnh để tìm điểm gãy), và soak (chạy dài để bắt rò rỉ) — mỗi loại có chỗ đứng riêng theo giai đoạn pipeline: smoke ở mỗi PR, load ở nhánh chính, stress và soak theo lịch định kỳ.

Ba nguyên tắc sống còn: một, luôn gác cổng bằng percentile (p95/p99) chứ không bao giờ bằng average, vì average che giấu nỗi đau ở đuôi phân phối. Hai, cố định môi trường đo — performance test cực nhạy với tài nguyên, và runner biến động là nguồn gốc của flaky test làm cả team mất niềm tin. Ba, kết hợp ngưỡng tuyệt đối với gác cổng theo baseline để bắt được cả regression tuyệt đối lẫn kiểu bào mòn dần "luộc ếch" qua hàng trăm commit.

Ba tình huống thực tế — từ đợt sale 12/12, pipeline nhấp nháy của startup fintech, đến regression âm thầm ở API ngân hàng — đều dẫn về cùng một chân lý: pipeline kiểm tra tính đúng thôi chưa đủ, bạn còn phải bảo vệ tính nhanh, một cách tự động và có kỷ luật.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi