Danh sách bài viết

Bài 73: Cân Bằng Load — Shard Theo File Vs Theo Test

Shard chia đều số lượng test không đồng nghĩa với chia đều thời gian chạy. Bài này phân tích hai chiến lược phân phối test vào shard — file-level và test-level — giải thích khi nào từng chiến lược gây imbalance, cách chẩn đoán shard chậm qua HTML report và CI log, và các pattern thực tế để equalize wall-clock time.

28/05/2026
0 lượt xem
1

Mục Tiêu Bài Học

Sau bài này bạn sẽ:

  • Phân biệt file-level shard và test-level shard — cơ chế phân phối của từng loại.
  • Hiểu khi nào shard bị mất cân bằng dù số test chia đều.
  • Đọc HTML report và CI log để xác định shard chậm.
  • Áp dụng các pattern thực tế để equalize wall-clock time.
  • Biết giới hạn của Playwright built-in sharding so với third-party như Knapsack.
2

Vấn Đề: Shard Không Đều

Khi chạy --shard=x/4, mục tiêu là mỗi shard chiếm xấp xỉ 1/4 tổng thời gian để tất cả hoàn thành cùng lúc. Wall-clock time của toàn pipeline phụ thuộc vào shard chậm nhất — các shard khác đã xong nhưng vẫn phải chờ.

Giả sử có 4 file test:

File Số test Duration thực tế
auth.spec.ts1045s
checkout.spec.ts50180s
search.spec.ts3090s
profile.spec.ts2060s

Tổng: 110 test — 375s. Với 4 shard lý tưởng: ~94s/shard. Nhưng nếu checkout.spec.ts (180s) rơi vào một shard duy nhất, shard đó sẽ chiếm 180s trong khi 3 shard còn lại chỉ 65s. Pipeline kết thúc sau 180s thay vì 94s — chậm hơn ~2x so với lý thuyết.

Nguyên nhân không phải do cú pháp --shard sai, mà do cách Playwright phân phối test vào shard.

3

File-Level Shard

File-level shard nghĩa là tất cả test trong một file luôn nằm trong cùng một shard. Playwright không tách file ra nhiều shard. Đây là cơ chế mặc định khi fullyParallel: false (giá trị mặc định trước v1.30).

Cơ chế phân phối

Playwright lấy danh sách file test, sort theo tên (alphabetical), rồi phân phối lần lượt vào các shard theo round-robin dựa trên số file. Với 4 file và 4 shard:

Shard 1 (--shard=1/4): auth.spec.ts        → 10 test
Shard 2 (--shard=2/4): checkout.spec.ts    → 50 test
Shard 3 (--shard=3/4): search.spec.ts      → 30 test
Shard 4 (--shard=4/4): profile.spec.ts     → 20 test

Ưu điểm

  • Deterministic: cùng input luôn ra cùng phân phối.
  • Safe với shared state: beforeAllafterAll trong file chỉ chạy một lần trên shard đó, không bị split.
  • Dễ debug: biết file nào đang chạy trên shard nào.

Nhược điểm

  • File lớn (>50 test) làm shard đó chiếm phần lớn duration.
  • Số file ít hơn số shard → một số shard không có test để chạy, lãng phí CI machine.
  • Không tận dụng được việc equalize theo duration thực tế.
// playwright.config.ts — file-level shard behavior (fullyParallel: false)
export default {
  // fullyParallel không set hoặc set false
  // → test trong file chạy tuần tự, toàn file ở cùng 1 shard
  testDir: './tests',
};
4

Test-Level Shard

Test-level shard cho phép các test trong cùng một file được phân phối vào các shard khác nhau. Điều kiện: fullyParallel: true ở config level hoặc describe.configure({ mode: 'parallel' }) ở file level.

Cơ chế phân phối

Playwright enumerate toàn bộ danh sách test (không phải file), sort, rồi phân phối theo round-robin. Với 110 test và 4 shard, mỗi shard nhận ~27-28 test:

Shard 1: test #1, #5, #9, ...  → 28 test (mix từ nhiều file)
Shard 2: test #2, #6, #10, ... → 28 test
Shard 3: test #3, #7, #11, ... → 27 test
Shard 4: test #4, #8, #12, ... → 27 test

