Skip to content

Phương pháp làm bài system design ​

Câu hỏi system design có dạng "thiết kế hệ thống X" và không có đáp án duy nhất. Người phỏng vấn quan tâm cách đi tới sơ đồ cuối cùng hơn là chính sơ đồ đó, cụ thể là có hỏi lại để hiểu đúng bài toán không, có ước lượng để biết bài toán lớn cỡ nào không, có chọn thành phần vì một lý do cụ thể không, và có nói được mỗi lựa chọn tốn gì không. Trang này là khung cho một buổi khoảng 45 phút, cách ước lượng nhanh kèm những con số cần nhớ, và cách tính độ sẵn sàng. Các case study trong phần này đều đi theo khung này.

Khung 45 phút ​

BướcThời gianLàm gìKết quả để lại trên bảng
1. Làm rõ yêu cầukhoảng 5 phútHỏi chức năng chính, quy mô, yêu cầu phi chức năngDanh sách chức năng trong và ngoài phạm vi; vài con số
2. Ước lượngkhoảng 5 phútĐổi số người dùng ra QPS, dung lượng, băng thôngVài con số đã làm tròn, kèm kết luận "một máy đủ" hay "phải chia"
3. API và data model5–10 phútCác endpoint chính, các bảng hoặc collection, cách truy cậpDanh sách API, schema thô
4. Thiết kế tổng thểkhoảng 10 phútSơ đồ đơn giản nhất chạy được, đi qua các luồng chínhSơ đồ khối, luồng đọc và luồng ghi
5. Đi sâukhoảng 15 phútPhần khó nhất của bài, so sánh 2–3 phương ánPhương án đã chọn và lý do
6. Tổng kếtkhoảng 5 phútĐiểm nghẽn, điểm lỗi, đánh đổi, cách theo dõiDanh sách rủi ro và cách xử lý

Thời gian chỉ là gợi ý. Người phỏng vấn thường dẫn sang chỗ họ quan tâm, và đi theo hướng đó quan trọng hơn giữ đúng lịch. Cái cần giữ là thứ tự, vì mỗi bước dùng kết quả của bước trước: không có con số ở bước 2 thì không có lý do để chọn ở bước 5.

Bước 1: làm rõ yêu cầu ​

Đề bài luôn cố tình mơ hồ. "Thiết kế hệ thống chat" có thể là chat 1-1 cho một app bán hàng, hoặc nhóm chat hàng nghìn người như Telegram, và hai bài đó cần hai thiết kế khác hẳn nhau. Vì vậy câu hỏi đầu tiên luôn là hỏi lại.

Yêu cầu chức năng. Chốt 3–5 việc cốt lõi người dùng làm được, và nói rõ những gì để ngoài phạm vi. Với bài chat, ví dụ: gửi tin 1-1 và nhóm tối đa 500 người, xem lịch sử, trạng thái đã đọc; để ngoài phạm vi gọi video và tìm kiếm tin nhắn. Phạm vi hẹp giúp có thời gian đi sâu vào phần khó.

Yêu cầu phi chức năng. Đây là phần quyết định kiến trúc, nên hỏi bằng con số:

Hỏi gìVì sao quyết định thiết kế
Bao nhiêu người dùng mỗi ngày, mỗi người làm gì bao nhiêu lần?Ra QPS và dung lượng ở bước 2
Đọc nhiều hay ghi nhiều, tỉ lệ bao nhiêu?Đọc nhiều thì nghĩ tới cache, replica; ghi nhiều thì nghĩ tới gom lô, hàng đợi, chia dữ liệu
Latency yêu cầu ở p99 là bao nhiêu?100 ms và 2 giây dẫn tới hai cách làm khác nhau (Latency)
Ngừng bao lâu thì chấp nhận được?Ra số "9" cần đạt, quyết định có cần dự phòng nhiều AZ hay nhiều vùng không
Dữ liệu nào được phép cũ vài giây, dữ liệu nào phải luôn đúng?Lượt thích cũ một chút không sao; số dư tài khoản, tồn kho thì không
Người dùng ở đâu?Một nước hay nhiều châu lục; mỗi round trip qua đại dương tốn hơn 100 ms (Thời gian trên mạng)
Dữ liệu giữ bao lâu, có được mất không?Quyết định dung lượng, backup, và có cần ghi đồng bộ ra nhiều bản không

