News feed mạng xã hội
Hiểu đề
Yêu cầu chức năng
- Người dùng có thể đăng bài có chữ và ảnh/video
- Người dùng có thể theo dõi hoặc bỏ theo dõi người khác
- Người dùng xem được feed gồm bài của những người mình theo dõi, bài liên quan nhất ở trên
- Người dùng cuộn feed vô hạn, không gặp bài trùng hay bị sót
Yêu cầu phi chức năng
- Tải trang feed đầu tiên p95 < 500 ms
- Bài mới xuất hiện trong feed người theo dõi trong < 10 s (chấp nhận nhất quán sau)
- Sẵn sàng 99,95% cho đường đọc feed
- Tỷ lệ đọc : ghi khoảng 100 : 1, chịu được tài khoản 10 triệu follower
Ngoài phạm vi: Bình luận, like chi tiết (chỉ dùng làm tín hiệu xếp hạng); Quảng cáo trong feed; Tìm kiếm bài viết, hashtag trending; Kiểm duyệt nội dung.
Thực thể và API
| Method | Endpoint | Ghi chú |
|---|---|---|
| POST | /v1/posts | Tạo bài, body chứa text và media_key đã upload |
| POST | /v1/media/upload-url | Xin presigned URL để upload ảnh/video thẳng lên blob store |
| PUT | /v1/users/{id}/follow | Theo dõi; DELETE cùng path để bỏ theo dõi |
| GET | /v1/feed?cursor=&limit=20 | Trang feed tiếp theo, cursor là chuỗi mờ do server cấp |
| GET | /v1/users/{id}/posts?cursor= | Trang cá nhân: bài của một người, mới nhất trước |
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ể đăng bài có chữ và ảnh/video
Media đi thẳng lên blob store, API chỉ lưu metadata. Việc đưa bài vào feed người khác chạy bất đồng bộ để người đăng không phải chờ.
- 1Client xin presigned URL, upload ảnh lên S3, worker sinh thumbnail và bản nhỏ
- 2Client gọi POST /posts với text và media_key
- 3API lưu bài vào post store, phát sự kiện post_created vào queue
- 4Fan-out worker đọc danh sách follower và chèn post_id vào feed của từng người
Nguồn: Grokking Modern System Design · ch.32 Design Newsfeed System ↗ · Design of a Newsfeed System · mục "The newsfeed publishing service" ; Grokking Modern System Design · ch.20 Blob Store ↗ · mục "High-level design" ; 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: presigned URL là tính năng của nhà cung cấp object storage
2) Người dùng có thể theo dõi hoặc bỏ theo dõi người khác
Ghi một cạnh vào bảng follow ở cả hai chiều. Feed của người theo dõi được bổ sung hoặc dọn dần, không cần làm lại toàn bộ.
- 1Ghi (follower, followee) và (followee, follower) vào follow store
- 2Tăng bộ đếm follower của followee
- 3Theo dõi mới: chèn vài bài gần nhất của followee vào feed; bỏ theo dõi: lọc bài đó lúc đọc
Nguồn: Grokking Modern System Design · ch.32 Design Newsfeed System ↗ · Design of a Newsfeed System · mục "Storage schema" (quan hệ follower/friend, graph store dùng schema quan hệ) ; Grokking Modern System Design · ch.9 Databases ↗ · Data Partitioning · mục "Sharding" (Key-range based sharding, Hash-based sharding, Consistent hashing) ; Chưa có nguồn trong thư viện: ghi cạnh follow theo cả hai chiều và bộ đếm follower là thiết kế của khoá học
3) Người dùng xem được feed gồm bài của những người mình theo dõi, bài liên quan nhất ở trên
Feed đọc từ danh sách post_id dựng sẵn trong cache, trộn thêm bài của tài khoản lớn, xếp hạng rồi mới lấy nội dung bài.
- 1Đọc 500 post_id mới nhất từ feed cache của người dùng
- 2Trộn bài mới của các tài khoản lớn mà người này theo dõi (kéo lúc đọc)
- 3Ranking chấm điểm ứng viên, giữ 20 bài đầu
- 4Lấy nội dung bài từ post cache (MGET), ảnh trả về dạng URL CDN
Nguồn: Grokking Modern System Design · ch.32 Design Newsfeed System ↗ · Design of a Newsfeed System · mục "The newsfeed generation service" và "Posts ranking and newsfeed construction" ; Grokking Modern System Design · ch.33 Design Instagram ↗ · Design of Instagram · mục "Generate a timeline" (hybrid) ; Redis in Action ↗ · ch.8 · §8.2 Home timeline (tr. 188) ; Redis in Action ↗ · ch.3 · §3.2 Lists (tr. 43) ; 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)
4) Người dùng cuộn feed vô hạn, không gặp bài trùng hay bị sót
Dùng cursor thay cho offset. Với feed đã xếp hạng, server giữ một bản chụp thứ tự trong phiên để các trang sau không bị xáo lại.
- 1Trang đầu: xếp hạng, lưu danh sách id đã xếp vào snapshot có TTL 30 phút
- 2Trả 20 bài kèm cursor = {snapshot_id, vị trí}
- 3Trang sau: đọc tiếp từ snapshot theo cursor, hết snapshot thì lùi theo post_id cũ hơn
Nguồn: Chưa có nguồn trong thư viện: phân trang bằng cursor và snapshot không được mô tả trong sách trong thư viện ; Grokking Modern System Design · ch.32 Design Newsfeed System ↗ · Design of a Newsfeed System · mục "Posts ranking and newsfeed construction" (xếp hạng làm thứ tự không còn theo thời gian)
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.
Một API Node.js và một Postgres. Feed tính lúc đọc bằng một câu SQL có index. Ảnh lên S3 và phát qua CDN ngay từ đầu vì băng thông ảnh mới là thứ lớn nhất.
Ước lượng
- Đọc feed: 10.000 × 10 lần mở × 2 trang / 86.400 ≈ 2,3 req/s, đỉnh ×10 ≈ 23 req/s → ceil(23 / 600) = 1, tối thiểu 2 instance
- Đăng bài: 10.000 × 0,2 = 2.000 bài/ngày × 1 KB = 2 MB/ngày ≈ 730 MB/năm
- Media mới: 50% × 2.000 = 1.000 ảnh × 500 KB (gốc + các bản nhỏ) = 500 MB/ngày ≈ 183 GB/năm
- Ảnh được xem: 200.000 trang × 5 ảnh × 100 KB = 100 GB/ngày ≈ 3 TB/tháng → cần CDN
- Fan-out on read: mỗi request gộp bài của tối đa 200 người theo dõi, 23 req/s là nhẹ với index (author_id, created_at)
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) ; Grokking Modern System Design · ch.32 Design Newsfeed System ↗ · Requirements of a Newsfeed System's Design · mục "Resource estimation" (1 server xử lý 8.000 RPS, 300 bạn + 250 page mỗi người) ; 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) ; Chưa có nguồn trong thư viện: số bài đăng 0,2/người/ngày và 10 lần mở feed là giả định của khoá học
Bảng hạ tầng
| Thành phần | Chọn | Số lượng | Cấu hình | Vì sao |
|---|---|---|---|---|
| API | Node.js (NestJS) trên Cloud Run hoặc 2 VM sau LB | 2 | 2 vCPU · 4 GB RAM | 23 req/s đỉnh cần chưa tới 1 instance, giữ 2 để không gián đoạn. |
| Database | PostgreSQL managed | 1 (+ backup hằng ngày) | 2 vCPU · 8 GB RAM · 50 GB | Bài và quan hệ follow dưới 1 GB/năm, toàn bộ vừa RAM. |
| Media | S3 / GCS, presigned URL | 1 bucket | ≈ 183 GB/năm | Không để ảnh trong DB. |
| Xử lý ảnh | Lambda / Cloud Function dùng sharp | theo sự kiện | 1 GB RAM | Sinh thumbnail khi có file mới, không chiếm CPU của API. |
| CDN | CloudFront / Cloudflare | 1 | ≈ 3 TB/tháng | Băng thông ảnh gấp hàng chục lần dữ liệu JSON. |
- Điểm xuất phát
- Feed tính lúc đọc, chưa có cache hay queue
- CDN có ngay vì băng thông ảnh
Câu SQL gộp bài của 200 người chạy trên mỗi lần mở feed. Ở hàng nghìn req/s, Postgres tốn CPU cho việc sắp xếp và gộp.
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ần | Mức 1 | Mức 2 | Mức 3 |
|---|---|---|---|
| Người dùng | 10.000 DAU | 1.000.000 DAU | 50.000.000 DAU |
| Chi phí | ≈ 100–300 USD/tháng (ước lượng) | ≈ 8.000–15.000 USD/tháng (ước lượng, phần lớn là CDN) | ≈ 400.000–900.000 USD/tháng (ước lượng, CDN chiếm phần lớn) |
| API | Node.js (NestJS) trên Cloud Run hoặc 2 VM sau LB 2 × 2 vCPU · 4 GB RAM | Node.js (NestJS), autoscale 2 (autoscale 2–6) × 2 vCPU · 4 GB RAM | Chưa cần |
| Database | PostgreSQL managed 1 (+ backup hằng ngày) × 2 vCPU · 8 GB RAM · 50 GB | PostgreSQL managed 1 primary + 2 replica × 8 vCPU · 32 GB | Chưa cần |
| Media | S3 / GCS, presigned URL 1 bucket × ≈ 183 GB/năm | S3 + CDN + worker resize 1 bucket + CDN × ≈ 300 TB/tháng egress | S3 multi-region + pipeline resize/HLS multi-region × 2,5 TB mới/ngày |
| Xử lý ảnh | Lambda / Cloud Function dùng sharp theo sự kiện × 1 GB RAM | Chưa cần | Chưa cần |
| CDN | CloudFront / Cloudflare 1 × ≈ 3 TB/tháng | Chưa cần | Chưa cần |
| Load balancer | Chưa cần | ALB / Cloud Load Balancing 1 × managed | Chưa cần |
| Queue | Chưa cần | SQS / Pub/Sub, sự kiện post_created 1 × managed | Chưa cần |
| Fan-out worker | Chưa cần | Node.js, ghi Redis bằng pipeline 2 × 2 vCPU · 4 GB RAM | Consumer Kafka, pipeline Redis 14 pod × 2 vCPU · 4 GB RAM |
| Redis | Chưa cần | ElastiCache / Memorystore: feed list + post cache 1 primary + 1 replica × 2 vCPU · 13 GB | Chưa cần |
| Edge | Chưa cần | Chưa cần | GeoDNS + CDN + LB mỗi region 3 region × managed |
| Feed/Post API | Chưa cần | Chưa cần | Node.js trên Kubernetes, HPA 59 pod × 2 vCPU · 4 GB RAM |
| Ranking service | Chưa cần | Chưa cần | Go/Python + model nhẹ (GBDT), feature store trên Redis 87 pod × 4 vCPU · 8 GB RAM |
| Redis Cluster | Chưa cần | Chưa cần | Feed list + post cache + snapshot 12 shard + 12 replica × 4 vCPU · 32 GB |
| Post store | Chưa cần | Chưa cần | ScyllaDB, key author_id, clustering post_id giảm dần 9 node (3/AZ) × 8 vCPU · 64 GB · 4 TB |
| Follow graph | Chưa cần | Chưa cần | PostgreSQL shard theo user_id (Citus/Vitess), 2 bảng theo 2 chiều 4 shard + replica × 16 vCPU · 64 GB · 1 TB |
| Kafka | Chưa cần | Chưa cần | Topic post_created, fanout_tasks, engagement 6 broker (2/AZ) × 8 vCPU · 32 GB |
Đà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) Fan-out on write, on read hay kết hợp? Xử lý tài khoản có hàng triệu follower thế nào?
Mỗi lần mở feed chạy SELECT bài của 200 người đang theo dõi, ORDER BY thời gian. Ổn ở 10.000 DAU vì có index (author_id, created_at).
Đánh đổi: Mỗi request đọc 200 danh sách rồi gộp, ở hàng chục nghìn req/s thì DB không chịu nổi.
Nguồn: Grokking Modern System Design · ch.33 Design Instagram ↗ · Design of Instagram · mục "Generate a timeline" · mục "The pull approach" (thiếu sót: phần lớn lần kéo trả rỗng) ; Grokking Modern System Design · ch.33 Instagram ↗ · mục "Relational or non-relational database"
Khi đăng bài, worker chèn post_id vào feed cache của từng follower. Đọc feed chỉ còn một lệnh đọc danh sách, rất nhanh.
Đánh đổi: Một tài khoản 10 triệu follower đăng một bài là 10 triệu lần ghi, mất nhiều phút mới tới hết và đè queue của mọi người khác.
Nguồn: Grokking Modern System Design · ch.33 Design Instagram ↗ · Design of Instagram · mục "Generate a timeline" · mục "The push approach" (Point to Ponder: tài khoản triệu follower làm đẩy kém hiệu quả) ; Redis in Action ↗ · ch.8 · §8.2 Home timeline (tr. 188) ; Redis in Action ↗ · ch.3 · §3.2 Lists (tr. 43)
Tác giả dưới ngưỡng (ví dụ 10.000 follower) thì fan-out on write. Tác giả trên ngưỡng chỉ lưu bài vào timeline của chính họ. Lúc đọc, service lấy feed dựng sẵn rồi trộn thêm bài mới của vài chục tài khoản lớn mà người dùng theo dõi, bài của họ thường đã nằm sẵn trong cache vì nhiều người cùng đọc. Người dùng lâu không vào cũng không được fan-out, feed của họ dựng lại khi họ quay lại.
Đánh đổi: Hai đường code phải giữ đồng bộ và đọc feed chậm hơn một chút vì phải trộn, đổi lại lượng ghi có trần rõ ràng.
Nguồn: Grokking Modern System Design · ch.33 Design Instagram ↗ · Design of Instagram · mục "Generate a timeline" · mục "Hybrid approach" (push cho người ít follower, pull cho người nổi tiếng) ; Grokking Modern System Design · ch.32 Design Newsfeed System ↗ · mục "The newsfeed publishing service" ; Chưa có nguồn trong thư viện: ngưỡng 10.000 follower là giả định của khoá học (Grokking chỉ nêu "hàng trăm/nghìn" so với "hàng trăm nghìn/triệu")
2) Feed cache lưu gì, giới hạn bao nhiêu, và phân trang ra sao?
Một bài 1 KB bị chép vào 200 feed. Sửa hay xoá bài phải cập nhật 200 chỗ. Offset bị lệch khi có bài mới chèn lên đầu, người dùng gặp bài trùng.
Đánh đổi: Đọc nhanh nhưng RAM gấp hàng trăm lần và dữ liệu cũ khó dọn.
Nguồn: Grokking Modern System Design · ch.32 Design Newsfeed System ↗ · Design of a Newsfeed System · mục "Storage schema" (post cache và newsfeed cache tách nhau) ; Grokking Modern System Design · ch.31 Twitter ↗ · mục "Cache"
LPUSH rồi LTRIM 0 499 khi fan-out. Nội dung bài nằm ở post cache riêng, lấy bằng MGET. Cursor là post_id cuối của trang trước, vì Snowflake id tăng theo thời gian nên lấy tiếp các id nhỏ hơn là không trùng.
Đánh đổi: Mỗi người khoảng 5 KB RAM, nhưng cuộn quá 500 bài phải rơi xuống truy vấn chậm hơn ở post store.
Nguồn: Redis in Action ↗ · ch.8 · §8.2 Home timeline (tr. 188) ; Redis in Action ↗ · ch.3 · §3.2 Lists (tr. 43) ; Grokking Modern System Design · ch.32 Design Newsfeed System ↗ · mục "Putting everything together" (newsfeed cache và post cache riêng) ; Grokking Modern System Design · ch.12 Sequencer ↗ · mục "Twitter Snowflake" (ID tăng theo thời gian)
Feed cache có TTL vài ngày, chỉ người đăng nhập gần đây mới được fan-out, người quay lại sau lâu thì dựng lại theo kiểu pull. Feed xếp hạng không còn sắp theo thời gian nên trang đầu lưu snapshot thứ tự, cursor mờ mã hoá snapshot_id và vị trí. Số like, comment để ở bộ đếm riêng nên không phải ghi lại feed.
Đánh đổi: Người lâu không vào chờ thêm vài trăm ms ở lần mở đầu, và snapshot tốn thêm RAM trong 30 phút của phiên.
Nguồn: Grokking Modern System Design · ch.16 Distributed Cache ↗ · Background of Distributed Cache · mục "Eviction policies" và "Cache invalidation" ; Grokking Modern System Design · ch.33 Design Instagram ↗ · Design of Instagram · mục "Generate a timeline" ; Chưa có nguồn trong thư viện: snapshot thứ tự và cursor mờ là thiết kế của khoá học
3) Xếp hạng feed thế nào mà vẫn kịp 500 ms?
Tính điểm cho hàng nghìn bài mỗi request bằng model lớn ngay trên đường đọc.
Đánh đổi: Chất lượng tốt nhưng độ trễ và chi phí CPU/GPU không chấp nhận được.
Nguồn: Grokking Modern System Design · ch.32 Design Newsfeed System ↗ · mục "The newsfeed ranking service" (xếp hạng đa tín hiệu, không thể chạy nặng trên đường đọc) ; 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)
Điểm = độ mới giảm dần theo giờ × (1 + tương tác của bài) × mức thân với tác giả. Tính trong Feed API trên 500 ứng viên, không cần hạ tầng riêng.
Đánh đổi: Rẻ và dễ giải thích, nhưng khó cá nhân hoá sâu và phải chỉnh trọng số bằng tay.
Nguồn: Grokking Modern System Design · ch.32 Design Newsfeed System ↗ · mục "Posts ranking and newsfeed construction" (điểm relevance từ các tín hiệu) ; Chưa có nguồn trong thư viện: công thức điểm cụ thể là ví dụ của khoá học
Tầng 1 lấy ứng viên (feed cache + tài khoản lớn + vài nguồn gợi ý). Tầng 2 model nhẹ chấm vài trăm ứng viên bằng feature đã tính sẵn trong feature store. Tầng 3 lọc bài đã xem, đa dạng tác giả, chèn bài mới. Sự kiện xem, like đi qua Kafka để cập nhật feature gần realtime và huấn luyện offline.
Đánh đổi: Cần đội ML, feature store và A/B test. Chỉ đáng khi quy mô đủ lớn để vài % tương tác thêm là đáng tiền.
Nguồn: Grokking Modern System Design · ch.32 Design Newsfeed System ↗ · mục "The newsfeed ranking service" và "Posts ranking and newsfeed construction" ; Grokking Modern System Design · ch.31 Twitter ↗ · mục "Observability" (luồng sự kiện qua Kafka) ; Chưa có nguồn trong thư viện: feature store và A/B test không có sách trong thư viện
4) Ảnh và video trong feed lưu và phát thế nào?
File đi qua Node.js rồi ghi vào cột bytea. DB phình nhanh, backup chậm, API bị chiếm băng thông.
Đánh đổi: Một chỗ lưu cho mọi thứ, nhưng là cách nhanh nhất để làm sập DB.
Nguồn: Grokking Modern System Design · ch.20 Blob Store ↗ · mục "Blob metadata" và "Stream a file" ; Grokking Modern System Design · ch.9 Databases ↗ · mục "Why non-relational (NoSQL) databases?" (so sánh lưu file lớn trong DB) ; Chưa có nguồn trong thư viện: cột bytea của PostgreSQL
Client upload thẳng lên S3, DB chỉ giữ media_key. Người xem tải qua CDN, phần lớn request không chạm tới origin.
Đánh đổi: Rẻ và đơn giản, nhưng ảnh gốc vài MB làm feed trên điện thoại tải chậm.
Nguồn: Grokking Modern System Design · ch.20 Blob Store ↗ · mục "Blob metadata" và "Stream a file" ; Grokking Modern System Design · ch.11 Content Delivery Network (CDN) ↗ · mục "Pull CDN" ; Chưa có nguồn trong thư viện: presigned URL là tính năng của nhà cung cấp object storage
Upload xong thì worker sinh thumbnail, bản 640 px, 1080 px dạng WebP/AVIF; video chuyển sang HLS nhiều bitrate. Feed trả URL đúng kích thước màn hình. Bài chỉ hiện khi media đã xử lý xong, hoặc hiện ảnh mờ trước.
Đánh đổi: Tốn thêm lưu trữ cho các biến thể và một hàng đợi xử lý, đổi lại băng thông CDN giảm nhiều lần.
Nguồn: Grokking Modern System Design · ch.26 Design YouTube ↗ · mục "Encode" và "Adaptive streaming" ; Grokking Modern System Design · ch.11 CDN ↗ · mục "Content caching strategies in CDN" ; Chưa có nguồn trong thư viện: định dạng WebP/AVIF và HLS cụ thể
Trong phỏng vấn
- “Hệ thống đọc nhiều hơn ghi khoảng 100 lần, nên tôi chuyển chi phí sang lúc ghi bằng fan-out on write.”
- “Tài khoản lớn là ngoại lệ: trên ngưỡng follower thì kéo lúc đọc, tránh hàng triệu lần ghi cho một bài.”
- “Feed cache chỉ chứa post_id có giới hạn độ dài, nội dung bài nằm ở cache riêng.”
- “Phân trang bằng cursor; feed đã xếp hạng thì giữ snapshot trong phiên để không trùng bài.”
- Phân trang bằng offset trên feed liên tục có bài mới
- Fan-out on write cho mọi tài khoản, quên celebrity
- Chép nguyên nội dung bài vào feed của từng người
- Quên băng thông ảnh: CDN thường là khoản chi lớn nhất
Đọc thêm
Sách trong thư viện của bạn. Số chương có thể lệch nhẹ so với bản PDF.