Tính con số rate limit — đo capacity, Little's law, headroom, percentile, burst, limit theo chi phí
"Đặt 60 request/phút được không?" là câu hỏi không có đáp án nếu thiếu hai con số: hệ thống chịu được bao nhiêu và người dùng thật cần bao nhiêu. Bài này đi cả hai chiều trên shop của ví dụ: chiều từ trên xuống — đo sức chịu tải thật của php-fpm rồi chọn giới hạn toàn hệ thống; chiều từ dưới lên — đọc log để biết khách thật, đối tác, bot gửi bao nhiêu, rồi chọn giới hạn theo user và theo route. Mọi con số có lab; phần dữ liệu log là dữ liệu giả lập (đánh dấu rõ ở mục 3.4).
Thuật ngữ dùng trong bài
capacity (sức chịu tải: số request/giây hệ thống xử lý được mà độ trễ còn chấp nhận được) · throughput / goodput (số request xử lý xong mỗi giây / phần trong đó thành công và kịp trước khi client bỏ đi) · Little's law (L = λ × W: số việc đang làm = tốc độ đến × thời gian ở lại) · utilization (phần trăm thời gian tài nguyên bận) · knee (điểm gãy: tải vượt qua đó thì độ trễ tăng vọt) · headroom (khoảng dư chừa lại dưới capacity) · percentile (p99 = 99% giá trị nhỏ hơn hoặc bằng nó) · open-loop / closed-loop load test (bắn tải theo lịch cố định bất kể server trả lời nhanh hay chậm / mỗi user ảo chờ response rồi mới gửi tiếp) · coordinated omission (công cụ đo tự chậm lại theo server nên không đo được thời gian chờ thật) · cost-based limit (mỗi request trừ ngân sách theo chi phí của nó, không phải 1 request = 1).
Kiểm chứng: AWS Builders' Library "Using load shedding to avoid overload" (David Yanacek); Google SRE Book ch. 21 "Handling Overload"; Stripe 2017; GitHub Docs (secondary rate limits, điểm theo loại request); Laravel docs 13.x (RateLimiter::increment(..., amount:)). Lab chạy thật: nginx 1.31.3 + php-fpm 8.3 (php:8.3-fpm-alpine,pm=static, 4 worker) + bộ bắn tải mở viết bằng Node 22, chạy 3 lần (out/04-capacity.run1.txt,run2.txt,run3.txt); câu SQL percentile chạy trên PostgreSQL 18.4 (out/04-percentile.txt); kịch bản burst từ lab bài 2. Máy lab 4 vCPU dùng chung — đúng lý do phần 3.1 phải chạy nhiều lần và so sánh.
Trả lời 30 giây
Hai chiều. Từ trên xuống: load test kiểu mở để tìm điểm gãy thật (lab: php-fpm 4 worker × 50ms, lý thuyết 80 rps theo Little's law, thật ~76–78), rồi đặt giới hạn toàn hệ thống ở 60–80% con số đó — vì sát điểm gãy độ trễ dao động mạnh (cùng 76 rps, lần đo này p50 55ms, lần khác 889ms) và vượt qua là sập: 120 rps làm goodput rơi còn ~19/giây, php-fpm làm phí 2 000 request cho client đã bỏ đi. Giới hạn theo tốc độ cố định vỡ khi backend chậm đi (DB chậm gấp đôi → goodput 7/giây), nên lớp toàn hệ thống nên là concurrency limit — Little's law tự co theo độ trễ (lab: vẫn 38/giây, không phí request nào). Từ dưới lên: đọc log, lấy p99/p99.9 số request mỗi user mỗi phút, đặt ngưỡng cao hơn hành vi thật nhưng thấp hơn bot (dữ liệu giả lập: 120/phút chặn cả 3 bot ~300/phút, chỉ chạm 1 khách thật 1 lần); burst lấy từ số request song song của một lần mở trang; route đắt thì trừ theo chi phí.
1. Bài toán gốc và bối cảnh ra đời
Con số rate limit thường được chọn bằng ba cách tệ: chép từ blog/đối thủ, đoán theo cảm giác, hoặc "để cao cho chắc". Cả ba đều thiếu một trong hai vế:
- Vế hệ thống: tổng lượng request được phép không được vượt sức chịu tải — nếu không, giới hạn chỉ là trang trí.
- Vế người dùng: giới hạn của từng key phải cao hơn hành vi thật — nếu không, chính giới hạn là sự cố (chặn khách đang mua hàng).
Khoảng giữa hai vế là nơi phân biệt khách thật với kẻ lạm dụng. Hai vế lại đo bằng hai công cụ khác nhau: load test cho vế hệ thống, log thật cho vế người dùng. Google SRE còn nhắc một điều nữa: "modeling capacity as 'queries per second' … often makes for a poor metric" — request khác nhau tốn tài nguyên khác nhau, nên con số tốt nhất đôi khi không tính bằng request (mục 3.7).
2. Mô hình tư duy cốt lõi
- Little's law:
số request đang xử lý = tốc độ đến × thời gian xử lý. Với php-fpm có 4 worker, mỗi request chiếm 1 worker 50ms:capacity = 4 ÷ 0,05s = 80 request/giây. Hệ quả quan trọng nhất: capacity tính bằng request/giây thay đổi ngay khi thời gian xử lý đổi — DB chậm thành 100ms là còn 40/giây, dù số worker không đổi. - Hai vế: giới hạn toàn hệ thống ≤ capacity đo được × hệ số an toàn; giới hạn mỗi key ≥ hành vi thật ở p99.9 × biên. Nếu tổng các giới hạn mỗi key lớn hơn capacity (gần như luôn vậy), vế hệ thống phải có lớp riêng.
3. Internals và quyết định thiết kế — từng bước tính
flowchart TD A["1. Load test kiểu mở
tìm điểm gãy thật"] --> B["2. Giới hạn toàn hệ thống
60–80% điểm gãy,
ưu tiên concurrency limit"] C["3. Đọc log thật
p99, p99.9 request/user/phút"] --> D["4. Giới hạn theo user, theo route
trên hành vi thật, dưới bot"] D --> E["5. Burst
từ số request song song
của 1 thao tác thật"] E --> F["6. Route đắt:
trừ theo chi phí"] B --> G["7. Kiểm tra chéo:
đỉnh traffic thật sau khi chặn bot
có dưới giới hạn toàn hệ thống?"] F --> G G --> H["8. Shadow mode,
rồi mới bật thật (bài 6)"]
3.1 Đo capacity bằng load test kiểu mở
Dựng một backend nhỏ có capacity biết trước: nginx → php-fpm pm=static, 4 worker; mỗi request usleep(50ms) (giả lập một câu query) rồi ghi một dòng
vào file để đếm số request php-fpm thực sự đã làm. Bộ bắn tải gửi đúng N request/giây theo lịch, mỗi request timeout 2 giây (như app mobile hay proxy phía
trước). Không dùng công cụ kiểu đóng (mỗi user ảo chờ response rồi mới gửi tiếp): khi server chậm, công cụ tự chậm theo, hàng đợi không bao giờ dài ra trong
số đo — traffic thật từ Internet không lịch sự như vậy.
// labs/rate-limit/node/loadgen.mjs — phần lõi
for (let i = 0; i < rps * secs; i++) {
const wait = t0 + (i * 1000) / rps - performance.now(); // lịch gửi cố định, KHÔNG chờ request trước
if (wait > 1) await sleep(wait);
inflight.push(fire()); // fire(): http.get + timeout 2s → 'timeout' nếu quá hạn
}
| Tải gửi vào | Lần 1: goodput · p50 | Lần 2: goodput · p50 | Lần 3 (máy rảnh): goodput · p50 | php-fpm làm phí (1 / 2 / 3) |
|---|---|---|---|---|
| 40 rps (0,5×) | 40,0/s · 56ms | 40,0/s · 58ms | 40,0/s · 54ms | 0 / 0 / 0 |
| 60 rps | 60,0/s · 54ms | 60,0/s · 54ms | 60,0/s · 54ms | 0 / 0 / 0 |
| 70 rps | 70,0/s · 56ms | 70,0/s · 600ms | 70,0/s · 53ms | 0 / 0 / 0 |
| 76 rps | 76,0/s · 55ms | 76,0/s · 889ms, tăng dần | 76,0/s · 111ms (p99 1 634ms) | 0 / 0 / 0 |
| 80 rps (1×) | 12,1/s · sập sau 4 giây | 77,8/s · p50 tăng 185 → 1 951ms, bắt đầu timeout | 80,0/s · p50 tăng 163 → 1 374ms | 1 359 / 43 / 0 |
| 120 rps (1,5×) | 19,9/s | 19,4/s | 17,7/s | 2 002 / 2 012 / 2 046 |
| 160 rps (2×) | 12,4/s | 13,8/s | 13,9/s | 2 952 / 2 924 / 2 921 |
Đọc bảng: (1) trần thật ~76–78 rps, thấp hơn lý thuyết 80 vì mỗi request còn tốn vài mili-giây cho nginx, FastCGI, PHP; (2) vượt trần là sập chứ không phải chậm đều — 1,5× tải mà goodput chỉ còn 1/4; (3) vùng sát trần dao động mạnh giữa các lần đo: cùng 70–76 rps, lần 1 và 3 êm, lần 2 p50 lên 600–889ms; ở 80 rps cả ba lần đều thấy hàng đợi dài dần, chỉ khác nhau ở lúc chạm timeout (lần 1 sau 4 giây, lần 2 ở giây 18, lần 3 chưa tới trong 20 giây nhưng p50 đã lên 1 374ms). Máy lab dùng chung với các việc khác; production cũng vậy — node bên cạnh, cache lạnh sau deploy, GC, backup chạy đêm. Vì thế không bao giờ đặt giới hạn bằng điểm gãy.
3.2 Chọn trần toàn hệ thống: chừa headroom
Rate limit toàn hệ thống 70 request/giây (≈ 90% trần đo được) đặt ở nginx với key hằng số, bắn 160 rps:
lần 1: rate 70/s, 160 rps (2×) | gửi 3200 | 200: 1415, 429: 1785 | goodput 70.8/s | 200: p50 284ms p99 494ms | làm phí 0
lần 2: rate 70/s, 160 rps (2×) | gửi 3200 | 200: 1418, 429: 1782 | goodput 70.9/s | 200: p50 83ms p99 277ms | làm phí 0
lần 3: rate 70/s, 160 rps (2×) | gửi 3200 | 200: 1419, 429: 1781 | goodput 71.0/s | 200: p50 54ms p99 251ms | làm phí 0
lần 2: rate 60/s, 160 rps (2×) | gửi 3200 | 200: 1046, 429: 2010, timeout: 144 | goodput 52.3/s | p99 2230ms | làm phí 144
lần 3: rate 60/s, 160 rps (2×) | gửi 3200 | 200: 1218, 429: 1982 | goodput 60.9/s | 200: p50 56ms p99 268ms | làm phí 0
- Phần vượt nhận 429 ngay (rẻ, chưa chạm PHP); phần được nhận phục vụ trong vài trăm mili-giây. Goodput giữ ~71/giây thay vì rơi còn 13/giây.
- Nhưng p50 lần 1 = 284ms (gấp 5,7 lần thời gian xử lý 50ms) — 90% utilization vẫn là sát quá: hàng đợi nhỏ hình thành và rút rất chậm (chuỗi 2 giây trong file output: 449ms ở đầu, phải tới giây 16 mới về 58ms).
- Ca 60/s của lần 2 còn tệ hơn ca 70/s ở 6 giây cuối (p50 lên 968 → 2 021ms, 144 timeout), trong khi lần 3 — máy rảnh — cùng cấu hình chạy êm (60,9/giây, p50 56ms, không phí request nào). Ngưỡng thấp hơn mà tệ hơn chỉ có một cách giải thích: capacity thật tụt xuống dưới 60 trong khoảng đó (máy dùng chung — load trung bình 5 phút ~4,3 trên 4 vCPU ngay sau lần 2). Một giới hạn tốc độ cố định không biết capacity đang tụt — cùng cơ chế với ca DB chậm ở mục 3.3.
Quy tắc thực dụng: trần toàn hệ thống ≈ 60–80% điểm gãy đo được ở điều kiện xấu (cache lạnh, giờ cao điểm), và đo lại sau mỗi thay đổi lớn. Lý
thuyết hàng đợi giải thích vì sao không lên cao hơn: thời gian chờ tăng theo kiểu ρ / (1 − ρ) — ở 50% utilization chờ ≈ 1 lần thời gian xử lý, 75% ≈ 3
lần, 90% ≈ 9 lần (mô hình M/M/1, chỉ để thấy hình dạng đường cong).
3.3 Khi capacity đổi: rate limit cố định vỡ, concurrency limit thì không
DB chậm đi: mỗi request 100ms → trần thật ~40 rps
lần 1: rate 70/s, 80 rps, xử lý 100ms | goodput 7.3/s | timeout 1188 | php-fpm làm phí 1188
lần 2: rate 70/s, 80 rps, xử lý 100ms | goodput 6.8/s | timeout 1280 | php-fpm làm phí 1280
lần 3: rate 70/s, 80 rps, xử lý 100ms | goodput 7.4/s | timeout 1270 | php-fpm làm phí 1270
lần 1: conn 8, 80 rps, xử lý 100ms | goodput 38.8/s | 429: 825 | p50 196ms p99 287ms | làm phí 0
lần 2: conn 8, 80 rps, xử lý 100ms | goodput 37.5/s | 429: 851 | p50 201ms p99 349ms | làm phí 0
lần 3: conn 8, 80 rps, xử lý 100ms | goodput 35.3/s | 429: 895 | p50 208ms p99 609ms | làm phí 0
lần 1: conn 8, 160 rps, xử lý 50ms | goodput 76.0/s | 429: 1681 | p50 100ms p99 133ms | làm phí 0
lần 2: conn 8, 160 rps, xử lý 50ms | goodput 71.5/s | 429: 1770 | p50 104ms p99 226ms | làm phí 0
lần 3: conn 8, 160 rps, xử lý 50ms | goodput 74.8/s | 429: 1705 | p50 102ms p99 152ms | làm phí 0
Cùng một dòng cấu hình nginx limit_conn global_conn 8; (4 đang chạy trên php-fpm + tối đa 4 chờ) cho goodput đúng bằng capacity thật trong cả hai trường
hợp, không phí request nào, và p50 ≈ 2 × thời gian xử lý (một request chờ tối đa 4 request trước nó trên 4 worker). Đây là Little's law làm việc: giới hạn
L (số request trong hệ thống) thì λ tự bằng L ÷ W. Chọn L = số worker + một hàng đợi ngắn; hàng đợi càng dài thì càng nhận được burst nhưng thời gian
chờ tối đa càng lớn (L ÷ số worker × thời gian xử lý).
# nginx — giới hạn toàn hệ thống theo số request đồng thời (lab: nginx/04-capacity.conf)
limit_conn_zone $server_name zone=global_conn:1m;
server {
server_name shop.example; # BẮT BUỘC: không có server_name thì $server_name rỗng → nginx bỏ qua → không giới hạn gì
location / {
limit_conn global_conn 8;
limit_conn_status 429;
fastcgi_pass php-fpm:9000;
}
}
Bẫy của concurrency limit: (1) nó bảo vệ hệ thống, không phân biệt client — một khách tham lam vẫn chiếm hết 8 chỗ; vẫn cần rate limit theo key cho
công bằng; (2) đặt ở nginx thì đếm kết nối tới nginx, không biết request nào đắt (Amazon dùng max_conns của proxy "as a last resort instead of the
default protection option" và để server tự shed với thông tin cục bộ chính xác hơn); (3) request dài (upload, report) giữ chỗ lâu — tách location riêng.
Bước tiếp theo là concurrency limit tự điều chỉnh theo độ trễ (kiểu AIMD — tăng dần khi êm, giảm mạnh khi chậm), chưa dựng lab trong series này.
3.4 Từ dưới lên: giới hạn theo user từ percentile của log
Dữ liệu giả lập. 3 giờ traffic tối của một shop: 2 000 khách thường (số request/phút theo phân phối lệch phải — đa số vài request, ít người
rất nhiều), 20 đối tác/power user gửi đều ~20/phút, 3 bot cào giá ~300/phút. Tổng 599 933 request, sinh bằng SQL (labs/rate-limit/sql/04-percentile.sql).
Con số cụ thể là minh hoạ; phương pháp là thứ dùng được cho log thật.
WITH um AS (SELECT user_id, date_trunc('minute', ts) AS m, count(*) AS n
FROM access_log GROUP BY 1, 2) -- số request mỗi user mỗi phút
SELECT nhom, count(*) AS user_phut,
percentile_disc(0.5) WITHIN GROUP (ORDER BY n) AS p50,
percentile_disc(0.99) WITHIN GROUP (ORDER BY n) AS p99,
percentile_disc(0.999) WITHIN GROUP (ORDER BY n) AS p999,
max(n)
FROM um … GROUP BY nhom;
nhom | user_phut | p50 | p99 | p999 | max
-----------------------+-----------+-----+-----+------+-----
20 đối tác/power user | 3225 | 20 | 50 | 72 | 83
2000 khách thường | 90164 | 3 | 22 | 43 | 147
3 bot cào giá | 540 | 298 | 348 | 349 | 349
Thử từng ngưỡng trên chính log đó — câu hỏi đúng không phải "p99 là bao nhiêu" mà là "đặt X thì ai bị chặn, bao nhiêu request thật bị từ chối":
limit/phút | khách bị chặn ≥1 lần | đối tác bị chặn | bot bị chặn | % request thật bị từ chối
------------+----------------------+-----------------+-------------+---------------------------
20 | 842 | 20 | 3 | 5.634
30 | 307 | 20 | 3 | 1.857
60 | 29 | 8 | 3 | 0.154
120 | 1 | 0 | 3 | 0.006
240 | 0 | 0 | 3 | 0.000
- 120/phút/user chặn cả 3 bot mà chỉ chạm 1 khách thật một lần (0,006% request thật). Đặt 20/phút "cho chắc" thì 842 khách bị chặn ít nhất một lần — chính giới hạn trở thành sự cố.
- Đối tác không dùng chung ngưỡng với khách lẻ: p99.9 của họ là 72, max 83 — cho họ giới hạn riêng theo API key và gói cước (bài 5), đừng nâng ngưỡng chung lên vì họ.
- Max của khách thường là 147 — một người dùng có thật gửi 147 request trong một phút (mở nhiều tab, app đồng bộ lại). Quyết định chặn hay không là quyết định sản phẩm, không phải kỹ thuật; ghi vào ADR.
3.5 Theo route
route | p99 | p999 | max (khách thường, request/user/phút)
----------------+-----+------+-----
GET /product | 12 | 24 | 71
GET /search | 8 | 15 | 33
POST /cart | 6 | 11 | 31
POST /checkout | 4 | 7 | 13
Route đắt hoặc nhạy cảm có hành vi thật thấp hơn nhiều so với trang sản phẩm → được phép đặt chặt hơn: vd tìm kiếm 30/phút, thêm giỏ 30/phút, thanh toán 10/phút (khoảng 2× p99.9, có burst). Thanh toán mà ai đó gửi 50 lần/phút thì đó là thử thẻ (card testing), không phải khách.
3.6 Burst: từ một thao tác thật
Token bucket cho phép trong mọi khoảng T tối đa b + r·T request (bài 2). Chọn r từ percentile (mục 3.4), chọn
b từ số request song song của một thao tác thật. Lab bài 2 (trang mở ra bắn 12 request cùng lúc, giới hạn "10/giây"):
token bucket b=1, 10/giây | qua 1/12 mỗi lần mở trang
token bucket b=20, 10/giây | qua 12/12 mỗi lần mở trang
Ví dụ storefront: r = 2/giây (120/phút), b = 30. Khách mở trang bắn 12 request: qua hết. Bot cào giá 5 request/giây: dùng hết 30 token đầu
trong ~10 giây, sau đó chỉ còn 2/giây — tức 120/phút, đúng ngưỡng bot bị chặn ở mục 3.4.
3.7 Request không bằng nhau: giới hạn theo chi phí
Một request tìm kiếm full-text có thể tốn gấp 10 lần một request xem sản phẩm đã cache. Đếm request thì kẻ tấn công chọn route đắt nhất. Cách làm: gán trọng số cho route theo chi phí đo được, mỗi request trừ đúng số đó.
| Route (ví dụ minh hoạ) | Thời gian xử lý p50 | Trọng số |
|---|---|---|
| GET /product (có cache) | 20ms | 1 |
| GET /search | 120ms | 6 |
| POST /checkout | 300ms (gọi cổng thanh toán) | 15 |
- Bên ngoài đã làm vậy: GitHub REST tính điểm (GET 1, POST/PATCH/PUT/DELETE 5), giới hạn phụ 900 điểm/phút; GraphQL thì gần như bắt buộc (xem bài bảo mật GraphQL — 1 request chứa 10 000 lần thử OTP). Google SRE đề xuất giới hạn khách theo CPU-giây: "Gmail is allowed to consume up to 4,000 CPU seconds per second".
- Laravel:
RateLimiter::increment($key, amount: $cost)có sẵn — nhưng ghép vớitooManyAttempts()lại là check-then-act (bài 3). Với thao tác nhạy cảm, dùngINCRBYtheo chi phí trong một Lua script. - Trọng số phải được đo lại: thêm index làm search rẻ đi 5 lần thì trọng số cũ phạt oan khách. Gắn việc cập nhật trọng số với dashboard độ trễ theo route.
3.8 Kiểm tra chéo hai vế
Toàn hệ thống (dữ liệu giả lập), request mỗi giây:
TB/giây | p99/giây | đỉnh/giây | TB/giây (bỏ bot) | đỉnh/giây (bỏ bot)
55.5 | 74 | 87 | 40.6 | 69
Ghép với lab capacity: đỉnh 87 rps > trần ~76 của một máy php-fpm 4 worker → nếu không có gì chặn bot, mỗi đợt dồn là một lần chạm vùng sập ở mục 3.1. Bỏ bot (giới hạn 120/phút/user ở mục 3.4 làm việc đó) thì đỉnh còn 69 — dưới trần nhưng chỉ chừa ~10% headroom, tức là vẫn nên thêm worker hoặc máy trước mùa khuyến mãi. Và tổng giới hạn theo user không bao giờ là "trần": 2 000 khách × 120/phút = 4 000 request/giây "được phép" trên giấy — lớp toàn hệ thống (mục 3.2, 3.3) mới là thứ giữ hệ thống sống.
4. Đảm bảo và giới hạn
- Little's law đúng cho trung bình ở trạng thái ổn định, không mô tả từng đợt dồn ngắn; đủ để tính capacity và chọn concurrency limit, không đủ để dự đoán p99.
- Capacity không phải hằng số: đổi theo tỉ lệ cache hit, trạng thái DB, request mix, máy hàng xóm (hai lần lab khác nhau ở vùng sát trần).
- Percentile chỉ mô tả quá khứ: tính năng mới (app mobile đồng bộ nền, trang có thêm widget) đổi hành vi — đo lại sau mỗi release lớn.
- Thêm worker không tăng capacity tuyến tính: tới lúc DB, Redis hay khoá dòng thành nút cổ chai (Amdahl's law: phần không song song hoá được quyết định trần).
- Giới hạn ở nginx là của một instance: 3 nginx sau LB thì
rate=70r/sthành 210 (bài 5). Concurrency limit theo instance thì lại tự đúng khi scale, vì mỗi instance có worker riêng.
5. Failure modes
| Sai lầm | Hậu quả đo được | Phát hiện |
|---|---|---|
| Không có lớp toàn hệ thống | 1,5× tải → goodput ~19/giây, 2 000+ request làm phí | p99 tăng vọt, 499/504 ở nginx, worker bận 100% |
| Đặt trần sát điểm gãy (90%) | p50 284ms (5,7× thời gian xử lý), hồi phục chậm | Độ trễ tăng dù không có 5xx |
| Rate limit cố định khi backend chậm đi | Goodput 6,8–7,3/giây, ~1 200 request làm phí | Độ trễ DB tăng trước, 429 không tăng |
| Ngưỡng quá thấp | 20/phút chặn 842 khách thật (dữ liệu giả lập) | Ticket "không mua được hàng", tỉ lệ 429 ở user đã đăng nhập |
| Burst quá nhỏ | 1/12 request của một lần mở trang qua | 429 ngay lần tải trang đầu |
| Load test kiểu đóng | Không thấy hàng đợi, không thấy sập | Kết quả load test đẹp, production sập ở tải thấp hơn |
| Quên retry | Mỗi request bị chặn quay lại 3–5 lần (bài 6: ×5,2) | Tổng request tăng khi bắt đầu chặn |
6. Vận hành trong production
- Load test kiểu mở chạy định kỳ (mỗi release lớn, trước mùa khuyến mãi), trên môi trường giống production, với dữ liệu và cache giống production; đẩy tải tới quá điểm gãy — AWS: "if they haven't load tested their service to the point where it breaks, and far beyond the point where it breaks, they should assume that the service will fail in the least desirable way possible."
- Dashboard hai vế: utilization so với trần toàn hệ thống; phân phối request/user/phút so với ngưỡng (mấy % key đang ở trên 80% ngưỡng).
- Đo việc làm phí: số request backend xử lý xong nhưng client đã đóng kết nối (nginx log status 499) — chỉ số sớm nhất của vòng luẩn quẩn.
- Autoscaling: giới hạn toàn hệ thống cố định ở LB không tăng theo số instance; concurrency limit theo instance thì tăng tự nhiên. Ghi rõ trong runbook giới hạn nào cần sửa tay khi scale.
- Đổi con số = deploy có kiểm soát: chạy shadow (tính nhưng không chặn) với ngưỡng mới, so số key sẽ bị chặn, rồi mới bật (bài 6).
7. So sánh và ma trận quyết định
| Cách chọn con số | Ưu | Nhược | Dùng khi |
|---|---|---|---|
| Chép từ blog/đối thủ | Nhanh | Không gắn với hệ thống của mình | Không bao giờ — tối đa làm điểm khởi đầu cho shadow mode |
| Từ trên xuống (capacity) | Bảo vệ được hệ thống | Không công bằng giữa client | Giới hạn toàn hệ thống |
| Từ dưới lên (percentile) | Ít chặn oan | Không bảo vệ được hệ thống (tổng quá lớn) | Giới hạn theo user, route |
| Theo chi phí | Chặn đúng thứ đắt | Phải đo và duy trì trọng số | Route chênh lệch chi phí lớn, GraphQL, API đối tác |
| Concurrency limit | Tự co theo độ trễ | Không phân biệt client | Lớp bảo vệ backend |
| Tự điều chỉnh (AIMD) | Không cần đoán | Khó hiểu, khó debug, dễ dao động | Hệ thống lớn có đội platform |
Kết luận: dùng cả hai chiều — concurrency limit (hoặc rate limit 60–80% capacity) cho vế hệ thống, percentile cho vế người dùng, trọng số cho route đắt. Tránh một con số duy nhất cho mọi thứ, và tránh mọi con số không đi kèm cách nó được tính.
8. Góc nhìn tech lead: tác động tổ chức
- ADR phải có phép tính, không chỉ kết quả: "capacity đo ngày X: 76 rps/instance (load test mở, file Y); trần 55 rps/instance (72%); ngưỡng user 120/phút = trên p99.9 khách thật (43) và dưới bot (~300); đối tác theo gói". Năm sau người khác đọc vẫn biết khi nào phải tính lại.
- Ai sở hữu con số: đội platform giữ vế hệ thống (gắn với hạ tầng, autoscaling); đội sản phẩm giữ vế người dùng (gắn với hành vi, gói cước). Hai bên cùng ký vào ADR.
- Hợp đồng với đối tác: công bố giới hạn thấp hơn mức hạ tầng chịu được (để còn headroom), ghi rõ định nghĩa (bài 2), hứa trước thời gian báo trước khi đổi — GitHub thông báo thay đổi API trước nhiều tháng.
- Đưa load test kiểu mở vào quy trình release lớn — chi phí nhỏ hơn nhiều một lần sập giữa đợt khuyến mãi.
9. Anti-patterns và bài học thực tế
- Con số không có nguồn gốc ("60/phút vì Laravel mặc định vậy").
- Đặt trần bằng 100% capacity — lab: vùng sát trần là vùng sập.
- Chỉ có giới hạn theo user, coi tổng của chúng là bảo vệ hệ thống.
- Một ngưỡng cho mọi route — search và checkout bị đối xử như trang sản phẩm.
- Load test kiểu đóng rồi tin kết quả.
- Hạ ngưỡng chung vì một đối tác, thay vì cho đối tác giới hạn riêng theo API key.
- Quên phần làm phí: request đã vào hàng đợi vẫn được xử lý sau khi client bỏ đi (lab: tới 2 952 request). Amazon khuyên giới hạn cả thời gian một request được nằm trong hàng đợi: "we throw it out if it's too old".
10. Câu hỏi phụ interviewer hay hỏi tiếp
- Tính capacity thế nào?
- Little's law cho con số lý thuyết (số worker ÷ thời gian xử lý), load test kiểu mở cho con số thật (lab: 80 lý thuyết, ~76–78 thật), đo ở điều kiện xấu.
- Đặt giới hạn toàn hệ thống bao nhiêu phần trăm capacity?
- 60–80%. Sát hơn thì độ trễ dao động mạnh (lab 90%: p50 284ms với thời gian xử lý 50ms) và capacity tụt một chút là sập.
- Rate limit hay concurrency limit để bảo vệ backend?
- Concurrency limit — tự co theo độ trễ. Lab DB chậm gấp đôi: rate limit cố định goodput 7/giây, concurrency limit 38/giây. Rate limit vẫn cần cho công bằng giữa client.
- Giới hạn theo user lấy từ đâu?
- Log thật: phân phối request/user/phút, thử từng ngưỡng xem ai bị chặn. Đặt trên hành vi thật (p99.9 + biên), dưới hành vi lạm dụng; nhóm khác hành vi (đối tác) thì giới hạn riêng.
- Burst đặt bao nhiêu?
- Lớn hơn số request song song của một thao tác thật (một lần mở trang, một lần đồng bộ app).
- Request đắt rẻ khác nhau thì sao?
- Giới hạn theo chi phí: mỗi route một trọng số đo từ thời gian xử lý (GitHub: GET 1, POST 5), trừ đúng trọng số mỗi request.
- Sao load test của tôi đẹp mà production vẫn sập?
- Thường là load test kiểu đóng (tự chậm theo server), dữ liệu/cache không giống production, hoặc không đẩy tải qua điểm gãy.
Phụ lục: lab đã chạy
Môi trường: nginx 1.31.3 + php-fpm 8.3 (php:8.3-fpm-alpine), pm=static, 4 worker; bộ bắn tải mở node/loadgen.mjs (Node 22.23.3,
http.Agent keep-alive, timeout 2 giây); mỗi lượt 20 giây và dựng lại container cho sạch (request "chết" của lượt trước còn trong hàng đợi php-fpm sẽ làm hỏng lượt sau).
PostgreSQL 18.4 (container test có sẵn), bảng tmp_rl_access_log tự tạo rồi tự xoá. Máy 4 vCPU dùng chung.
cd labs/rate-limit
./04-capacity.sh # cả bộ → out/04-capacity.txt (3 lần đã chạy, lưu ở 04-capacity.run1.txt, run2.txt, run3.txt)
SUFFIX=.run4 ./04-capacity.sh # chạy thêm 1 lần, ghi file riêng
./04-capacity.sh conn # 1 phần: nolimit | rate | slowdb | conn
/var/www/chiennn/postgresql-learning/scripts/psql.sh < sql/04-percentile.sql # → out/04-percentile.txt
Chưa verify: concurrency limit tự điều chỉnh (AIMD, gradient); giới hạn theo chi phí trong Lua (INCRBY theo trọng số); capacity khi DB là nút
cổ chai thật (lab giả lập bằng usleep); số liệu log thật của shop (dữ liệu ở mục 3.4 là giả lập).