Nếu người phỏng vấn không cho con số thì tự giả định và nói ra, ví dụ "giả sử 10 triệu người dùng mỗi ngày, mỗi người gửi 40 tin". Cách này cho thấy mình biết con số nào quan trọng, và để người phỏng vấn chỉnh nếu giả định lệch. Bài toán chỉ xoay quanh database thì có bảng câu hỏi chi tiết hơn ở Phân tích bài toán database.

Bước 2: ước lượng nhanh ​

Ước lượng không cần chính xác, chỉ cần đúng bậc độ lớn để biết bài toán nằm ở vùng nào: một máy dư sức, cần thêm replica và cache, hay phải chia dữ liệu ra nhiều máy. Ước lượng xong mà không dùng kết quả để quyết định gì thì coi như chưa làm.

Mẹo làm tròn ​

Đại lượngGiá trị đúngLàm tròn để tính nhẩm
Một ngày86.400 giây10⁵ giây (nhỏ hơn thật khoảng 14%, không đổi bậc độ lớn)
Một thángkhoảng 2,6 triệu giây2,5 × 10⁶ giây
Một nămkhoảng 31,5 triệu giây3 × 10⁷ giây
1 triệu sự kiện mỗi ngàykhoảng 11,6 mỗi giâykhoảng 10 mỗi giây
1 KB × 1 triệu1 GB
1 KB × 1 tỷ1 TB

Với các mẹo này, mỗi con số chỉ cần một hai phép nhân:

  • QPS trung bình = số người dùng mỗi ngày × số lần mỗi người ÷ 10⁵.
  • QPS cao điểm = QPS trung bình × hệ số cao điểm. Không có số liệu thì giả định gấp 2–3 lần; bài có sự kiện như flash sale thì có thể gấp hàng chục, hàng trăm lần trong vài phút (Flash sale).
  • Dung lượng = số bản ghi mới mỗi ngày × kích thước × số ngày giữ, rồi nhân thêm khoảng 2 cho index và nhân số bản (primary và replica). Hệ số index lấy từ số đo ở Phân tích bài toán database.
  • Băng thông = QPS × kích thước mỗi response.

Thử với vài bài mẫu. Mỗi kết luận bên dưới so con số ước lượng với những gì một máy làm được, ở bảng ngay sau mô phỏng:

Bài mẫu
12 / 35lệnh ghi mỗi giây: trung bình / cao điểm
1.157 / 3.472lệnh đọc mỗi giây: trung bình / cao điểm
500 MBdữ liệu thô mỗi ngày
5,5 TBlưu thật sau 5 năm
1,7 MB/sbăng thông đọc lúc cao điểm
  • ● Ghi 35 lệnh/s lúc cao điểm: một primary dư sức, kể cả commit từng dòng (laptop của lab làm được khoảng 2.400 dòng/s).
  • ● Đọc 3.472 lệnh/s: một node database đủ (point select trên laptop khoảng 25.000 query/s).
  • ● Lưu 5,5 TB sau 5 năm: một máy lớn vẫn chứa được, nhưng backup, tạo index, đổi schema sẽ rất lâu. Cân nhắc partition theo thời gian và chuyển dữ liệu cũ đi.

Tính nhẩm: 1.000.000 lệnh ghi mỗi ngày chia 10⁵ giây ra 10 lệnh/s, so với 12 khi chia đúng 86.400 giây. Làm tròn như vậy cho số nhỏ hơn khoảng 14%, không đổi bậc độ lớn.

Giả định: mỗi lần đọc trả về một bản ghi; dung lượng lưu = dữ liệu thô × 2 (một index phụ) × số bản. Ngưỡng so sánh lấy từ số đo trên laptop của lab (MySQL 8.0, Redis 8.4 trong Docker), máy chủ thật thường mạnh hơn vài lần, nên chỉ dùng để biết bài toán nằm ở vùng nào.

Bài URL shortener cho thấy vì sao phải ước lượng. Nghe như một hệ thống lớn, nhưng 10 triệu người dùng mỗi ngày tạo khoảng 1 triệu link, tức chỉ khoảng 35 lệnh ghi mỗi giây lúc cao điểm. Một primary MySQL dư sức, và phần khó thật của bài nằm ở chỗ khác. Bài chat thì ngược lại: 2 tỷ tin nhắn mỗi ngày là hàng chục nghìn lệnh ghi mỗi giây, nên phải chia dữ liệu ngay từ đầu.

Một máy làm được bao nhiêu ​