Điều kiện bắt buộc

// playwright.config.ts
export default {
  fullyParallel: true,  // Test trong file chạy song song, có thể split cross-shard
  testDir: './tests',
};

Nếu file dùng describe.configure({ mode: 'serial' }), các test trong describe block đó vẫn bị giữ trên cùng shard — Playwright tôn trọng serial constraint.

Ảnh hưởng lên beforeAll

Khi file bị split qua nhiều shard, beforeAll ở file scope sẽ chạy mỗi shard nếu shard đó có ít nhất một test từ file đó. Nếu beforeAll tốn thời gian đáng kể (ví dụ seed database), overhead này nhân lên theo số shard chứa test từ file đó.

// tests/checkout.spec.ts
import { test, beforeAll } from '@playwright/test';

// Chạy trên mỗi shard có test từ file này
beforeAll(async ({ request }) => {
  // Seed 100 sản phẩm vào test DB — tốn 3s
  await seedProducts(request);
});

test('add to cart', async ({ page }) => { /* ... */ });
test('remove from cart', async ({ page }) => { /* ... */ });
// ... 48 test nữa

Ưu điểm

  • Balanced hơn về số lượng test per shard.
  • File giant (100 test) không còn làm một shard "ngốn" toàn bộ duration.

Nhược điểm

  • Test phải stateless và isolated — không dùng shared mutable state giữa các test trong file.
  • beforeAll overhead nhân lên nếu nhiều shard chứa test từ cùng file.
  • Cân bằng số lượng test không đồng nghĩa cân bằng duration nếu test có duration chênh lệch lớn.
5

Default Behavior Theo fullyParallel

Config Unit phân phối vào shard File có thể split?
fullyParallel: false (default) File Không
fullyParallel: true Test case
describe.configure({ mode: 'serial' }) Describe block (giữ nguyên trên 1 shard) Không (trong block đó)
describe.configure({ mode: 'parallel' }) Test case trong describe Có (trong block đó)

Khi fullyParallel: false nhưng có một số file dùng describe.configure({ mode: 'parallel' }), các test trong describe đó vẫn chạy song song trong worker nhưng toàn bộ file vẫn nằm trên cùng một shard.

fullyParallel quyết định xem shard có thể tách test từ cùng file ra không, còn workers quyết định số luồng song song trong shard đó. Đây là hai chiều độc lập nhau.

6

Các Kịch Bản Gây Imbalance

Kịch bản 1: File count vs test count không cân

File-level shard phân phối theo file, không theo test count. 4 file có [10, 50, 30, 20] test với 4 shard → mỗi shard 1 file nhưng test count chênh lệch 5x.

Kịch bản 2: Một test slow dominate

Ngay cả khi test-level shard chia đều số lượng, nếu một test tốn 60s (ví dụ: upload file 100MB, chờ email verification) trong khi phần còn lại mỗi test ~2s, shard chứa test đó sẽ chậm hơn nhiều.

test('upload 100MB file and verify', async ({ page }) => {
  // Test này tốn ~60s
  await page.setInputFiles('#upload', '/fixtures/large-video.mp4');
  await page.click('[data-testid="submit-upload"]');
  await expect(page.locator('.upload-success')).toBeVisible({ timeout: 70_000 });
});

Kịch bản 3: Setup overhead tích lũy

Với test-level shard, nếu shard nhận nhiều test từ nhiều file khác nhau, beforeAll chạy cho mỗi file có mặt → nhiều lần init. Shard có 5 file khác nhau với beforeAll tốn 3s mỗi file = 15s overhead thuần túy.

Kịch bản 4: repeat-each nhân bội test count

Khi dùng --repeat-each=10 để detect flaky, 110 test thành 1100 lần chạy. Một test slow (60s) giờ thành 10 lần × 60s = 600s. Shard chứa test đó bị dominate hoàn toàn.

Kịch bản 5: Số file ít hơn số shard

