Harbor

Nền tảng luyện code online

Trung bìnhChạy code không tin cậyHàng đợiBảng xếp hạngĐột biến tảiGiống LeetCode, Codeforces

Hiểu đề

Nền tảng luyện code online là gì? Nền tảng cho lập trình viên đọc đề, viết code trên trình duyệt, nộp bài và nhận kết quả chấm sau vài giây. Định kỳ có contest 90 phút với hàng chục nghìn người thi cùng lúc và bảng xếp hạng trực tiếp.

Yêu cầu chức năng

  1. Người dùng có thể xem danh sách bài (lọc theo độ khó, chủ đề) và đọc đề
  2. Người dùng có thể nộp code bằng nhiều ngôn ngữ và nhận kết quả AC/WA/TLE/MLE/RE/CE
  3. Người dùng có thể tham gia contest có giờ bắt đầu và kết thúc
  4. Người dùng có thể xem bảng xếp hạng contest gần như real-time và thứ hạng của mình

Yêu cầu phi chức năng

  • Kết quả chấm p95 < 5 s ngoài contest, < 15 s lúc cao điểm contest
  • Cô lập tuyệt đối: code người dùng không gọi được mạng, không đọc được test ẩn, không thoát khỏi sandbox
  • Không mất submission nào trong contest 500.000 người, kể cả khi judge bị lỗi
  • Bảng xếp hạng trễ tối đa 5 s; thời gian chấm đo ổn định, sai lệch < 10% giữa các lần chạy
  • Độ sẵn sàng 99,9% cho đọc đề và nộp bài

Ngoài phạm vi: Diễn đàn thảo luận, lời giải cộng đồng; Gói Premium và thanh toán; Phát hiện đạo code (chỉ nhắc như một consumer của sự kiện chấm); Gợi ý bằng AI, soạn code cùng nhau.

Thực thể và API

Problem
Đề bài: slug, tiêu đề, nội dung, độ khó, tag, giới hạn thời gian và bộ nhớ theo ngôn ngữ.
TestCase
Bộ test ẩn (input, output mong đợi). File nằm trên S3, judge tải và cache cục bộ, không bao giờ trả về client.
Submission
Một lần nộp: user, problem, contest (nếu có), ngôn ngữ, link code, thời điểm nộp, verdict, thời gian chạy, bộ nhớ.
Contest
Cuộc thi: danh sách bài, giờ bắt đầu, kết thúc, luật tính điểm (số bài, penalty).
ContestStanding
Kết quả một người trong contest: số bài AC, penalty, số lần sai mỗi bài. Nguồn sự thật để dựng lại bảng xếp hạng.
MethodEndpointGhi chú
GET/v1/problems?tag=&difficulty=&cursor=Danh sách bài, cache ở CDN.
GET/v1/problems/{slug}Đề bài và test ví dụ. Nội dung gần như tĩnh nên cache CDN.
POST/v1/problems/{slug}/submissionsBody {language, code, contestId?}. Trả 202 {submissionId} ngay, không chờ chấm.
GET/v1/submissions/{id}Trạng thái và kết quả. Client poll mỗi giây hoặc nhận đẩy qua WebSocket.
POST/v1/contests/{id}/registerĐăng ký thi trước giờ bắt đầu.
GET/v1/contests/{id}/leaderboard?page=Trang bảng xếp hạng từ snapshot trên CDN; /me trả thứ hạng riêng.

Thiết kế tổng thể

Đi lần lượt từng yêu cầu chức năng. Mục tiêu ở đây là chạy đúng, việc tối ưu để dành cho phần đào sâu.

1) Người dùng có thể xem danh sách bài và đọc đề

Đề bài gần như không đổi nên phục vụ qua CDN. API chỉ trả dữ liệu riêng của người dùng như bài nào đã AC.

  1. 1Client gọi GET /v1/problems/{slug} qua CDN
  2. 2CDN trả cache (TTL vài phút), hết hạn thì gọi API đọc Postgres
  3. 3Client gọi riêng một API nhỏ để lấy trạng thái đã giải của mình