Những con số sau đo trên laptop của lab (MacBook M2, database trong Docker), nên máy chủ thật thường mạnh hơn vài lần. Chúng chỉ dùng để biết bậc độ lớn, không dùng để chọn cấu hình máy:

Thành phầnBậc độ lớnSố đo trong sổ tay
Đọc từ RAM, đọc từ SSD, round trip trong data centerkhoảng 100 ns, khoảng 100 µs, khoảng 0,5 msMySQL chương 1, Thời gian trên mạng
Database, đọc theo khoá chínhhàng chục nghìn query mỗi giây một node25.000–27.000 query/s với 32 connection, cả MySQL và PostgreSQL (số đo)
Database, ghi mỗi dòng một transactionvài nghìn dòng mỗi giây, vì mỗi commit chờ một lần fsyncMySQL khoảng 2.400 dòng/s, PostgreSQL khoảng 10.500 dòng/s
Database, ghi theo lôhàng trăm nghìn dòng mỗi giâyMySQL khoảng 250.000 dòng/s với lô 1.000 dòng (Import nhanh)
Một dòng bị nhiều request cùng cập nhậtvài trăm tới vài nghìn lệnh mỗi giây, bất kể máy mạnh cỡ nàoMySQL 760–800 lệnh/s, PostgreSQL khoảng 3.600 (Bộ đếm)
Redisvài trăm nghìn lệnh mỗi giây, hàng triệu khi gom lệnhkhoảng 230.000 lệnh/s, 1,5–2,6 triệu với pipelining (Redis)
Một process Node.jskhoảng 1.000 ÷ (số ms CPU mỗi request) request mỗi giâyEndpoint tốn 2 ms CPU: khoảng 500 request/s mỗi worker (Scale Node.js)
Connection tới databasetrăm connection một nodePostgreSQL mặc định max_connections = 100 (Node.js với database)

Hai dòng cần để ý nhất khi làm bài là ghi từng dòng và một dòng nóng. Rất nhiều bài khó (flash sale, đặt vé, bộ đếm lượt thích) khó chính vì chúng dồn ghi vào một chỗ, và thêm máy không giúp gì cho một dòng duy nhất.

Bước 3: API và data model ​

Viết vài endpoint chính, mỗi endpoint gồm method, đường dẫn, tham số quan trọng và dữ liệu trả về. Không cần đủ hết, chỉ cần đủ cho các chức năng đã chốt ở bước 1. Ba điểm hay được hỏi tiếp:

  • Phân trang cho mọi API trả danh sách, nên dùng cursor theo khoá thay vì OFFSET (Keyset pagination).
  • Idempotency key cho mọi API có tác dụng phụ mà client có thể gửi lại, như tạo đơn hay thanh toán (Idempotency key).
  • Chọn kiểu storage theo cách truy cập. Liệt kê các câu truy vấn chính trước ("lấy 50 tin mới nhất của một cuộc chat"), rồi mới chọn bảng, khoá và index cho chúng (SQL hay NoSQL).

Bước 4: thiết kế tổng thể ​

Bắt đầu bằng thiết kế đơn giản nhất chạy được: client, load balancer, vài app server không giữ trạng thái, một database. Rồi đi qua từng luồng chính trên sơ đồ, như luồng ghi một tin nhắn hay luồng đọc newsfeed, để kiểm tra thiết kế làm được mọi chức năng đã chốt.

Chỉ thêm thành phần khi có lý do từ bước 1 và 2: thêm cache vì đọc gấp 100 lần ghi, thêm hàng đợi vì việc gửi email không cần xong trong lúc người dùng chờ, chia dữ liệu vì lệnh ghi vượt sức một primary. Mỗi thành phần thêm vào là một chỗ có thể hỏng, một thứ phải vận hành, nên nói được vì sao cần nó quan trọng hơn biết nhiều tên công nghệ. Các thành phần hay dùng và trang chuyên sâu của từng cái ở Building blocks.

App server nên stateless, tức không giữ session hay dữ liệu riêng của người dùng trong bộ nhớ, để thêm bớt instance tự do (Scale Node.js).

Bước 5: đi sâu ​

Mỗi bài có một phần khó nhất, thường được nêu ngay trong đề. Phần đó là chỗ phân biệt ứng viên. Bảng case study ở trang tổng quan ghi phần khó của từng bài: hai người không được đặt cùng một chỗ, một dòng tồn kho bị hàng chục nghìn request tranh nhau, người có hàng triệu follower đăng bài.