3 file, 4 shard với file-level shard → shard 4 không có file nào, machine đó boot lên rồi thoát ngay. Overhead khởi động CI runner mà không chạy test.

7

Chẩn Đoán Shard Chậm

Cách 1: HTML report sau merge

Sau khi merge blob reports từ các shard (npx playwright merge-reports), mở HTML report và filter theo duration. Tab "Tests" sort theo "Duration" descending sẽ hiển thị test nào chiếm thời gian nhiều nhất. Tab "Files" cho thấy file nào tốn thời gian tổng cao nhất.

Cách 2: CI log duration per shard

Trong GitHub Actions, mỗi shard job có thời gian chạy riêng hiển thị trong summary. Xem cột "Duration" của từng job trong matrix:

# .github/workflows/playwright.yml
jobs:
  test:
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - name: Run tests (shard ${{ matrix.shard }}/4)
        run: npx playwright test --shard=${{ matrix.shard }}/4
        # Thời gian step này hiện trong Actions UI → so sánh giữa các shard

Cách 3: JSON reporter per shard

Thêm JSON reporter song song với blob reporter để xem breakdown per shard mà không cần merge:

// playwright.config.ts
export default {
  reporter: [
    ['blob'],
    ['json', { outputFile: `test-results/shard-${process.env.SHARD_INDEX}.json` }],
  ],
};

Parse JSON output để tính tổng duration của shard đó:

# Tính total duration từ JSON report
node -e "
  const r = require('./test-results/shard-1.json');
  const total = r.suites.reduce((s, suite) => s + suite.specs.reduce((s2, spec) => s2 + spec.tests[0].results[0].duration, 0), 0);
  console.log('Shard 1 total:', total, 'ms');
"

Cách 4: --list trước khi chạy

Dùng --list kết hợp với --shard để xem test nào rơi vào shard nào mà không thực sự chạy:

npx playwright test --list --shard=2/4
# Output: danh sách test sẽ chạy trên shard 2
# Đếm số test và ước lượng duration từ lần chạy trước
8

Chiến Lược Cân Bằng Thực Tế

Pattern 1: Equalize file size

File lớn với >50 test là nguyên nhân phổ biến nhất của imbalance. Refactor thành nhiều file nhỏ 10–20 test mỗi file:

tests/
  checkout/
    checkout-cart.spec.ts      # 15 test — add/remove cart
    checkout-payment.spec.ts   # 18 test — payment methods
    checkout-confirmation.spec.ts  # 12 test — order confirmation
# Thay vì: tests/checkout.spec.ts  # 50 test

Khi file nhỏ hơn, file-level shard phân phối đều hơn. Thêm nữa, từng file có beforeAll riêng scope hẹp hơn → init nhanh hơn.

Pattern 2: Group test theo expected duration

Nhóm các test nhanh vào một file, test chậm vào file khác, rồi dùng số shard phù hợp với mỗi nhóm:

tests/
  fast/           # test <3s mỗi cái
    ui-smoke.spec.ts
    form-validation.spec.ts
  slow/           # test >10s mỗi cái
    upload-large.spec.ts
    email-flow.spec.ts

Pattern 3: Bật fullyParallel cho file có nhiều test độc lập

// tests/search.spec.ts — 30 test, tất cả stateless
import { test } from '@playwright/test';
test.describe.configure({ mode: 'parallel' });

test('search by keyword', async ({ page }) => { /* ... */ });
test('search by category', async ({ page }) => { /* ... */ });
// ... 28 test nữa
// → 30 test này có thể split qua nhiều shard dù global fullyParallel: false

Pattern 4: Không shard test flaky riêng lẻ

Nếu một test thường xuyên cần retry và tốn 30s mỗi lần thử, đưa nó vào một project riêng và chạy job riêng. Tách khỏi main shard pipeline:

// playwright.config.ts
export default {
  projects: [
    {
      name: 'main',
      testMatch: /(?<!flaky)\.spec\.ts$/,
    },
    {
      name: 'flaky-isolated',
      testMatch: /\.flaky\.spec\.ts$/,
      retries: 3,
      workers: 1,  // Chạy tuần tự để dễ debug
    },
  ],
};
9