Nguồn: Grokking Modern System Design · ch.11 Content Delivery Network (CDN) ↗ · Content Consistency in CDN · mục "Time-to-live (TTL)" ; Grokking Modern System Design · ch.11 CDN ↗ · mục "Pull CDN" ; Grokking Modern System Design · ch.16 Distributed Cache ↗ · Background of Distributed Cache · mục "Writing policies" (write-around: ghi vào cache sau lần trượt)

2) Người dùng có thể nộp code và nhận kết quả

Chấm bài mất 1-3 s và chạy code lạ nên không làm trong API. API lưu submission, đẩy vào queue rồi trả 202; judge worker chạy code trong sandbox và ghi kết quả.

  1. 1Client gửi POST /submissions, API kiểm tra rate limit và lưu code (S3) cùng metadata
  2. 2API đẩy {submissionId} vào queue (contest hoặc luyện tập) và trả 202
  3. 3Judge worker lấy job, tải test đã cache, biên dịch và chạy từng test trong sandbox có giới hạn CPU, RAM, thời gian, không có mạng
  4. 4Judge so output, ghi verdict, thời gian, bộ nhớ vào DB rồi xác nhận (ack) job
  5. 5Client poll GET /submissions/{id} hoặc nhận kết quả qua WebSocket

Nguồn: Grokking Modern System Design · ch.17 Distributed Messaging Queue ↗ · mục "Motivation" (Improved performance, Granular scalability) ; Grokking Modern System Design · ch.17 Distributed Messaging Queue ↗ · mục "Messaging queue use cases" (Post-processing) ; Grokking Modern System Design · ch.23 Distributed Task Scheduler ↗ · mục "Task idempotency" ; Grokking Modern System Design · ch.23 Distributed Task Scheduler ↗ · mục "Schedule and execute untrusted tasks" (sandbox bằng Docker hoặc máy ảo, giới hạn và dừng tác vụ vượt tài nguyên) ; Grokking Modern System Design · ch.19 Rate Limiter ↗ · mục "Where to place the rate limiter" ; Grokking Modern System Design · ch.36 WhatsApp ↗ · mục "Connection with a WebSocket server"

3) Người dùng có thể thi contest và xem bảng xếp hạng

Mỗi lần AC trong contest cập nhật điểm của người đó trong một Redis sorted set. Trang top được chụp thành snapshot đưa lên CDN, thứ hạng riêng đọc bằng ZREVRANK.

  1. 1Judge phát sự kiện verdict có contestId
  2. 2Leaderboard service tính lại điểm người đó (AC đầu tiên mỗi bài mới tính) và ghi ContestStanding vào DB
  3. 3ZADD contest:{id} score userId, với score = số bài × 10^7 − penalty (giây)
  4. 4Mỗi 2 s, job đọc ZREVRANGE top 1.000 và ghi file JSON lên CDN; client đọc file đó
  5. 5Thứ hạng riêng: GET /leaderboard/me → ZREVRANK

Nguồn: Redis in Action ↗ · ch.1 · §1.2.5 Sorted sets in Redis (tr. 13) ; Redis in Action ↗ · ch.3 · §3.5 Sorted sets (tr. 50) ; Grokking Modern System Design · ch.11 CDN ↗ · mục "Pull CDN" ; Chưa có nguồn trong thư viện: công thức score = số bài × 10^7 − penalty là thiết kế của khoá học

Hạ tầng theo quy mô

Cùng một sản phẩm, ba mức người dùng. Chọn từng mức để xem sơ đồ, phép tính và cấu hình server thay đổi thế nào.

Người dùng
10.000 DAU, 500 bài, contest hằng tuần 1.000 người
Tải đỉnh
≈ 3,3 submission/s phút cuối contest, ≈ 63 req/s API
Chi phí ước lượng
≈ 300–700 USD/tháng (ước lượng)