Cách trình bày tốt cho phần này là nêu 2–3 phương án, so sánh bằng một bảng ngắn theo các tiêu chí của bước 1 (latency, độ đúng, chi phí, độ phức tạp), rồi chọn một và nói vì sao. Người phỏng vấn thường đổi giả định ("nếu số follower tăng gấp 100 lần thì sao?") để xem phương án có còn đứng không, nên biết trước phương án của mình gãy ở đâu là một lợi thế.

Bước 6: điểm nghẽn, lỗi và đánh đổi ​

Phần cuối đi qua thiết kế một lần nữa với các câu hỏi:

  • Điểm lỗi đơn lẻ: thành phần nào chỉ có một bản, nó hỏng thì sao?
  • Điểm nghẽn: khi tải tăng gấp 10 lần thì chỗ nào chạm trần trước, có key nóng hay dòng nóng nào không (Sự cố cache)?
  • Khi một dependency chậm: có timeout, có giới hạn retry, có circuit breaker không, hay một service chậm kéo sập cả hệ thống (Xử lý lỗi & idempotency)?
  • Mất dữ liệu: async replication thì failover mất bao nhiêu lệnh ghi gần nhất (Replication)?
  • Theo dõi: đo gì để biết hệ thống đang khỏe, ví dụ p99 latency, tỉ lệ lỗi, độ dài hàng đợi, replication lag.

Độ sẵn sàng: tính số 9 ​

Độ sẵn sàng (availability) là tỉ lệ thời gian hệ thống phục vụ được. Người ta hay nói bằng số chữ số 9: "ba số 9" là 99,9%, tức được phép ngừng khoảng 8,8 giờ mỗi năm, hay 43 phút mỗi tháng. Mỗi số 9 thêm vào chia thời gian ngừng cho 10:

Độ sẵn sàngNgừng mỗi nămNgừng mỗi tháng
99%3,65 ngày7,2 giờ
99,9%8,76 giờ43,2 phút
99,95%4,38 giờ21,6 phút
99,99%52,6 phút4,32 phút
99,999%5,26 phút25,9 giây

Khi một request phải đi qua nhiều thành phần, độ sẵn sàng của chúng nhân với nhau, vì chỉ cần một thành phần hỏng là request lỗi. Năm thành phần, mỗi cái 99,9%, cho cả hệ thống khoảng 99,5%, tức ngừng gần hai ngày mỗi năm. Hệ quả là hệ thống không thể sẵn sàng hơn dependency kém nhất trên đường đi của nó. Mỗi service gọi đồng bộ là một lần nhân nữa, nên những việc không cần kết quả ngay (gửi email, ghi thống kê) nên chuyển sang hàng đợi để ra khỏi đường request.

Ngược lại, chạy nhiều bản của một thành phần thì nó chỉ hỏng khi mọi bản cùng hỏng: hai bản 99,9% hỏng độc lập cho 99,9999%. Nhưng giả định "hỏng độc lập" hiếm khi đúng. Các bản cùng nằm trong một AZ, cùng chạy một bản deploy lỗi, cùng đọc một cấu hình sai thì hỏng cùng lúc. Phần lỗi chung đó đặt giới hạn cho lợi ích của dự phòng, và là lý do người ta đặt bản dự phòng ở nhiều AZ và deploy từng phần (canary). Thử ghép các tầng và kéo phần lỗi chung lên để thấy giới hạn này:

Bắt đầu từ
Tầng (nối tiếp)Mỗi bản sẵn sàngSố bảnCả tầng
Load balancer99,9900%
App server99,50%
Database99,900%
Redis99,900%
99,29%cả đường request
2,1số chữ số 9
2,6 ngàyngừng mỗi năm
5,2 giờngừng mỗi tháng

Cả đường request sẵn sàng 99,29%, tức khoảng 2,1 số 9: mỗi năm ngừng khoảng 2,6 ngày, mỗi tháng khoảng 5,2 giờ. Tầng kéo xuống nhiều nhất là App server (ngừng khoảng 1,8 ngày mỗi năm).

Bảng quy đổi số 9 ra thời gian ngừng
Độ sẵn sàngMỗi nămMỗi thángMỗi ngày
99%3,7 ngày7,3 giờ14 phút
99,5%1,8 ngày3,7 giờ7 phút
99,9%8,8 giờ44 phút1 phút
99,95%4,4 giờ22 phút43 giây
99,99%53 phút4 phút9 giây
99,999%5 phút26 giây1 giây