Slow Test — Tách Project Riêng

Với test tốn thời gian lớn (upload file, end-to-end email flow, video processing), giải pháp bền vững nhất là tách thành project riêng và shard riêng với tỉ lệ phù hợp.

// playwright.config.ts
export default {
  projects: [
    {
      name: 'fast',
      testMatch: /tests\/fast\/.+\.spec\.ts$/,
      // 100 test nhỏ — dùng 4 shard
    },
    {
      name: 'slow',
      testMatch: /tests\/slow\/.+\.spec\.ts$/,
      // 10 test nặng — dùng 2 shard, mỗi shard 5 test
    },
  ],
};

CI workflow với shard riêng cho từng project:

# .github/workflows/playwright.yml
jobs:
  test-fast:
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - run: npx playwright test --project=fast --shard=${{ matrix.shard }}/4

  test-slow:
    strategy:
      matrix:
        shard: [1, 2]
    steps:
      - run: npx playwright test --project=slow --shard=${{ matrix.shard }}/2

Kết quả: pipeline fast chạy ~25 test/shard × 4 shard song song; pipeline slow chạy 5 test/shard × 2 shard song song. Cả hai pipeline chạy song song với nhau trên CI.

10

Giới Hạn Built-in Vs Third-Party

Playwright built-in sharding dùng static split: phân phối theo thứ tự tại thời điểm bắt đầu, không có thông tin về duration từ lần chạy trước.

Tính năng Playwright built-in Knapsack / Test Reactor
Cơ chế split Static round-robin Dynamic — dựa historical duration
Thông tin duration lịch sử Không Có (từ DB/API)
Cân bằng theo thời gian chạy Không đảm bảo Mục tiêu chính
Tự adjust khi thêm test Không Có (re-profile)
Chi phí Miễn phí Có phí (SaaS)
Setup complexity Zero Cần tích hợp API/SDK

Knapsack Pro (knapsackpro.com) thu thập duration của từng test từ lần chạy trước, lưu vào server, rồi khi CI trigger lần tiếp theo, nó distribute test vào shard sao cho tổng duration của mỗi shard bằng nhau. Phù hợp với suite lớn (>500 test) có duration chênh lệch đáng kể.

Với suite nhỏ hơn hoặc test có duration tương đối đồng đều, Playwright built-in kết hợp các pattern cân bằng thủ công từ bài này thường đủ dùng mà không cần third-party.

11

Công Thức Chọn Số Shard

Không có con số tuyệt đối. Một số guideline thực tế:

Overhead per shard: Khởi động CI runner + cài dependencies + khởi động browser tốn khoảng 30–90s tùy môi trường. Nếu mỗi shard chỉ chạy 2–3 test tốn 10s → overhead lớn hơn test duration → không có lợi.

Quy tắc ngón tay cái: Số shard nên ≤ tổng test / 10. Với 100 test → tối đa 10 shard. Với 30 test → 3 shard là hợp lý.

Tổng test Shard hợp lý Target per shard
20–50210–25 test
50–200412–50 test
200–5004–825–125 test
>5008–1630–65 test

Khi thêm test mới vào suite, số shard cũng cần review. Hardcode số shard cụ thể vào workflow mà không xem lại khi suite grow là một dạng technical debt thầm lặng.

# Kiểm tra số test hiện tại trước khi quyết định shard count
npx playwright test --list | wc -l
# Trừ đi header lines để ra số test thực
12

Pitfalls

Pitfall 1: Suite quá nhỏ với quá nhiều shard

20 test chạy trên 8 shard → mỗi shard 2-3 test. Overhead runner (60s) > test duration (20s). Tổng thời gian thực tế tăng do overhead, không giảm. Dùng tối đa 2 shard cho suite nhỏ.

Pitfall 2: Một file giant thống trị shard

File 200 test với file-level shard sẽ chiếm một shard gần như hoàn toàn. Không thể fix bằng cách tăng shard count — shard đó vẫn có 200 test. Giải pháp duy nhất là refactor file hoặc bật fullyParallel.

Pitfall 3: fullyParallel: false với file bị split kỳ vọng