Trang luyện code của một trường hoặc cộng đồng nhỏ. Một API, một Postgres vừa lưu dữ liệu vừa làm hàng đợi (SKIP LOCKED), hai máy judge chạy Docker đã khóa chặt.

Người dùngTrình duyệtCDNCDNDịch vụAPI Node.jsDatabasePostgres + jobsWorkerJudge (Docker)Lưu trữS3 testHTTPScache miss, nộplưu + xếp jobSKIP LOCKEDtải test

Ước lượng

  • Nộp bài thường: 10.000 × 2 / 86.400 ≈ 0,23/s, đỉnh ×5 ≈ 1,2/s
  • Contest: 1.000 người × 6 lần nộp / 5.400 s ≈ 1,1/s, 15 phút cuối ×3 ≈ 3,3/s
  • Slot judge (Little's law): 3,3/s × 2 s mỗi lần chấm ≈ 6,7 slot; máy 8 vCPU cho 7 slot → ceil(6,7 / 7) = 1 → chạy 2 máy để dự phòng
  • API: 10.000 × 40 req / 86.400 ≈ 4,6 req/s, ×10 ≈ 46 req/s + poll 3,3 × 5 ≈ 17 → ≈ 63 req/s → 2 instance
  • Lưu trữ: 10.000 × 2 × 365 = 7,3 triệu submission/năm × 2 KB ≈ 14,6 GB/năm; test 500 bài × 20 MB = 10 GB trên S3

Nguồn con số: Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations ↗ · mục "Examples of Resource Estimation" (Number of servers required, Storage requirements, Bandwidth requirements) ; Quy tắc ước lượng của khoá học · /hoc/chon-server (1.000 req/s mỗi máy 2 vCPU, chạy tối đa 60% CPU, Redis ×1,3) ; Lý thuyết · Định luật Little: trong hệ thống ổn định, số yêu cầu đang xử lý trung bình L bằng tốc độ đến λ nhân thời gian ở lại trung bình W, tức L = λ × W. Ví dụ λ = 470 req/s và W = 0,08 s thì L = 470 × 0,08 ≈ 38 request cùng lúc; mỗi instance xử lý được c request đồng thời thì cần ít nhất L ÷ c instance, rồi chia thêm cho 0,6 để chừa chỗ cho đỉnh. Định luật đúng cho mọi hàng đợi ổn định, không phụ thuộc phân phối thời gian đến hay thời gian xử lý, nên dùng được để kiểm tra chéo kết quả ước lượng.

Bảng hạ tầng

Thành phầnChọnSố lượngCấu hìnhVì sao
CDNCloudflare1Cache đề bài, JS editorĐề gần như tĩnh, CDN đỡ phần đọc.
APINode.js (NestJS), Docker trên Cloud Run22 vCPU · 4 GB RAM63 req/s chỉ cần 1 instance, chạy 2 để dự phòng.
Database + queuePostgreSQL managed, bảng jobs dùng SELECT ... FOR UPDATE SKIP LOCKED12 vCPU · 8 GB · 50 GB SSD3 job/s là quá nhỏ để cần hệ thống queue riêng; bảng xếp hạng 1.000 người tính bằng SQL, cache 10 s.
JudgeVM chạy Docker (--network none, cgroups, seccomp, read-only)28 vCPU · 16 GB, 7 slot mỗi máy6,7 slot lúc đỉnh vừa 1 máy, máy thứ 2 để dự phòng; tách khỏi API và không có credential.
Lưu testS3 / GCS110 GB, judge cache ra đĩa localTest lớn không để trong DB; tải một lần rồi dùng lại.
Thay đổi so với mức trước
  • Điểm xuất phát: API, Postgres và máy judge tách riêng ngay từ đầu vì chạy code lạ
  • Postgres kiêm hàng đợi bằng SKIP LOCKED, chưa cần Redis hay SQS
  • Bảng xếp hạng là một câu SQL cache 10 s
Nút thắt tiếp theo

Contest vài chục nghìn người đẩy hàng trăm bài/s: bảng jobs trên Postgres bị tranh khóa, hai máy judge không đủ slot và câu SQL bảng xếp hạng quá nặng khi hàng nghìn người cùng xem.

Cấu hình và chi phí là ước lượng để học và trình bày trong phỏng vấn. Xem cách tính ở bài Chọn server.

So sánh nhanh ba mức

Thành phầnMức 1Mức 2Mức 3
Người dùng10.000 DAU, 500 bài, contest hằng tuần 1.000 người1 triệu DAU, 3.000 bài, contest 50.000 người10 triệu DAU; contest toàn cầu 500.000 người
Chi phí≈ 300–700 USD/tháng (ước lượng)≈ 8.000–20.000 USD/tháng (ước lượng, phụ thuộc số giờ contest)≈ 100.000–200.000 USD/tháng (ước lượng, phần lớn là máy judge)
CDN
Cloudflare
1 × Cache đề bài, JS editor
CloudFront / Cloudflare
1 × Đề bài, snapshot bảng xếp hạng TTL 2 s
Chưa cần
API
Node.js (NestJS), Docker trên Cloud Run
2 × 2 vCPU · 4 GB RAM
Node.js, container autoscale
2–9 (autoscale) × 2 vCPU · 4 GB RAM
Node.js trên Kubernetes, multi-AZ
83 pod (HPA) × 2 vCPU · 4 GB mỗi pod
Database + queue
PostgreSQL managed, bảng jobs dùng SELECT ... FOR UPDATE SKIP LOCKED
1 × 2 vCPU · 8 GB · 50 GB SSD
Chưa cầnChưa cần
Judge
VM chạy Docker (--network none, cgroups, seccomp, read-only)
2 × 8 vCPU · 16 GB, 7 slot mỗi máy
VM + gVisor (runsc), autoscale theo độ dài queue
17–28 (theo lịch contest) × 16 vCPU · 32 GB, 14 slot mỗi máy
Firecracker microVM trên bare-metal, pool ấm
27–44 máy (theo lịch contest) × 96 vCPU · 384 GB, 88 slot mỗi máy
Lưu test
S3 / GCS
1 × 10 GB, judge cache ra đĩa local
Chưa cầnChưa cần
QueueChưa cần
Amazon SQS, 3 hàng: contest, luyện tập, chạy thử
3 queue × Visibility timeout 60 s, dead-letter queue
SQS theo mức ưu tiên và theo ngôn ngữ
≈ 12 queue × Visibility timeout, dead-letter queue
RedisChưa cần
ElastiCache
1 primary + 1 replica × 2 vCPU · 4 GB
ElastiCache
1 primary + 2 replica × 4 vCPU · 8 GB
DatabaseChưa cần
PostgreSQL managed (HA)
1 primary + 1 read replica × 4 vCPU · 16 GB · 500 GB SSD
Aurora PostgreSQL, multi-AZ
1 primary + 2 replica × 8 vCPU · 32 GB
Lưu trữChưa cần
S3
1 × Test 3.000 bài × 20 MB = 60 GB; code 1,46 TB/năm
Chưa cần
CDN + WAFChưa cầnChưa cần
Cloudflare / CloudFront
1 × Đề bài, snapshot bảng xếp hạng TTL 2 s
Submission storeChưa cầnChưa cần
DynamoDB (PK user_id, SK submission_id) + S3 cho code
1 bảng × ≈ 1.900 ghi/s đỉnh, 1,46 TB/năm
KafkaChưa cầnChưa cần
MSK / Confluent
3 broker × Topic verdicts 6 partition, RF 3
Leaderboard serviceChưa cầnChưa cần
Node.js consumer, một consumer mỗi contest
3 pod × 2 vCPU · 4 GB mỗi pod

Đào sâu

Mỗi câu hỏi đi từ cách đơn giản đến cách người phỏng vấn mong đợi ở cấp senior.

1) Chạy code bất kỳ của người lạ thế nào cho an toàn?

