Ràng buộc và ước lượng: bao nhiêu lượt gửi tin mập mờ?
Bảng ràng buộc gửi OTP, tin giao dịch, tin khuyến mãi; tự tính lượt gửi mỗi giây, lượt timeout, giá tin mất so với tin trùng, rồi đo tốc độ ghi job trên Postgres.
TL;DR: Case thông báo gửi ba loại tin qua nhà cung cấp ngoài: OTP (mã xác thực dùng một lần) 200 lượt mỗi giây, tin giao dịch 1.000 lượt mỗi giây và năm triệu tin khuyến mãi trong 15 phút, tức khoảng 5.556 lượt mỗi giây. Timeout 2 giây cắt đúng phần đuôi 0,5% độ trễ của nhà cung cấp, nên mỗi đợt khuyến mãi sinh khoảng 25.000 lượt gửi mập mờ, không biết tin đã tới hay chưa. Phía đắt hơn đổi chiều theo loại: OTP mất đắt gấp khoảng 7 lần OTP trùng, còn tin khuyến mãi trùng đắt gấp 10 lần tin mất.
Tám giờ tối thứ Sáu, marketing bấm gửi khuyến mãi tới năm triệu người, đúng lúc người dùng vẫn chờ OTP và vừa thanh toán đơn. Sáng hôm sau, tổng đài nhận hai khiếu nại ngược chiều: "tôi nhận một tin khuyến mãi ba lần" và "tôi không nhận được mã OTP".
Chưa vội nghĩ tới hàng đợi hay khoá chống trùng. Bài này công bố bảng ràng buộc cả case dùng lại, biến nó thành ba con số (lượt gửi mỗi giây, lượt gửi mập mờ, tỉ lệ giá tin mất so với tin trùng), rồi so tải đỉnh với trần ghi job đo trên máy bạn. Chưa có thiết kế nào ở đây.
1. Lượt gửi mập mờ giống chuyện gì ngoài đời?
Bạn chuyển khoản 2 triệu, app quay vòng 30 giây rồi báo "giao dịch đang xử lý". Tiền đi chưa? Có thể lệnh chưa tới ngân hàng, có thể đã chuyển xong mà kết quả chưa về. Gửi thông báo qua nhà cung cấp ngoài (gọi tắt là NCC: dịch vụ SMS, dịch vụ push tức thông báo đẩy về app) gặp đúng tình cảnh đó, hàng chục lần mỗi giây.
| Chuyển khoản | Gửi thông báo |
|---|---|
| Lệnh chuyển 2 triệu | Một lượt gọi API của NCC để gửi một tin |
| App quay quá 30 giây | Hết timeout mà NCC chưa trả lời: lượt gửi mập mờ |
| Chuyển lại, có khi mất hai lần tiền | Thử lại, có khi người dùng nhận hai tin (tin trùng) |
| Bỏ qua, có khi người nhận không có tiền | Không thử lại, có khi người dùng không nhận tin nào (tin mất) |
| Ngân hàng không chặn lệnh chuyển lặp | NCC không chống trùng: gọi hai lần là gửi hai tin |
Mập mờ không phải thất bại mà là "không biết". Mọi cách xử lý nó đều chọn trước mình sẽ sai theo hướng nào.
Không nhận được trả lời thì bên gửi không phân biệt được request mất, response mất hay bên kia chỉ đang chậm; timeout chỉ là phỏng đoán. Bài này không giảng lại cơ chế đó mà đếm xem nó sinh bao nhiêu lượt "không biết" trong một đợt gửi.
2. Bảng ràng buộc: những con số cả case đứng trên
Bảng dưới là ràng buộc tự đặt, không phải số của công ty nào. Thiếu số thì mỗi người hiểu đề một kiểu. Các bài 02 đến 05 trích lại đúng bộ số này.
| Ràng buộc | Giá trị | Nghĩa là gì |
|---|---|---|
| OTP qua SMS | Đỉnh 200 lượt mỗi giây, tới trong 15 giây | Quá hạn thì người dùng bấm "gửi lại mã" hoặc bỏ đi |
| Tin giao dịch qua push | Đỉnh 1.000 lượt mỗi giây, tới trong 1 phút | Biến động số dư, xác nhận đơn |
| Tin khuyến mãi qua push | 5.000.000 tin trong 15 phút | Một đợt, bấm nút một lần |
| NCC chống trùng | Không | Chống trùng là việc của bạn |
| Độ trễ NCC | p50 150 ms, p99 1,2 giây, p99,5 2 giây, p99,9 4 giây | Đo phía bạn, từ lúc gọi tới lúc có trả lời |
| Timeout phía bạn | 2 giây | Quá ngưỡng thì lượt gọi không rõ kết quả |
p99 bằng 1,2 giây nghĩa là 99% lượt gọi trả lời nhanh hơn 1,2 giây. Độ trễ cố ý ghi bằng percentile, lý do nằm ở mục 4.
3. Mỗi giây phải gửi bao nhiêu tin?
Tính kiểu back-of-envelope (vài phép nhân chia đủ ra bậc độ lớn). Tin khuyến mãi: 5.000.000 tin chia 900 giây là khoảng 5.556 lượt mỗi giây. Cộng ba luồng, giả định đỉnh OTP và đỉnh giao dịch rơi đúng lúc đợt khuyến mãi chạy: 200 + 1.000 + 5.556 = khoảng 6.756 lượt mỗi giây. Giả định không bi quan quá: khuyến mãi kéo người dùng vào app đăng nhập rồi mua hàng.
Để ý: tin khuyến mãi chiếm hơn 80% lưu lượng nhưng ít gấp nhất, còn OTP chỉ chiếm 3% mà hạn chặt nhất.
Thử ngẫmnếu OTP và tin khuyến mãi chung một hàng đợi đến trước gửi trước, một OTP tạo ra lúc đợt gửi vừa bắt đầu phải xếp sau bao nhiêu tin?
4. Một đợt khuyến mãi sinh bao nhiêu lượt gửi mập mờ?
Một nửa lượt gọi NCC trả lời trong 150 ms, còn timeout là 2 giây, dài gấp hơn 13 lần. Bao nhiêu phần trăm lượt gọi bị timeout? Ghi ra một con số rồi mới đọc tiếp.
Lời giải: con số 150 ms không trả lời được câu này. Trung vị nói về giữa phân phối, còn timeout cắt vào đuôi. Bảng ghi p99,5 bằng 2 giây, tức 0,5% lượt gọi chưa xong khi hết timeout. Đó là tỉ lệ mập mờ: 0,5%. Đuôi của NCC dài tới 4 giây ở p99,9.