Mô hình đơn giản hoá: các tầng hỏng độc lập với nhau, failover giữa các bản xảy ra tức thì, và lỗi chung là một tỉ lệ cố định. Độ sẵn sàng mỗi bản là giả định để minh hoạ, không phải SLA của sản phẩm nào.

Ba khái niệm hay đi cùng nhau: SLI là chỉ số đo được (tỉ lệ request thành công, p99 latency), SLO là mục tiêu nội bộ cho chỉ số đó (99,9% request thành công trong 30 ngày), còn SLA là cam kết với khách hàng, thường thấp hơn SLO và kèm bồi thường khi vi phạm. Cách đặt SLO, error budget và cảnh báo nằm ở Vận hành & độ tin cậy.

Lỗi hay gặp ​

  • Vẽ ngay mà không hỏi. Thiết kế đẹp cho một bài toán khác vẫn là thiết kế sai.
  • Ước lượng rồi bỏ đó. Con số chỉ có giá trị khi dẫn tới một quyết định, như "35 lệnh ghi mỗi giây nên một primary là đủ".
  • Dùng thứ phức tạp khi chưa cần. Microservices, Kafka, sharding cho một hệ thống vài chục QPS làm thiết kế khó vận hành hơn mà không được gì.
  • Kể tên công nghệ thay vì cơ chế. "Dùng Redis" chưa đủ; cần nói Redis giữ gì, hết hạn thế nào, và khi Redis chết thì sao.
  • Chỉ đi luồng thành công. Người phỏng vấn gần như luôn hỏi khi một thành phần chậm hoặc chết thì chuyện gì xảy ra.
  • Không nói đánh đổi. Mọi lựa chọn đều tốn một thứ gì đó: cache đổi lấy dữ liệu cũ, async replication đổi lấy khả năng mất lệnh ghi khi failover.

Bài toán xoay quanh database có thêm các lỗi riêng ở Phân tích bài toán database.

Ghi nhớ

  1. Đi theo thứ tự: làm rõ yêu cầu, ước lượng, API và data model, thiết kế tổng thể, đi sâu phần khó, tổng kết rủi ro. Bước sau dùng kết quả của bước trước.
  2. Hỏi yêu cầu phi chức năng bằng con số: quy mô, tỉ lệ đọc ghi, p99, mức ngừng chấp nhận được, dữ liệu nào phải luôn đúng.
  3. Ước lượng theo bậc độ lớn (một ngày khoảng 10⁵ giây) để biết bài toán ở vùng một máy, replica và cache, hay chia dữ liệu; nhớ ghi từng dòng chỉ vài nghìn mỗi giây và một dòng nóng chỉ vài trăm.
  4. Bắt đầu từ thiết kế đơn giản nhất chạy được, chỉ thêm thành phần khi có lý do từ con số; ở phần khó, so sánh 2–3 phương án rồi chọn.
  5. Độ sẵn sàng nhân với nhau theo các tầng nối tiếp; dự phòng chỉ giúp tới mức lỗi chung cho phép. 99,9% là khoảng 8,8 giờ ngừng mỗi năm.

Nguồn tham khảo ​

Đã đối chiếu (đọc nguồn ngày 2026-10-28):

NguồnDùng cho phần
Google SRE Book, Availability TableBảng quy đổi độ sẵn sàng ra thời gian ngừng mỗi năm, tháng (99,9% là 8,76 giờ mỗi năm, 43,2 phút mỗi tháng)
Google SRE Book, Embracing RiskĐộ sẵn sàng tính theo thời gian (time-based) và theo tỉ lệ request thành công (aggregate)

Số đo dùng lại từ các trang khác (laptop của lab, chi tiết và cách đo ở mục Nguồn tham khảo của từng trang): query theo khoá chính và insert ở Phân tích bài toán database, dòng nóng ở Bộ đếm, Redis ở Redis, Node.js theo số worker ở Scale Node.js, max_connections ở Node.js với database, độ trễ mạng ở Latency.

Từ kiến thức chung, chưa đối chiếu lần này: khung 45 phút và thời gian từng bước (mỗi công ty một khác); hệ số cao điểm 2–3 lần; công thức ghép độ sẵn sàng nối tiếp và song song, với giả định các thành phần hỏng độc lập (bài The Calculus of Service Availability trên ACM Queue chặn tải về nên chưa đọc được); định nghĩa SLI, SLO, SLA; mô hình lỗi chung trong AvailabilityCalc là mô hình đơn giản hoá của tác giả trang.