Cách chưa tốt
child_process.exec ngay trên API server

Code người dùng chạy cùng máy, cùng quyền với API. Một dòng đọc biến môi trường là lộ key DB, một fork bomb là sập API, và code có thể gọi mạng ra ngoài.

Đánh đổi: Dựng nhanh nhất cho demo, không bao giờ dùng được cho người lạ.

Nguồn: Grokking Modern System Design · ch.23 Distributed Task Scheduler ↗ · mục "Schedule and execute untrusted tasks" (sandbox bằng Docker hoặc máy ảo, giới hạn và dừng tác vụ vượt tài nguyên) ; Chưa có nguồn trong thư viện: child_process.exec của Node.js ở mức cách ly (xem Node.js in Practice về child_process nhưng sách không nói về bảo mật chạy code lạ)

Cách tốt
Docker container riêng cho mỗi submission, trên máy judge tách biệt

Chạy với --network none, --memory, --cpus, --pids-limit, rootfs read-only, user không phải root, seccomp mặc định, giết sau thời gian tối đa. Máy judge ở subnet riêng, không giữ credential nào.

Đánh đổi: Container dùng chung kernel với host, một lỗ hổng kernel là thoát ra ngoài; tạo container mới cho mỗi lần chấm tốn vài trăm ms.

Nguồn: Grokking Modern System Design · ch.23 Distributed Task Scheduler ↗ · mục "Schedule and execute untrusted tasks" (sandbox bằng Docker hoặc máy ảo, giới hạn và dừng tác vụ vượt tài nguyên) ; Chưa có nguồn trong thư viện: các cờ --network none, --pids-limit, seccomp và cgroups cụ thể (không có sách về Docker/Kubernetes trong thư viện)