Percentile pN nghĩa là N% request nhanh hơn ngưỡng đó, và trung bình che mất cái đuôi. Ở đây mỗi lượt gọi nằm trong đuôi sau ngưỡng timeout đổi thành một lượt "không biết kết quả".
Với tỉ lệ 0,5%, mỗi giây có bao nhiêu lượt mập mờ của OTP (200 lượt), tin giao dịch (1.000 lượt), tin khuyến mãi (5.556 lượt)? Cả đợt năm triệu tin khuyến mãi sinh bao nhiêu lượt mập mờ? Tự nhân trước khi xem bảng.
| Loại | Lượt gửi | Lượt mập mờ (× 0,5%) |
|---|---|---|
| OTP | 200 mỗi giây | 1 mỗi giây |
| Tin giao dịch | 1.000 mỗi giây | 5 mỗi giây |
| Tin khuyến mãi | 5.556 mỗi giây | khoảng 28 mỗi giây |
| Cả đợt khuyến mãi | 5.000.000 tin | 25.000 lượt |
Khoảng 34 lượt mỗi giây nghe nhỏ, nhưng cả đợt là 25.000 người có thể nhận hai tin hoặc không nhận tin nào.
Thử ngẫmnếu đỉnh OTP tăng gấp đôi lên 400 lượt mỗi giây mà timeout vẫn 2 giây, dòng nào của bảng trên đổi và dòng nào giữ nguyên?
5. Một tin mất đắt hơn hay rẻ hơn một tin trùng?
Mập mờ buộc bạn chọn sai theo một hướng, nên phải gắn tiền cho từng hướng. Bảng giá dưới cũng tự đặt, kèm cách suy ra:
| Loại | Giá một tin mất | Giá một tin trùng | Phía đắt hơn |
|---|---|---|---|
| OTP | 5.000 đ: 10% người không nhận mã bỏ đơn lãi 50.000 đ | 700 đ: phí một SMS thừa | Mất, gấp khoảng 7 lần |
| Tin giao dịch | 3.000 đ: 20% người gọi tổng đài, mỗi cuộc 15.000 đ | 600 đ: 4% người tưởng bị trừ tiền hai lần, gọi tổng đài | Mất, gấp 5 lần |
| Tin khuyến mãi | 20 đ: 0,1% người nhận mua, mỗi người lãi 20.000 đ | 200 đ: 1% người tắt thông báo, mất 20.000 đ về sau | Trùng, gấp 10 lần |
Cột cuối là thứ bạn mang theo. Một tin khuyến mãi lỡ gửi không ai nhớ, còn ba tin giống hệt trong một phút đủ để người dùng tắt hẳn thông báo. Số tuyệt đối sẽ khác ở sản phẩm của bạn, chiều của từng hàng thì khó đổi.
Thử ngẫmbạn thêm loại tin nhắc lịch hẹn. Trong bảng giá trên, bạn phải tự điền những ô nào trước khi biết phía nào đắt hơn?
6. Hàng job trên Postgres có gánh nổi tải đỉnh không?
Mỗi tin thường được ghi thành một job (một dòng ghi "việc cần gửi", worker lấy ra gửi sau) trước khi gửi. Postgres ghi được bao nhiêu job mỗi giây? pgbench, công cụ benchmark đi kèm Postgres, mở nhiều kết nối chạy lặp một script SQL rồi báo số transaction mỗi giây (TPS) ở dòng tps =. Tạo database lab và bảng job:
-- createdb notilab, roi chay trong psql notilab
CREATE TABLE notification_jobs (
id bigserial PRIMARY KEY, kind text NOT NULL, user_id bigint NOT NULL,
status text NOT NULL DEFAULT 'pending',
created_at timestamptz NOT NULL DEFAULT now());
enqueue_one.sql ghi một job mỗi transaction; enqueue_batch.sql ghi 1.000 job trong một câu, tức một commit cho cả lô:
-- enqueue_one.sql
\set uid random(1, 5000000)
INSERT INTO notification_jobs (kind, user_id) VALUES ('promo', :uid);
-- enqueue_batch.sql
\set s random(1, 4999000)
INSERT INTO notification_jobs (kind, user_id)
SELECT 'promo', g FROM generate_series(:s, :s + 999) AS g;
pgbench -n -f enqueue_one.sql -c 8 -j 8 -T 10 notilab
pgbench -n -f enqueue_batch.sql -c 8 -j 8 -T 10 notilab
-c 8 là 8 kết nối, -T 10 chạy 10 giây, -n bỏ vacuum vì script không dùng bảng mặc định của pgbench. Script lô thì nhân TPS với 1.000 ra job mỗi giây. Số tác giả đo trên Apple M4, Postgres 16.14 trong Docker, ba lần chạy trên bảng vừa TRUNCATE:
| Cách ghi, 8 kết nối | Job mỗi giây, máy tác giả | Máy bạn |
|---|---|---|
| Từng job một commit | 1.527 đến 2.234, lấy cỡ 2.000 | ? |
| Lô 1.000 job một commit | 276.000 đến 466.000, lấy cỡ 300.000 | ? |
Các lần chạy lệch nhau tới khoảng 70% nên hãy bám tỉ lệ. Tải đỉnh 6.756 chia 2.000 là khoảng 3,4 lần: ghi từng job không theo kịp. Ghi lô thì trần cao hơn tải đỉnh hơn 40 lần, năm triệu job ghi xong trong khoảng 17 giây.

