Mọi cuộc thảo luận về thiết kế hệ thống rồi cũng quy về hai con số: độ trễ và thông lượng. Nghe đơn giản, nhưng đây là chỗ sai lệch bắt đầu — vì phần lớn chúng ta được dạy tư duy tuyến tính, trong khi hệ thống có hàng đợi lại hành xử phi tuyến một cách tàn nhẫn.
Bài này thiết lập bốn nền tảng dùng lại suốt 17 bài sau: phân biệt latency với throughput, ghi nhớ bậc độ lớn của các loại độ trễ, hiểu vì sao giá trị trung bình là con số lừa dối, và cuối cùng là công thức giải thích tại sao một server "còn 30% CPU rảnh" vẫn có thể đang chết.
1.1 Latency và Throughput: hai đại lượng, không phải một
Latency là thời gian phục vụ một request, đo bằng đơn vị thời gian. Throughput là số request phục vụ được trong một đơn vị thời gian, đo bằng req/s. Chúng liên quan nhưng không suy ra được nhau, và điều quan trọng hơn cả: tối ưu cái này thường làm hại cái kia.
Ví dụ dễ thấy nhất là ghép lô (batching). Gom 100 lệnh ghi rồi đẩy xuống database một lần giúp throughput tăng vọt vì chi phí cố định mỗi lần ghi được chia đều. Nhưng request đến đầu tiên phải chờ 99 request kia đến đủ mới được xử lý — latency của nó tăng lên. Nén dữ liệu cũng vậy: tiết kiệm băng thông (tăng throughput trên cùng đường truyền) nhưng thêm thời gian nén/giải nén vào mỗi request.
| Kỹ thuật | Tác động lên throughput | Tác động lên latency |
|---|---|---|
| Batching | Tăng mạnh (chia đều chi phí cố định) | Xấu đi (phải chờ đủ lô) |
| Nén dữ liệu | Tăng (ít byte hơn trên cùng đường truyền) | Xấu đi (thêm thời gian nén/giải nén) |
| Cache | Tăng (bớt việc cho tầng sau) | Tốt lên (đường đi ngắn hơn) |
| Thêm replica | Tăng gần tuyến tính | Tốt lên gián tiếp (giảm chờ hàng đợi, không làm mỗi request nhanh hơn) |
Dòng cuối bảng chứa một ý tinh tế và rất hay bị hiểu sai: thêm server không làm một request đơn lẻ được xử lý nhanh hơn. Nếu handler mất 20 ms CPU thì nó vẫn mất 20 ms, dù bạn có 1 hay 100 server. Thêm server chỉ làm giảm thời gian xếp hàng. Khi hệ thống rảnh rỗi, thêm server hầu như không cải thiện latency chút nào.
1.2 Bậc độ lớn của latency: bảng số định hình mọi kiến trúc
Đây có lẽ là bảng số quan trọng nhất mà một người thiết kế hệ thống cần nằm lòng. Không phải để nhớ chính xác từng con số, mà để nhớ khoảng cách giữa chúng — vì chính khoảng cách đó quyết định kiến trúc.
Hãy đọc bảng này theo hướng ứng dụng. RAM nhanh hơn một round-trip liên lục địa khoảng 1,5 triệu lần. Điều đó giải thích ba quyết định kiến trúc kinh điển:
- Vì sao cache thắng gần như luôn luôn: đổi một truy vấn qua mạng (~1 ms) thành một lần đọc RAM (~100 ns) là tiết kiệm 10.000 lần. Không có tối ưu thuật toán nào cho tỉ lệ như vậy.
- Vì sao CDN tồn tại: khi người dùng ở cách server nửa vòng trái đất, 150 ms là giới hạn vật lý — sẽ nói rõ ở Bài 6. Cách duy nhất là đưa dữ liệu lại gần họ.
- Vì sao "N+1 query" là tội đồ: 100 truy vấn tuần tự × 1 ms = 100 ms, trong khi gộp thành một truy vấn có thể chỉ 3 ms. Vấn đề không nằm ở database mà ở số lần đi qua mạng.
1.3 Tail latency: vì sao giá trị trung bình là con số lừa dối
Phân phối latency của hệ thống thực gần như luôn lệch phải: phần lớn request nhanh, một phần nhỏ chậm hẳn, và cái đuôi dài đó kéo giá trị trung bình đi. Kết quả là trung bình không đại diện cho ai cả — không phải trải nghiệm điển hình, cũng không phải trải nghiệm tệ nhất.
Vì vậy ta dùng phân vị (percentile). Nói "p99 = 420 ms" nghĩa là 99% request nhanh hơn 420 ms, và 1% chậm hơn. Với một dịch vụ nhận 10 triệu request mỗi ngày, cái "1%" đó là 100.000 lần người dùng có trải nghiệm tệ — mỗi ngày.
Vì sao p99 của service lại thành p90 của người dùng
Đây là phần khiến tail latency nguy hiểm hơn nhiều so với cảm nhận ban đầu. Một trang web hiện đại thường phải gọi nhiều service để dựng xong một màn hình. Nếu mỗi lời gọi độc lập và xác suất một lời gọi bị chậm là $p$, thì xác suất người dùng gặp ít nhất một lời gọi chậm khi trang gọi $n$ service là:
$$P(\text{cham}) = 1 - (1 - p)^n$$
Trong đó $p$ là xác suất một lời gọi vượt ngưỡng chậm (ví dụ $p = 0{,}01$ ứng với ngưỡng p99), và $n$ là số service mà trang phải gọi. Thay số:
| Số service gọi ($n$) | Xác suất gặp ít nhất một lời gọi chậm | Nghĩa là |
|---|---|---|
| 1 | 1,0% | p99 của service = p99 của người dùng |
| 10 | 9,6% | p99 của service ≈ p90 của người dùng |
| 50 | 39,5% | gần 4/10 lượt tải trang bị ảnh hưởng |
| 100 | 63,4% | chậm trở thành trạng thái bình thường |
Đây chính là lý do các hệ thống lớn theo dõi tới p99,9 và tại sao việc chia nhỏ thành nhiều service làm bài toán tail latency khó hơn hẳn — một điểm ta sẽ tính bằng con số cụ thể ở Bài 15.
1.4 Định luật Little và lý thuyết hàng đợi
Giờ đến phần giải thích được hầu hết những gì "lạ" trong vận hành hệ thống. Bắt đầu bằng một đẳng thức đơn giản đến mức đáng ngạc nhiên — định luật Little:
$$L = \lambda W$$
Trong đó $L$ là số request trung bình đang nằm trong hệ thống, $\lambda$ (lambda) là tốc độ request đến (req/s), và $W$ là thời gian trung bình mỗi request nằm trong hệ thống (giây). Đẳng thức này đúng cho mọi hệ thống ổn định, không cần giả định gì về phân phối.
Ứng dụng trực tiếp: nếu dịch vụ của bạn nhận 500 req/s và mỗi request mất trung bình 40 ms, thì số request đang xử lý đồng thời là $L = 500 \times 0{,}04 = 20$. Đó chính là con số bạn cần để chọn kích thước connection pool hoặc số worker — không phải đoán.
Utilization và cái vách phi tuyến
Gọi $\mu$ (mu) là tốc độ phục vụ tối đa của server (req/s). Mức chiếm dụng là:
$$\rho = \frac{\lambda}{\mu}$$
Để đi xa hơn, ta cần một mô hình hàng đợi cụ thể. Đơn giản nhất là M/M/1, và ba ký hiệu đó đọc từ trái sang phải là: request đến ngẫu nhiên, thời gian phục vụ ngẫu nhiên, và có một server.
Chữ M thứ hai — thời gian phục vụ. "Phân phối exponential" nghĩa là phần lớn request xử lý nhanh, một số ít lâu hơn hẳn, và không có trần trên. Đây chính là hình dạng lệch phải ở mục 1.3.
Số 1 — có một server. Đúng một luồng phục vụ. Thêm server thành M/M/c, và bài 3 sẽ đo xem thêm vào giúp được đến đâu.
Hệ thống thật hiếm khi khớp hoàn hảo cả ba giả định. Nhưng M/M/1 vẫn đáng học vì nó cho ra đúng hình dạng của đường cong — cái vách dựng đứng khi ρ tiến tới 1 — và hình dạng đó mới là thứ trực giác tuyến tính của chúng ta liên tục bỏ sót.
Với mô hình đó, thời gian trung bình một request nằm trong hệ là:
$$W = \frac{1}{\mu - \lambda}$$
Hãy để ý mẫu số. Khi $\lambda$ tiến tới $\mu$, mẫu số tiến tới 0 và $W$ tiến tới vô cùng. Đây không phải sự cố — đây là hành vi bình thường của mọi hàng đợi.
Các mốc trên hình là thời gian chờ trong hàng đợi so với thời gian phục vụ, tức $\rho/(1-\rho)$ — không phải tổng thời gian trong hệ $W$. Hai đại lượng này chênh nhau đúng 1: ở $\rho = 0{,}7$, chờ trong hàng đợi gấp 2,3 lần thời gian phục vụ, còn tổng thời gian trong hệ gấp 3,3 lần lúc rảnh.
Con số cụ thể cho thời gian chờ trong hàng đợi (không tính thời gian phục vụ):
| Mức chiếm dụng ρ | Thời gian chờ so với lúc rảnh | Cảm nhận vận hành |
|---|---|---|
| 0,50 | ×1 | Thoải mái |
| 0,70 | ×2,3 | "CPU mới 70%, còn dư mà" — nhưng p99 đã xấu rõ |
| 0,90 | ×9 | Rung lắc, sự cố nhỏ thành sự cố lớn |
| 0,95 | ×19 | Một đợt burst nhẹ là sập |
| 0,99 | ×99 | Về mặt thực tế là đã chết |
Và xin đừng "giải quyết" bằng cách tăng độ dài hàng đợi. Hàng đợi dài hơn không tăng $\mu$ — nó chỉ đổi hình thức thất bại từ từ chối nhanh sang chờ rồi timeout, thường là tệ hơn cho người dùng. Bài 13 sẽ nói vì sao trả 429 ngay lại tử tế hơn.
Thấy nó xảy ra: Traffic Lab một node
Đọc bảng số là một chuyện, thấy đường cong dựng đứng dưới tay mình là chuyện khác. Demo dưới đây chạy một bộ mô phỏng sự kiện rời rạc thật (không phải animation vẽ sẵn): kéo thanh tải và quan sát p99 cùng độ dài hàng đợi. Đường xanh là lý thuyết $W = 1/(\mu-\lambda)$, các điểm vàng là số mô phỏng đo được — hai thứ này phải khớp nhau, và đó cũng là cách engine của series được kiểm chứng.
Mở Traffic Lab ở tab riêng →
Mã nguồn: tính phân vị và mô phỏng M/M/1
Hai hàm dưới đây là mã thật đang chạy trong engine của series. Hàm percentile dùng phương pháp "nearest-rank" — cùng cách mà các công cụ đo tải phổ biến báo cáo, nên số của bạn so sánh được với số của chúng.
// Tinh phan vi theo phuong phap nearest-rank (khong noi suy).
// Luu y: KHONG sua doi mang dau vao - da tung co bug vi ham sort() tai cho.
function percentile(values, p) {
if (!values || values.length === 0) return 0;
const sorted = [...values].sort((a, b) => a - b);
if (p <= 0) return sorted[0];
if (p >= 100) return sorted[sorted.length - 1];
const rank = Math.ceil((p / 100) * sorted.length);
return sorted[Math.min(sorted.length - 1, Math.max(0, rank - 1))];
}
// Ly thuyet hang doi M/M/1. Tra ve null khi rho >= 1: hang doi tang vo han,
// KHONG co trang thai on dinh - dung tra ve mot con so gia o day.
function mm1Theory(lambda, mu) {
const rho = lambda / mu;
if (rho >= 1) return { rho, W: null, Wq: null, L: null, stable: false };
const W = 1 / (mu - lambda); // tong thoi gian trong he (giay)
const Wq = rho / (mu - lambda); // rieng thoi gian CHO trong hang doi
const L = lambda * W; // dinh luat Little
return { rho, W, Wq, L, stable: true };
}
// Doi chieu nhanh: mu = 50 req/s (moi request 20ms)
for (const lambda of [25, 35, 45, 49]) {
const t = mm1Theory(lambda, 50);
console.log(`rho=${t.rho.toFixed(2)} W=${(t.W * 1000).toFixed(1)}ms L=${t.L.toFixed(2)}`);
}
// rho=0.50 W=40.0ms L=1.00
// rho=0.70 W=66.7ms L=2.33
// rho=0.90 W=200.0ms L=9.00
// rho=0.98 W=1000.0ms L=49.00 <-- chi tang lambda tu 45 len 49 ma W gap 5 lan
Bốn dòng kết quả cuối là toàn bộ bài học của mục này gói lại: tăng tải từ 45 lên 49 req/s — chỉ 9% — làm thời gian trong hệ tăng từ 200 ms lên 1000 ms, tức 5 lần.
node sysdesign-engine-selftest.mjs — 42
mục kiểm chứng, không cần cài thêm gì. Toàn bộ số hiệu năng trong series đến từ máy đo thật hoặc từ bộ mô phỏng đã kiểm chứng. Không có con số nào được bịa, và các bài có lab đều ghi rõ cấu hình máy đo.
1.5 Ước lượng back-of-envelope
Kỹ năng cuối của bài: từ một yêu cầu bằng lời, ra được các con số để bắt đầu thiết kế. Đây là bước phải làm trước khi vẽ bất cứ sơ đồ nào, vì nếu không thì mọi lựa chọn về sau chỉ là phỏng đoán.
Làm mẫu với một bài toán cụ thể: 10 triệu người dùng hoạt động hằng ngày (DAU).
# 1) DAU -> RPS trung binh
10.000.000 DAU x 20 request/nguoi/ngay = 200.000.000 request/ngay
200.000.000 / 86.400 giay = ~2.315 req/s (trung binh)
# 2) He so dinh: luu luong KHONG deu trong ngay
# Thuc te thuong dat 2-5 lan trung binh vao gio cao diem.
2.315 x 3 = ~7.000 req/s (dinh)
# 3) Ti le doc/ghi: dinh hinh toan bo kien truc
# Gia dinh 100:1 (mang xa hoi, tin tuc, thuong mai dien tu deu quanh muc nay)
doc: ~6.930 req/s -> cache + read replica giai quyet duoc (Bai 5, Bai 7)
ghi: ~70 req/s -> mot primary con thua suc, CHUA can sharding (Bai 8)
# 4) Dung luong
# Moi request ghi ~2 KB payload
70 req/s x 2 KB x 86.400 = ~12 GB/ngay
= ~4,4 TB/nam -> can nghi ve archival
# 5) Bang thong ra
# Moi response doc ~10 KB
6.930 req/s x 10 KB = ~69 MB/s = ~550 Mbps -> CDN (Bai 6)
Năm bước đó mất chưa tới hai phút nhưng đã loại bỏ được rất nhiều lựa chọn sai. Đáng chú ý nhất: với 70 ghi/giây, sharding là không cần thiết — một quyết định tiết kiệm hàng tháng công sức và vô số phức tạp vĩnh viễn. Nếu bỏ qua bước ước lượng, rất dễ bắt đầu bằng một kiến trúc phân tán cho một bài toán mà một server đơn giải quyết được.
1. Quên hệ số đỉnh. Thiết kế cho 2.315 req/s rồi giờ cao điểm nhận 7.000 req/s — hệ thống chạy ở ρ ≈ 3, tức là không có trạng thái ổn định nào cả.
2. Quên độ lệch đọc/ghi. Nhìn con số tổng 7.000 req/s rồi kết luận "phải sharding ngay", trong khi thực tế chỉ có 70 ghi/giây. Tối ưu sai tầng, trả giá phức tạp mà không được lợi ích.
Còn một câu hỏi chưa trả lời
Toàn bộ bài này xoay quanh $\mu$ — tốc độ phục vụ tối đa. Bảng số, đường cong, mọi ước lượng đều dựa vào nó. Nhưng ta chưa hề nói $\mu$ của server bạn thực tế là bao nhiêu.
Con số đó không đọc được từ tài liệu và không đoán được từ cấu hình máy. Phải đo. Bài 2 sẽ dựng một app Node thật trong Docker, chạy tải tăng dần, và tìm điểm mà throughput ngừng tăng nhưng độ trễ bùng nổ — cái vách bạn vừa thấy trong lý thuyết, lần này bằng số đo của chính máy bạn. Trên đường đi, ta cũng sẽ thấy một handler viết sai kiểu có thể làm throughput sụp 195 lần.
📖 Tài liệu tham khảo
- Dean & Barroso — "The Tail at Scale" (bài gốc về tail latency trong hệ phân tán)
- Brendan Gregg — The USE Method (phương pháp phân tích utilization/saturation/errors)
- Interactive Latency Numbers — bảng bậc độ lớn latency thay đổi theo từng năm
- Gil Tene — "How NOT to Measure Latency" (nguồn gốc khái niệm coordinated omission)
Tải mã nguồn thực hành
Lõi mô phỏng dùng cho mọi demo trong series, kèm bộ kiểm chứng 42 mục chạy được bằng Node (không cần cài
thêm gì). Bộ kiểm chứng import cả hai file còn lại, nên phải tải
đủ cả ba vào cùng một thư mục rồi chạy node sysdesign-engine-selftest.mjs:
Bình luận