Cách tốt nhất
Thêm lớp cách ly kernel: gVisor hoặc Firecracker microVM, có pool khởi động sẵn

gVisor chặn syscall bằng một kernel chạy ở user space; Firecracker cho mỗi lần chạy một microVM KVM riêng, khởi động khoảng 125 ms. Giữ sẵn một pool sandbox ấm để không trả chi phí khởi động. Test ẩn mount read-only, việc so output làm bên ngoài sandbox nên code không thấy đáp án. Mỗi sandbox ghim 1 vCPU riêng và đo CPU time qua cgroup để kết quả TLE công bằng.

Đánh đổi: gVisor làm chương trình nhiều I/O chậm hơn nên phải chỉnh time limit theo ngôn ngữ; Firecracker cần máy bare-metal hoặc hỗ trợ ảo hóa lồng.

Nguồn: Grokking Modern System Design · ch.23 Distributed Task Scheduler ↗ · mục "Schedule and execute untrusted tasks" (sandbox bằng Docker hoặc máy ảo, giới hạn và dừng tác vụ vượt tài nguyên) (nêu máy ảo làm cách ly) ; Chưa có nguồn trong thư viện: gVisor và Firecracker không có sách trong thư viện, giữ ở mức khái niệm; con số 125 ms là của nhà cung cấp, không đưa vào để kiểm chứng

2) Phút cuối contest, hàng trăm submission mỗi giây đổ vào. Xếp hàng thế nào?

Cách chưa tốt
API gọi judge đồng bộ và giữ HTTP request đến khi chấm xong

Mỗi request chiếm connection 2-10 s. Khi judge đầy, request timeout, người dùng bấm nộp lại và tải tăng gấp đôi. API crash là mất submission.

Đánh đổi: Đơn giản nhưng sập đúng lúc quan trọng nhất.

Nguồn: Grokking Modern System Design · ch.17 Distributed Messaging Queue ↗ · mục "Motivation" (Improved performance, Granular scalability) · mục "Improved performance" (tách tác vụ chậm khỏi đường chính) ; Grokking Modern System Design · ch.4 Non-functional System Characteristics ↗ · mục "Reliability"

