System Design là gì?
Xem thửTình huống: 6 ngày trước Black Friday
gdev-shop đang chạy trên một máy chủ duy nhất: ứng dụng và PostgreSQL nằm cùng một VM 4 vCPU / 8GB RAM. Hệ thống phục vụ khoảng 800 đơn hàng/ngày, p99 trang sản phẩm khoảng 180ms. Không ai phàn nàn.
Sáng nay, team marketing thông báo: chiến dịch Black Friday chạy trong 4 giờ tối thứ Sáu, traffic dự kiến gấp 20 lần bình thường.
Bạn có 6 ngày và 2 engineer.
Bạn làm gì đầu tiên?
- A. Nâng VM lên 16 vCPU / 64GB.
- B. Thêm Redis cache trước database.
- C. Tách
catalogvàcheckoutthành hai service riêng để scale độc lập. - D. Chưa thêm gì cả — chạy load test và đo xem cái gì vỡ trước.
Dừng lại ở đây. Chọn một phương án và viết ra một câu lý do, trước khi đọc tiếp. Việc bạn đoán sai ở bước này tạo ra trí nhớ mạnh hơn nhiều so với việc đọc thẳng đáp án.
Cách đọc hình kiến trúc. Ứng dụng và PostgreSQL cùng nằm trên một VM. Dưới tải x20, tầng ứng dụng chỉ dùng 24% CPU (rảnh), trong khi PostgreSQL ở 91% CPU và 98 trên 100 kết nối, vì một truy vấn không có index được gọi khoảng 19.000 lần mỗi phút. Hình cho thấy bottleneck nằm ở một thành phần cụ thể, không phải ở toàn bộ kiến trúc, nên tách service hay thêm app server sẽ không giải quyết được điều gì.
Đọc sơ đồ V1
Một tiến trình ứng dụng và một database nằm chung một máy. Mọi request đọc sản phẩm, mọi lệnh ghi đơn hàng đều đi qua đúng hai thành phần này. Không cache, không replica, không hàng đợi.
Kiến trúc này không sai. Với 800 đơn/ngày, nó là lựa chọn đúng: rẻ nhất, ít thứ có thể hỏng nhất, một người cũng vận hành được.
Bằng chứng: kết quả load test ×20
App CPU : 24%
App memory : 41%
DB CPU : 91%
DB connections : 98 / 100 (max_connections = 100)
Cache hit rate : không có cache
p99 trang sản phẩm : 180ms -> 2.9s
Tỉ lệ request lỗi : 7.4% (phần lớn: timeout khi chờ connection tới DB)
Top query theo tổng thời gian:
SELECT * FROM products WHERE category_id = $1 ORDER BY created_at DESC
-> chiếm 68% tổng thời gian DB, không dùng index, gọi ~19.000 lần/phút
Với bằng chứng này, hãy trả lời lại trước khi đọc tiếp:
- Bottleneck thật sự nằm ở thành phần nào?
- Phương án nào trong A/B/C chắc chắn không giải quyết được bottleneck đó?
Cách đọc hình kiến trúc. Đọc theo mũi tên từ trái sang phải rồi xuống dưới: traffic tăng khiến truy vấn thiếu index chạy rất nhiều lần, CPU database lên 91%, mỗi truy vấn giữ kết nối lâu hơn nên bể kết nối gần cạn (98/100), request mới phải xếp hàng chờ nên p99 tăng từ 180 ms lên 2,9 giây, và cuối cùng 7,4% request hết thời gian chờ. Mỗi hộp là một số đo bạn có thể kiểm chứng, đó là cách biến cảm giác 'hệ thống chậm' thành một chuỗi nguyên nhân cụ thể.
Đang tải Animation…
Đáp án và reasoning
Bottleneck nằm ở database — cụ thể hơn: ở một câu query đọc không có index.
Đọc lại chuỗi nhân quả ở hình phía trên: traffic tăng không trực tiếp làm hỏng gì cả. Nó khiến một câu query vốn đã kém hiệu quả bị gọi 19.000 lần/phút; query đó đẩy DB CPU lên 91%; CPU cao khiến mỗi query giữ connection lâu hơn; connection pool cạn ở 98/100; request mới phải xếp hàng chờ connection; p99 nhảy lên 2.9s; và 7.4% khách hàng không checkout được.
Phương án C (tách microservices) chắc chắn không giải quyết gì. App CPU chỉ 24% — tầng ứng dụng đang rảnh. Tách nó thành hai service thì vẫn là đúng một database đó, đúng câu query đó, và bạn vừa thêm network call, thêm pipeline deploy, thêm monitoring cho hai service — trong 6 ngày, với 2 engineer.
Phương án A (nâng VM) mua thêm được ít thời gian, nhưng bản chất là trả tiền để chạy nhanh hơn một câu query lẽ ra không nên chậm.
Phương án B (Redis) đi đúng hướng, nhưng vẫn chưa phải bước đầu tiên.
Bước đầu tiên đúng là D. Sau khi đo, hành động rẻ nhất giải quyết đúng bottleneck là thêm index cho category_id — một lệnh migration — rồi mới cân nhắc cache cho những category được xem nhiều nhất.
Kiến trúc không thay đổi vì traffic tăng. Nó thay đổi vì một bottleneck cụ thể đã được đo.
Vậy System Design là gì?
Bây giờ định nghĩa mới có chỗ để bám:
System Design là chuỗi quyết định về cách các thành phần phối hợp với nhau, để hệ thống đáp ứng được yêu cầu dưới tải và khi có lỗi — trong giới hạn về thời gian, chi phí và năng lực đội ngũ.
Ba chi tiết đáng chú ý:
- "Chuỗi quyết định" — không phải một bản vẽ. Diagram là đầu ra của các quyết định, không phải công việc chính.
- "Dưới tải và khi có lỗi" — một thiết kế chỉ đúng lúc mọi thứ bình thường là thiết kế chưa hoàn chỉnh.
- "Trong giới hạn" — "6 ngày và 2 engineer" là một phần của bài toán, không phải chi tiết phụ. Kiến trúc tốt nhất về lý thuyết mà đội của bạn không vận hành nổi là một kiến trúc tồi.
Đang tải sơ đồ…
Đọc sơ đồ: vòng lặp quyết định
Sơ đồ này không vẽ một hệ thống. Nó vẽ thứ tự bạn nên suy nghĩ trước khi vẽ bất kỳ hộp nào lên bảng. Hãy gắn từng hộp vào tình huống Black Friday ở đầu bài:
- Problem (vấn đề). Chỉ ghi thứ người dùng thật sự cảm thấy: 7,4% lượt thanh toán lỗi và p99 trang sản phẩm lên 2,9 giây. "CPU database cao" chưa phải vấn đề, nó chỉ là một triệu chứng bên trong.
- Evidence (bằng chứng). Đo trước khi đoán. Load test gấp 20 lần cho thấy CPU tầng ứng dụng chỉ 24%, CPU database 91% và bể kết nối 98 trên 100. Riêng bằng chứng này đã loại phương án tách microservices, vì tầng ứng dụng đang rảnh.
- Bottleneck (nút thắt). Gọi tên đúng thành phần và đúng câu lệnh: truy vấn lọc theo
category_idkhông có index, bị gọi khoảng 19.000 lần mỗi phút. "Database chậm" chưa đủ cụ thể để hành động. - Candidates (ứng viên). Liệt kê các cách, rẻ nhất trước: thêm index, thêm Redis, nâng cấu hình máy, tách service. Viết ra cả danh sách buộc bạn so sánh chi phí thay vì chọn cách quen tay.
- Decision (quyết định). Chọn thay đổi nhỏ nhất xóa được nút thắt đã đo: một migration thêm index, rồi mới cân nhắc cache cho vài danh mục xem nhiều.
- Trade-off (cái giá). Nói rõ vấn đề mới vừa mua: cache cần được vô hiệu hóa đúng lúc, Redis là thêm một thứ có thể chết, và đội phải vận hành thêm một thành phần.
Hai chỗ hay bị bỏ qua nhất. Thứ nhất là Evidence: nhiều thiết kế nhảy thẳng từ Problem sang Decision, và đó chính là cách phương án tách microservices xuất hiện trong tình huống Black Friday. Thứ hai là mũi tên cuối đi từ Trade-off quay về Problem. Mỗi giải pháp sinh ra nhóm vấn đề mới, nên vòng lặp không kết thúc, nó chỉ chuyển sang một nút thắt khác. Sau khi thêm index và Redis, câu hỏi tiếp theo đã hiện ra: nếu Redis chết thì database chịu được không?
Dùng trong phỏng vấn và trong công việc. Khi được hỏi "traffic tăng gấp 20 lần thì bạn làm gì", hãy nói đúng sáu bước này theo thứ tự. Người nghe nhìn thấy bạn có phương pháp, thay vì thấy một danh sách công nghệ.
Cách đọc hình kiến trúc. Sau khi đo, thay đổi nhỏ nhất giải quyết đúng bottleneck là thêm index cho category_id, rồi mới thêm Redis cho các danh mục xem nhiều. Ứng dụng đọc Redis trước (1) và chỉ xuống database khi trượt cache (2). Kết quả: CPU database từ 91% xuống 38%, p99 từ 2,9 giây xuống 240 ms. Nhưng V2 mua thêm ba vấn đề mới là vô hiệu hóa cache, thêm một thành phần có thể chết và chi phí vận hành, nên bottleneck không biến mất mà dịch chuyển.
Đang tải Animation…
V2 mua được gì, và phải trả giá gì
Sau khi thêm index và cache cho các category nóng, load test ×20 cho kết quả:
DB CPU : 91% -> 38%
p99 trang sản phẩm : 2.9s -> 240ms
Tỉ lệ request lỗi : 7.4% -> 0.1%
Nhưng bạn vừa mua thêm ba vấn đề mà V1 không có:
| Vấn đề mới | Biểu hiện cụ thể trong gdev-shop |
|---|---|
| Cache invalidation | Admin sửa giá sản phẩm, khách vẫn thấy giá cũ cho tới khi TTL hết hạn |
| Thêm một thứ có thể chết | Redis restart lúc 20:00 tối thứ Sáu — điều gì xảy ra với DB ngay sau đó? |
| Chi phí vận hành | Thêm một thành phần phải monitor, phải hiểu khi bị gọi dậy lúc 2 giờ sáng |
Câu hỏi ở hàng thứ hai chưa có lời giải trong lesson này — nó chính là Failure Lab ở bài Capstone cuối chương. Hãy giữ nó trong đầu.
Mỗi component mới vừa giải quyết một vấn đề, vừa mua về một nhóm vấn đề mới. Kỹ năng System Design không nằm ở việc biết nhiều component, mà ở việc biết cái giá của từng component.
Hiểu lầm cần phá bỏ
Hiểu lầm 1 — "Kiến trúc tốt là kiến trúc có nhiều thành phần"
Sơ đồ V1 (ba hộp) không tệ hơn sơ đồ V2. Nó đúng với quy mô của nó. Một kiến trúc có Redis, Kafka, Elasticsearch và 12 microservice chạy cho 800 đơn/ngày không phải là kiến trúc "trưởng thành hơn" — nó là kiến trúc đắt hơn, dễ hỏng hơn, khó debug hơn, để giải quyết những vấn đề chưa tồn tại.
Hiểu lầm 2 — "Traffic lớn thì phải chuyển sang microservices"
Traffic ×20 nhưng tầng ứng dụng chỉ dùng 24% CPU. "Traffic lớn" không cho bạn biết thành phần nào đang chịu không nổi. Câu hỏi đúng không phải "traffic bao nhiêu", mà là "cái gì vỡ trước, và số liệu nào chứng minh điều đó".
Hiểu lầm 3 — "Thiết kế xong là xong"
Sau V2, bottleneck không biến mất — nó di chuyển. DB CPU về 38%, nhưng giờ hệ thống phụ thuộc vào Redis, và đó là nơi sự cố tiếp theo nhiều khả năng sẽ xuất hiện.
Big Tech Case Study — Netflix
Bối cảnh
Năm 2008, Netflix vận hành hạ tầng trong data center riêng, với máy chủ scale theo chiều dọc và một cơ sở dữ liệu quan hệ tập trung cho các nghiệp vụ cốt lõi.
Điều gì đã vỡ?
Tháng 8/2008, sự cố hỏng dữ liệu (database corruption) ở database tập trung khiến Netflix không thể gửi đĩa DVD cho khách hàng trong ba ngày. Đây không phải sự cố hiệu năng — không lượng CPU nào cứu được tình huống đó. Vấn đề là: một thành phần duy nhất, scale theo chiều dọc, đang là điểm chết của toàn bộ nghiệp vụ.
Quyết định và lý do
Netflix chuyển sang cloud và tái kiến trúc theo hướng service phân tán, scale ngang, loại bỏ điểm chết tập trung. Lý do không phải "cloud hiện đại hơn", mà là mô hình cũ không cho phép họ vừa tăng trưởng nhanh vừa loại bỏ được các single point of failure. Quá trình này kéo dài nhiều năm (2008–2016), không phải một dự án vài tháng.
Cái giá của kiến trúc mới
Kiến trúc mới mua về một lớp vấn đề hoàn toàn khác: hàng trăm service phụ thuộc lẫn nhau, lỗi cục bộ dễ lan rộng, hành vi hệ thống khó dự đoán khi một phần chết. Chính vì vậy Netflix phải xây thêm cả một lớp công cụ cho kiến trúc đó — thử nghiệm hỗn loạn có chủ đích (Chaos Monkey) và các cơ chế chịu lỗi phía client như circuit breaker.
Bài học
Sự kiện kích hoạt thay đổi kiến trúc của Netflix không phải traffic tăng, mà là một failure mode không chấp nhận được. Đây là lý do khóa học này đặt "failure thinking" ngang hàng với "scale thinking".
Điều không nên sao chép
gdev-shop với 2 engineer không nên bắt đầu từ kiến trúc phân tán kiểu Netflix. Netflix trả cái giá đó vì họ có hàng trăm engineer và một failure mode gây thiệt hại trực tiếp cho hàng triệu khách hàng. Sao chép kiến trúc cuối cùng của một công ty mà không sao chép được bối cảnh và nguồn lực của họ là một trong những cách over-engineer phổ biến nhất.
Nguồn
- Netflix Technology Blog — "Completing the Netflix Cloud Migration" (2016).
- Netflix Technology Blog — loạt bài về Chaos Engineering và Hystrix.
Đối trọng — Stack Overflow
Nếu Netflix cho thấy "khi nào cần kiến trúc phân tán", Stack Overflow cho thấy chiều ngược lại cũng đúng.
Theo bài viết kiến trúc năm 2016 của Nick Craver (engineer tại Stack Overflow), site này phục vụ một lượng traffic rất lớn bằng kiến trúc monolith .NET chạy trên khoảng chục web server, cùng SQL Server và một lớp cache — không phải hàng trăm microservice.
Bài học: quy mô lớn không tự động đòi hỏi kiến trúc phân tán. Với access pattern đọc-nhiều, dữ liệu vừa với bộ nhớ, và một đội hiểu rất rõ hệ thống của mình, một kiến trúc đơn giản được tối ưu kỹ có thể đi xa hơn nhiều so với trực giác thông thường.
Nguồn: Nick Craver — "Stack Overflow: The Architecture — 2016 Edition".
Góc nhìn phỏng vấn System Design
Với chủ đề này, interviewer hiếm khi hỏi "System Design là gì". Họ đặt bạn vào đúng tình huống Black Friday ở đầu bài rồi quan sát bạn hỏi gì trước khi vẽ.
Interviewer có thể hỏi
- "Traffic sắp tăng 20 lần, bạn thiết kế lại hệ thống thế nào?" — đây là bẫy. Câu trả lời tốt bắt đầu bằng việc hỏi ngược: hiện tại thành phần nào gần giới hạn, số liệu nào cho biết điều đó?
- "Tại sao không thêm Redis ngay từ đầu cho chắc?"
- "Nếu bạn không được phép thêm bất kỳ component mới nào, bạn sẽ làm gì?"
- "Kiến trúc này chạy tốt ở 20×. Ở 200× thì thành phần nào vỡ trước?"
Khung reasoning nên nói thành lời
Cách đọc hình. Đọc từ trên xuống, mỗi câu hỏi dẫn tới câu sau. Bằng tiếng Việt, năm câu là: requirement đang thay đổi là gì; số liệu nào cho tôi biết cái gì vỡ trước; bottleneck cụ thể nằm ở đâu; thay đổi nhỏ nhất loại bỏ được bottleneck đó là gì; và thay đổi đó mua về vấn đề mới nào. Câu thứ năm không phải điểm dừng: vấn đề mới vừa mua chính là requirement của vòng kế tiếp, nên dòng chữ nhỏ ở cuối hình ghi rằng hãy lặp lại vòng này cho nút thắt tiếp theo. Trong phỏng vấn, hãy nói đúng thứ tự này thành lời trước khi chạm vào bảng, vì nó cho người nghe thấy bạn dẫn dắt bằng bằng chứng chứ không bằng danh sách công nghệ.
Câu trả lời yếu và câu trả lời tốt hơn
Yếu: "Traffic tăng 20 lần thì em thêm load balancer, Redis cache và tách service để scale ngang."
Tốt hơn: "Trước khi thêm component, em cần biết thành phần nào đang gần giới hạn. Nếu load test cho thấy app CPU 24% còn DB CPU 91%, với một query đọc chiếm 68% thời gian database, thì bottleneck nằm ở read path của database — thêm app server hay tách service không giải quyết gì. Em xử lý theo thứ tự chi phí tăng dần: index cho query đó trước, sau đó cache cho các category nóng, và chỉ cân nhắc replica khi read traffic vượt quá khả năng của một primary."
Khác biệt không nằm ở việc biết nhiều component hơn — mà ở chỗ mỗi quyết định đều gắn với một con số.
Tự kiểm tra — không nhìn lại bài
- App CPU 24% nhưng DB CPU 91%. Hãy giải thích chuỗi nhân quả dẫn từ "một query thiếu index" tới "7.4% khách hàng không checkout được".
- Vì sao "traffic tăng 20 lần" chưa đủ để kết luận cần tách microservices? Bạn cần thêm thông tin gì?
- V2 giảm p99 từ 2.9s xuống 240ms. Kể ra hai vấn đề mới mà V2 mua về, còn V1 thì không có.
Chuyển sang bài toán khác
gdev-feed (mạng xã hội nội bộ) có đặc điểm khác hẳn gdev-shop: khoảng 95% request là đọc feed, dữ liệu thay đổi liên tục, và một số tài khoản có lượng người theo dõi lớn gấp hàng nghìn lần tài khoản thường.
Giả sử gdev-feed cũng gặp tình trạng DB CPU 91%:
- Câu hỏi "cái gì vỡ trước" có được trả lời theo cùng một cách không?
- Giải pháp "thêm index rồi cache" có còn là bước đi rẻ nhất không, khi dữ liệu thay đổi liên tục?
Chưa cần trả lời đầy đủ ngay — hãy ghi lại phán đoán của bạn và đối chiếu khi học tới chương caching và fan-out.
Đáp án chi tiết cho các câu hỏi trong bài
Đáp án cho phần Tự kiểm tra
- Một truy vấn đọc thiếu index chiếm 68% thời gian database và được gọi khoảng 19.000 lần mỗi phút. Khi traffic tăng gấp 20 lần, CPU database lên 91%, nên mỗi truy vấn chạy lâu hơn và giữ kết nối lâu hơn. Bể kết nối cạn ở mức 98/100, các request mới phải xếp hàng chờ một kết nối trống, nên p99 nhảy từ 180 ms lên 2,9 giây. Nhiều request chờ quá thời hạn và hết thời gian (timeout), nên 7,4% thao tác, trong đó có checkout, không hoàn tất. Toàn bộ chuỗi bắt đầu từ một truy vấn kém, còn tầng ứng dụng vẫn rảnh ở 24% CPU.
- Vì "traffic tăng 20 lần" không cho biết thành phần nào đang quá tải. Trong tình huống này traffic tăng nhưng tầng ứng dụng chỉ dùng 24% CPU, nên tách service không giảm tải cho database. Cần thêm: số đo mức sử dụng của từng thành phần (CPU, kết nối, I/O), top truy vấn theo tổng thời gian, độ trễ ở từng tầng và tỉ lệ lỗi. Có số đo mới biết bottleneck nằm ở đâu và chọn thay đổi nhỏ nhất xử lý nó.
- Hai vấn đề mới của V2: vô hiệu hóa cache (admin sửa giá nhưng khách vẫn thấy giá cũ cho đến khi TTL hết), và thêm một thành phần có thể chết (Redis khởi động lại thì mọi lượt đọc đổ xuống database). Ngoài ra còn chi phí vận hành: phải giám sát và hiểu Redis khi bị gọi dậy lúc nửa đêm.
Đáp án cho bài toán chuyển sang gdev-feed
- Câu hỏi "cái gì vỡ trước" có trả lời theo cách giống không? Cách tìm thì giống: đo mức sử dụng từng thành phần, top truy vấn, độ trễ từng tầng. Nhưng đáp án có thể khác: feed đọc chiếm khoảng 95% và dữ liệu thay đổi liên tục, nên thủ phạm thường là cách dựng feed (ghép bài của nhiều người theo dõi mỗi lần đọc) và các tài khoản có rất nhiều người theo dõi, chứ không chỉ một truy vấn thiếu index.
- "Index rồi cache" còn là bước rẻ nhất không? Index vẫn là bước đầu tiên và rẻ. Nhưng cache khó hơn vì dữ liệu đổi liên tục: TTL ngắn thì hit ratio thấp, TTL dài thì feed cũ. Với feed, bước tiếp theo thường là lưu sẵn feed đã dựng (đẩy khi ghi) cho người dùng thường và dựng lúc đọc cho tài khoản rất nhiều người theo dõi. Phần này được học ở các chương caching và case study News Feed.
Đáp án cho câu hỏi để ngỏ trong bài
Redis khởi động lại lúc 20:00 tối thứ Sáu thì cache trống hoàn toàn (cache lạnh), nên mọi lượt đọc danh mục trượt cache và đổ cùng lúc xuống PostgreSQL. Dù đã có index, lượng truy vấn tăng vọt trong lúc cache được nạp lại có thể đưa CPU database trở lại gần mức nguy hiểm. Cách giảm rủi ro: làm ấm cache trước khi nhận lưu lượng, giới hạn số truy vấn đồng thời xuống database, gộp các request cùng khóa, và có Redis dự phòng. Chi tiết nằm ở bài Capstone cuối chương và chương caching.
Đáp án gợi ý cho các câu hỏi phỏng vấn
- "Traffic tăng 20 lần, thiết kế lại thế nào?" Hỏi ngược trước khi vẽ: thành phần nào gần giới hạn, số liệu nào cho biết? Nếu app CPU thấp và DB CPU cao với một truy vấn chiếm phần lớn thời gian thì bottleneck nằm ở đường đọc của database. Xử lý theo thứ tự chi phí tăng dần: index, rồi cache cho phần nóng, rồi replica nếu đọc vẫn vượt khả năng một node.
- "Sao không thêm Redis ngay từ đầu?" Vì Redis giải quyết một vấn đề chưa được đo, đồng thời thêm chi phí: vô hiệu hóa cache, một thành phần có thể chết và phải vận hành. Khi chưa có bằng chứng thì kiến trúc đơn giản (ít thành phần) rẻ hơn và ít hỏng hơn. Thêm khi số đo cho thấy cần.
- "Không được thêm component mới thì làm gì?" Vẫn còn rất nhiều đòn bẩy không cần thành phần mới: thêm index, viết lại truy vấn, gộp truy vấn N+1, tăng bể kết nối hợp lý, tinh chỉnh cấu hình database, nâng cấu hình VM (scale up) và tối ưu code. Nhiều bottleneck chỉ cần những việc này.
- "Chạy tốt ở 20 lần, ở 200 lần thành phần nào vỡ trước?" Dựa trên số đo hiện tại: thành phần có mức sử dụng cao nhất và khó mở rộng nhất, thường là ghi vào Primary DB hoặc một khóa cache rất nóng. Sau V2, database còn dư năng lực nhưng Redis và đường ghi là ứng viên; trả lời cần kèm cách kiểm chứng bằng load test ở 200 lần và biện pháp dự kiến (replica, phân mảnh, cache nhiều tầng).
Nếu chỉ nhớ 3 điều
- Kiến trúc thay đổi vì một bottleneck đã được đo, không phải vì traffic tăng.
- Mỗi component mới vừa giải quyết một vấn đề, vừa mua về một nhóm vấn đề mới.
- Kiến trúc đúng là kiến trúc nhỏ nhất đủ giải quyết vấn đề hiện tại, trong giới hạn nguồn lực của đội.