Đặt shard count cao với kỳ vọng file lớn sẽ được split, nhưng quên bật fullyParallel: true → file vẫn nằm nguyên trên 1 shard, các shard khác gần như trống.

// Sai: kỳ vọng file split nhưng fullyParallel: false
export default { /* fullyParallel không set */ };
// Đúng:
export default { fullyParallel: true };

Pitfall 4: Hardcode test count trong assertions

Một số team viết test kiểm tra số lượng item trong danh sách dựa trên seed data. Khi test-level shard chạy chỉ một phần test từ file, nếu beforeAll seed đầy đủ nhưng test kiểm tra count toàn bộ → có thể fail hoặc pass sai. Đảm bảo mỗi test assert trạng thái của chính nó, không phụ thuộc vào test khác trong file.

Pitfall 5: Không review shard count sau khi suite thay đổi đáng kể

Suite tăng từ 100 lên 400 test nhưng vẫn giữ 4 shard → mỗi shard 100 test thay vì 25. Duration mỗi shard tăng 4x. Tệ hơn là khi một shard có test chậm, imbalance trở nên nghiêm trọng hơn với suite lớn hơn. Review shard count định kỳ hoặc khi suite tăng >50%.

13

Quiz

Câu 1. Bạn có 3 file test với 5, 5, 50 test tương ứng. Chạy --shard=x/3 với fullyParallel: false. Shard nào sẽ chậm nhất và tại sao không thể fix bằng cách tăng số shard lên 6?

Xem đáp án

Shard nhận file 50 test sẽ chậm nhất. Tăng lên 6 shard cũng không giúp — file 50 test vẫn nằm nguyên trên 1 shard vì fullyParallel: false giữ toàn bộ file trên cùng shard. Giải pháp là bật fullyParallel: true hoặc tách file 50 test thành nhiều file nhỏ.

Câu 2. Suite có 200 test. Với fullyParallel: true và 4 shard, mỗi shard nhận ~50 test. Tuy nhiên shard 3 luôn chậm hơn 3 shard còn lại 2x. Đâu là nguyên nhân có thể và cách chẩn đoán?

Xem đáp án

Nguyên nhân có thể: (1) shard 3 nhận một test slow đặc biệt (upload file, email flow), (2) shard 3 nhận nhiều test từ nhiều file có beforeAll nặng, (3) test trong shard 3 có nhiều page.waitForTimeout ẩn. Chẩn đoán: mở HTML report sau merge, sort test theo duration descending, kiểm tra xem test nào >10s. Hoặc dùng --list --shard=3/4 để xem danh sách, rồi tìm test slow trong danh sách đó.

Câu 3. beforeAll trong một file seed database tốn 5s. File có 20 test. Với fullyParallel: true và 4 shard, beforeAll chạy bao nhiêu lần? Với fullyParallel: false?

Xem đáp án

Với fullyParallel: true: 20 test phân phối ra 4 shard → 5 test/shard. beforeAll chạy trên mỗi shard có test từ file đó = 4 lần × 5s = 20s overhead. Với fullyParallel: false: toàn bộ 20 test ở 1 shard → beforeAll chạy 1 lần × 5s = 5s overhead. Trade-off: test-level shard cân bằng hơn về test count nhưng tăng setup overhead.

Câu 4. Khi nào nên cân nhắc Knapsack Pro thay vì Playwright built-in sharding?

Xem đáp án

Khi suite có >500 test với duration chênh lệch lớn giữa các test (một số test 60s+, phần lớn <5s), và bạn đã thử các pattern thủ công (tách file, group theo duration) nhưng vẫn có shard chậm hơn 1.5x–2x shard còn lại một cách nhất quán. Knapsack dùng historical duration để đảm bảo các shard kết thúc cùng lúc, nhưng đi kèm chi phí và phụ thuộc vào external service.

14

Bài Tiếp Theo

Bài 74: Retries — Cấu Hình retries: N — mở đầu nhóm A.8 Retries & Flaky: cấu hình số lần retry, phân biệt passed / flaky / failed, và khi nào retry che giấu vấn đề thay vì giải quyết nó.