Cách tốt
Queue bền (SQS, BullMQ) + trả 202 + client poll

API lưu submission rồi mới đẩy job, judge kéo job theo sức của mình. Job có visibility timeout: judge chết giữa chừng thì job tự quay lại queue. Chấm theo submissionId nên chấm lại hai lần vẫn ra cùng kết quả (Grokking ch.17).

Đánh đổi: Không mất bài nhưng contest và luyện tập chung một hàng thì người thi phải chờ sau hàng nghìn bài luyện tập.

Nguồn: Grokking Modern System Design · ch.17 Distributed Messaging Queue ↗ · mục "Distributed messaging queue" và Back-end service (visibility_timeout, giao ít nhất một lần) ; Grokking Modern System Design · ch.23 Distributed Task Scheduler ↗ · mục "Task idempotency" ; Grokking Modern System Design · ch.17 Distributed Messaging Queue ↗ · mục "Motivation" (Improved performance, Granular scalability)

Cách tốt nhất
Queue theo mức ưu tiên + rate limit theo người + penalty tính theo giờ nộp

Ba hàng riêng: contest, luyện tập, chạy thử test ví dụ (nhẹ). Judge luôn lấy contest trước, giống phần ưu tiên tác vụ trong Grokking ch.23. Mỗi người tối đa 1 lần nộp mỗi 10 s cho một bài (token bucket, Grokking ch.19) để chặn spam. Penalty tính theo thời điểm API nhận bài chứ không theo lúc chấm xong, nên hàng đợi dài không làm ai thiệt. Kết quả đẩy về qua WebSocket thay cho poll.

Đánh đổi: Bài luyện tập có thể chờ vài phút trong giờ contest; cần theo dõi để hàng ưu tiên thấp không bị bỏ đói mãi.

Nguồn: Grokking Modern System Design · ch.23 Distributed Task Scheduler ↗ · mục "Prioritization" (hàng ưu tiên riêng) ; Grokking Modern System Design · ch.19 Rate Limiter ↗ · mục "Algorithms for rate limiting" (Token bucket algorithm) ; Grokking Modern System Design · ch.17 Distributed Messaging Queue ↗ · mục "Motivation" (Priority queue) ; Grokking Modern System Design · ch.36 WhatsApp ↗ · mục "Connection with a WebSocket server" ; Chưa có nguồn trong thư viện: penalty tính theo giờ nhận bài là quy tắc contest của khoá học

3) Bảng xếp hạng cho 500.000 người thi, cập nhật trong vài giây, làm thế nào?

Cách chưa tốt
GROUP BY và ORDER BY trên bảng submissions ở mỗi lần xem

Mỗi lần xem tính lại điểm cho mọi người từ hàng triệu submission. Với 50.000 lượt xem mỗi giây thì DB chết trước khi contest kết thúc.

Đánh đổi: Đúng và đơn giản cho contest vài trăm người.

Nguồn: Redis in Action ↗ · ch.1 · §1.2.5 Sorted sets in Redis (tr. 13) ; Redis in Action ↗ · ch.3 · §3.5 Sorted sets (tr. 50) ; Chưa có nguồn trong thư viện: GROUP BY/ORDER BY trên bảng lớn là ví dụ minh họa của khoá học

Cách tốt
Redis sorted set: ZADD khi AC, ZREVRANGE lấy top, ZREVRANK lấy hạng riêng

Sorted set giữ thứ tự sẵn nên đọc top hay hạng một người đều theo thứ tự đã giữ sẵn (Redis in Action §1.2.5). Gộp hai tiêu chí vào một score: số bài × 10^7 − penalty (giây), penalty luôn dưới 10^7 nên nhiều bài hơn luôn đứng trên.

Đánh đổi: Nhanh nhưng mọi lượt xem vẫn đến Redis; Redis mất dữ liệu thì phải có chỗ dựng lại.