Vì sao lô nhanh hơn cả trăm lần? Mặc định Postgres chỉ báo COMMIT thành công sau khi WAL (write-ahead log, nhật ký Postgres đẩy xuống đĩa trước để không mất dữ liệu khi sập) của transaction đã flush xuống đĩa. Ghi từng job thì job nào cũng trả một lần chờ flush; ghi lô thì 1.000 job chia nhau một lần.
Tin khuyến mãi tạo từ danh sách người nhận nên ghi lô được; OTP và tin giao dịch cộng lại 1.200 lượt mỗi giây, vẫn dưới trần ghi từng job. Khâu ghi job chưa phải chỗ vỡ đầu tiên; chỗ khó là 34 lượt mập mờ mỗi giây phía NCC: thử lại hay bỏ qua, bài 02 và 03 sẽ trả lời.
7. Hai lỗi ước lượng hay gặp
❌ Nhầm 1: suy tỉ lệ timeout từ độ trễ trung bình. "Trung bình dưới 200 ms, timeout 2 giây thì không ai chạm."
✅ Tỉ lệ timeout là phần đuôi sau ngưỡng, chỉ percentile cao mới cho con số đó: ở đây 0,5%, tức 25.000 lượt mỗi đợt.
❌ Nhầm 2: tạo job khuyến mãi bằng vòng lặp, mỗi tin một câu tự commit.
-- autocommit: moi cau mot transaction, 5 trieu lan cho WAL flush
INSERT INTO notification_jobs (kind, user_id) VALUES ('promo', 1);
INSERT INTO notification_jobs (kind, user_id) VALUES ('promo', 2);
-- ... toi user_id = 5000000
✅ Lấy cả lô người nhận trong một câu, mỗi lô một commit:
INSERT INTO notification_jobs (kind, user_id)
SELECT 'promo', user_id FROM campaign_targets
WHERE campaign_id = 42 AND user_id BETWEEN 1 AND 1000;
Theo số đo mục 6, cách đầu mất hơn 40 phút chỉ để ghi job, lâu hơn cả đợt gửi; cách sau cỡ 17 giây.
Đào sâu: nguồn để tự kiểm số của mình
- pgbench (PostgreSQL 16): các cờ
-c,-j,-T,-f,-nvà hàmrandom()trong\set. - Populating a Database (PostgreSQL 16, mục 14.4): khuyên gom nhiều
INSERTvào một transaction thay vì commit từng dòng, và chỉ raCOPYcòn nhanh hơnINSERTtheo lô. - Asynchronous Commit (PostgreSQL 16): commit mặc định chờ WAL xuống đĩa.
Liên hệ các bài khác
- Bài 02: Gửi rồi đánh dấu: dùng bảng này để đếm tin trùng và tin mất của vòng gửi đơn giản nhất.
- Bài 03: Mất hay trùng theo loại: nhân tỉ lệ mập mờ của mục 4 với bảng giá mục 5 để chọn mức giao nhận.
- Bài 05: Cách ly luồng gửi: OTP chiếm 3% lưu lượng mà hạn 15 giây.
Tóm tắt
- Ước lượng tải hệ gửi thông báo theo khung giờ dồn tải (15 phút của đợt gửi), đừng chia theo cả ngày.
- Muốn biết bao nhiêu lượt gọi sẽ không rõ kết quả, đọc percentile của NCC tại đúng ngưỡng timeout; trung vị không trả lời được.
- Trước khi chọn thử lại hay bỏ qua, gắn giá cho cả tin mất lẫn tin trùng theo từng loại.
Tự kiểm tra
- Q1Vì sao bài tính tải tin khuyến mãi là khoảng 5.556 lượt mỗi giây mà không chia năm triệu tin cho cả ngày?
- Q2Đồng nghiệp nói: NCC trả lời trung bình 200 ms, timeout 2 giây gấp 10 lần nên gần như không có lượt gửi mập mờ. Lập luận sai ở đâu?
- Q3Kéo timeout từ 2 giây lên 4 giây (p99,9) thì số lượt mập mờ của đợt khuyến mãi đổi thế nào, và OTP trả giá gì?
- Q4Vì sao ghi lô 1.000 job một commit đạt cỡ 300.000 job mỗi giây trong khi ghi từng job một commit chỉ cỡ 2.000?
- Q5OTP mất đắt gấp khoảng 7 lần OTP trùng, còn tin khuyến mãi trùng đắt gấp 10 lần tin mất. Điều đó nói gì về cách xử lý lượt gửi mập mờ?
Bài tiếp theo: Gửi rồi đánh dấu: vì sao vòng gửi đơn giản vừa gửi trùng vừa đánh rơi tin
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Thông báo — tổng quan case không mất, không gửi trùng
Bài tiếpGửi rồi đánh dấu: vòng gửi vừa gửi trùng vừa đánh rơi tin
Thảo luận & góp ý
Hỏi đáp về bài này