Nguồn: Redis in Action ↗ · ch.1 · §1.2.5 Sorted sets in Redis (tr. 13) ; Redis in Action ↗ · ch.3 · §3.5 Sorted sets (tr. 50) (ZADD, ZREVRANGE, ZREVRANK) ; Chưa có nguồn trong thư viện: độ phức tạp O(log N) và cách gộp score là kiến thức chung/thiết kế của khoá học

Cách tốt nhất
Sorted set + snapshot trang top lên CDN + DB làm nguồn sự thật

Một consumer duy nhất cho mỗi contest đọc sự kiện verdict theo thứ tự, chỉ tính AC đầu tiên mỗi bài, ghi ContestStanding vào DB rồi ZADD. Mỗi 2 s chụp top 1.000 thành file JSON lên CDN, nên 50.000 lượt xem/s không chạm origin. Chỉ thứ hạng riêng mới gọi ZREVRANK, và client chỉ hỏi mỗi 30 s. Có thể đóng băng bảng trong giờ cuối như ICPC.

Đánh đổi: Bảng trễ khoảng 2 s; điểm hòa cần quy tắc phụ (ví dụ thời điểm AC cuối) và rejudge phải tính lại cả bảng từ DB.

Nguồn: Redis in Action ↗ · ch.1 · §1.2.5 Sorted sets in Redis (tr. 13) ; Redis in Action ↗ · ch.3 · §3.5 Sorted sets (tr. 50) ; Grokking Modern System Design · ch.11 CDN ↗ · mục "Time-to-live (TTL)" ; Grokking Modern System Design · ch.17 Distributed Messaging Queue ↗ · mục "Motivation" (Improved performance, Granular scalability) ; Chưa có nguồn trong thư viện: đóng băng bảng kiểu ICPC là quy tắc contest

4) Cần bao nhiêu judge worker và scale thế nào khi contest bắt đầu?

Cách chưa tốt
Đội judge cố định đủ cho đỉnh contest, hoặc autoscale theo CPU

Fleet cố định thì phần lớn thời gian ngồi không. Autoscale theo CPU thì vô dụng: judge bận là CPU 100%, và máy mới cần vài phút để lên trong khi hàng đợi đã dài.

Đánh đổi: Hoặc tốn tiền, hoặc chậm đúng lúc cần.

Nguồn: Quy tắc ước lượng của khoá học · /hoc/chon-server (1.000 req/s mỗi máy 2 vCPU, chạy tối đa 60% CPU, Redis ×1,3) (autoscale theo CPU kém với tải đột biến, máy mới cần 1-3 phút) ; Grokking Modern System Design · ch.23 Distributed Task Scheduler ↗ · mục "Resource capacity optimization"

Cách tốt
Tính số slot theo Little's law và autoscale theo độ dài hàng đợi

Số slot cần = tốc độ nộp × thời gian chấm trung bình. Ví dụ 190 bài/s × 2 s = 380 slot. Autoscale theo số job đang chờ chia cho tốc độ xử lý, thay vì theo CPU.

Đánh đổi: Phản ứng đúng hướng nhưng vẫn trễ vài phút cho máy mới và tải image.

Nguồn: Lý thuyết · Định luật Little: trong hệ thống ổn định, số yêu cầu đang xử lý trung bình L bằng tốc độ đến λ nhân thời gian ở lại trung bình W, tức L = λ × W. Ví dụ λ = 470 req/s và W = 0,08 s thì L = 470 × 0,08 ≈ 38 request cùng lúc; mỗi instance xử lý được c request đồng thời thì cần ít nhất L ÷ c instance, rồi chia thêm cho 0,6 để chừa chỗ cho đỉnh. Định luật đúng cho mọi hàng đợi ổn định, không phụ thuộc phân phối thời gian đến hay thời gian xử lý, nên dùng được để kiểm tra chéo kết quả ước lượng. ; Quy tắc ước lượng của khoá học · /hoc/chon-server (1.000 req/s mỗi máy 2 vCPU, chạy tối đa 60% CPU, Redis ×1,3) ; Grokking Modern System Design · ch.23 Distributed Task Scheduler ↗ · mục "Queueing"

Cách tốt nhất
Scale sẵn theo lịch contest + pool ấm + chia pool theo ngôn ngữ

Contest có giờ cố định nên scale lên đủ slot 30 phút trước khi bắt đầu. Máy judge có sẵn image ngôn ngữ, sandbox ấm và test của các bài trong contest đã tải về SSD. Chia pool theo ngôn ngữ (C++ biên dịch lâu, Python chạy chậm) để không pool nào chặn pool khác. Hàng contest chạy trên máy on-demand, hàng luyện tập có thể dùng spot.

Đánh đổi: Trả tiền cho máy chờ trước giờ thi; phải giữ cấu hình máy đồng nhất để thời gian chạy so sánh được.

Nguồn: Grokking Modern System Design · ch.23 Distributed Task Scheduler ↗ · mục "Resource capacity optimization" và "Prioritization" ; Chưa có nguồn trong thư viện: spot instance và scale theo lịch là thực hành vận hành của khoá học

Trong phỏng vấn

Nên nói
  • “Code người dùng là code thù địch: tôi chạy trong sandbox có lớp cách ly kernel, không mạng, không credential, giới hạn CPU, RAM, số tiến trình.”
  • “Nộp bài trả 202 và đi qua queue bền; penalty tính theo giờ nộp nên hàng đợi dài không làm ai thiệt.”
  • “Số slot judge = tốc độ nộp × thời gian chấm; contest có lịch nên tôi scale sẵn thay vì chờ autoscale.”
  • “Bảng xếp hạng là Redis sorted set với score = số bài × 10^7 − penalty, trang top là snapshot trên CDN, DB là nguồn sự thật.”
Bẫy hay gặp
  • Chạy code người dùng trên cùng máy với API.
  • Giữ request HTTP mở đến khi chấm xong.
  • Autoscale judge theo CPU, trong khi judge bận thì CPU luôn 100%.
  • Tính bảng xếp hạng bằng SQL ở mỗi lượt xem.

Đọc thêm

Tác vụ chạy lâu
Cách xử lý việc mất vài giây tới vài giờ mà không giữ request: nhận job, trả 202 kèm job id, worker xử lý, báo kết quả qua polling, webhook hoặc SSE. Kèm hàng đợi, retry có backoff và jitter, DLQ, idempotency, heartbeat, lập lịch, workflow engine và cách tính số worker.
Redis
Redis là kho key-value trong RAM với nhiều cấu trúc dữ liệu: dùng làm cache, bộ đếm, leaderboard, hàng đợi, GEO và pub/sub. Bài này đi qua mô hình dữ liệu, persistence, replication, Cluster, Lua và cách tính RAM.
Kafka
Kafka là log phân tán bền vững: topic chia partition, consumer đọc theo offset và có thể đọc lại. Bài này đi qua thứ tự theo partition, consumer group, replication với ISR và acks, giới hạn của exactly-once, cách chọn số partition và so sánh với SQS, Pub/Sub, RabbitMQ.
DynamoDB
DynamoDB là key-value và document database serverless của AWS: độ trễ vài ms ở mọi quy mô nếu bạn chọn đúng partition key. Bài này đi qua partition key, sort key, single-table design, GSI và LSI, hai chế độ capacity, hot partition, Streams, TTL, transaction và cách nghĩ về chi phí.
Cập nhật realtime
Đẩy thay đổi tới client trong vài trăm mili giây: chọn giữa polling, long polling, SSE, WebSocket, rồi fan-out qua nhiều gateway, xử lý reconnect và backpressure.
Máy tính hạ tầng
Nhập số người dùng và thói quen sử dụng, máy tính ước lượng số instance API, cấu hình database, read replica, RAM Redis, lưu trữ, băng thông và mức hạ tầng phù hợp.

Sách trong thư viện của bạn. Số chương có thể lệch nhẹ so với bản PDF.