Harbor

News feed mạng xã hội

KhóFan-outĐọc nhiềuCacheXếp hạngGiống Facebook, Twitter/X timeline

Hiểu đề

News feed mạng xã hội là gì? Trang chủ hiển thị bài mới từ những người và trang mà người dùng theo dõi. Đọc nhiều hơn ghi hàng trăm lần, và một số tài khoản có hàng triệu người theo dõi.

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

  1. Người dùng có thể đăng bài có chữ và ảnh/video
  2. Người dùng có thể theo dõi hoặc bỏ theo dõi người khá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
  4. 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

User
Người dùng, kèm số follower để biết có phải tài khoản lớn không.
Follow
Cạnh follower → followee. Lưu hai chiều để tra cả ai tôi theo dõi và ai theo dõi tôi.
Post
Bài viết: post_id (Snowflake, có thời gian bên trong), author_id, text, danh sách media_key.
Media
Ảnh/video trên blob store, nhiều kích thước sinh sẵn, phát qua CDN.
FeedEntry
Danh sách post_id đã fan-out sẵn cho từng người dùng, nằm trong Redis, có giới hạn độ dài.
MethodEndpointGhi chú
POST/v1/postsTạo bài, body chứa text và media_key đã upload
POST/v1/media/upload-urlXin presigned URL để upload ảnh/video thẳng lên blob store
PUT/v1/users/{id}/followTheo dõi; DELETE cùng path để bỏ theo dõi
GET/v1/feed?cursor=&limit=20Trang 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ờ.

  1. 1Client xin presigned URL, upload ảnh lên S3, worker sinh thumbnail và bản nhỏ
  2. 2Client gọi POST /posts với text và media_key
  3. 3API lưu bài vào post store, phát sự kiện post_created vào queue
  4. 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ộ.

  1. 1Ghi (follower, followee) và (followee, follower) vào follow store
  2. 2Tăng bộ đếm follower của followee
  3. 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. 1Đọc 500 post_id mới nhất từ feed cache của người dùng
  2. 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)
  3. 3Ranking chấm điểm ứng viên, giữ 20 bài đầu
  4. 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.

  1. 1Trang đầu: xếp hạng, lưu danh sách id đã xếp vào snapshot có TTL 30 phút
  2. 2Trả 20 bài kèm cursor = {snapshot_id, vị trí}
  3. 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.

Người dùng
10.000 DAU
Tải đỉnh
≈ 23 req/s đọc feed đỉnh
Chi phí ước lượng
≈ 100–300 USD/tháng (ước lượng)

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.

Người dùngNgười dùngCDNCDNCân bằng tảiLoad balancerLưu trữS3 mediaDịch vụAPI Node.jsDatabasePostgrestải ảnhHTTPSSQL gộp feed

Ướ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ầnChọnSố lượngCấu hìnhVì sao
APINode.js (NestJS) trên Cloud Run hoặc 2 VM sau LB22 vCPU · 4 GB RAM23 req/s đỉnh cần chưa tới 1 instance, giữ 2 để không gián đoạn.
DatabasePostgreSQL managed1 (+ backup hằng ngày)2 vCPU · 8 GB RAM · 50 GBBài và quan hệ follow dưới 1 GB/năm, toàn bộ vừa RAM.
MediaS3 / GCS, presigned URL1 bucket≈ 183 GB/nămKhông để ảnh trong DB.
Xử lý ảnhLambda / Cloud Function dùng sharptheo sự kiện1 GB RAMSinh thumbnail khi có file mới, không chiếm CPU của API.
CDNCloudFront / Cloudflare1≈ 3 TB/thángBăng thông ảnh gấp hàng chục lần dữ liệu JSON.
Thay đổi so với mức trước
  • Đ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
Nút thắt tiếp theo

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ầnMức 1Mức 2Mức 3
Người dùng10.000 DAU1.000.000 DAU50.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ầnChưa cần
CDN
CloudFront / Cloudflare
1 × ≈ 3 TB/tháng
Chưa cầnChưa cần
Load balancerChưa cần
ALB / Cloud Load Balancing
1 × managed
Chưa cần
QueueChưa cần
SQS / Pub/Sub, sự kiện post_created
1 × managed
Chưa cần
Fan-out workerChư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
RedisChưa cần
ElastiCache / Memorystore: feed list + post cache
1 primary + 1 replica × 2 vCPU · 13 GB
Chưa cần
EdgeChưa cầnChưa cần
GeoDNS + CDN + LB mỗi region
3 region × managed
Feed/Post APIChưa cầnChưa cần
Node.js trên Kubernetes, HPA
59 pod × 2 vCPU · 4 GB RAM
Ranking serviceChưa cầnChưa cần
Go/Python + model nhẹ (GBDT), feature store trên Redis
87 pod × 4 vCPU · 8 GB RAM
Redis ClusterChưa cầnChưa cần
Feed list + post cache + snapshot
12 shard + 12 replica × 4 vCPU · 32 GB
Post storeChưa cầnChư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 graphChưa cầnChư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
KafkaChưa cầnChư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?

Cách chưa tốt
Fan-out on read thuần bằng một câu SQL

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"

Cách tốt
Fan-out on write cho mọi tác giả

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)

Cách tốt nhất
Hybrid: push cho người thường, pull cho tài khoản lớn

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?

Cách chưa tốt
Lưu nguyên nội dung bài trong feed từng người, phân trang bằng offset

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"

Cách tốt
Redis list chỉ chứa post_id, cắt ở 500 phần tử, cursor theo post_id

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)

Cách tốt nhất
Chỉ cache người đang hoạt động + snapshot cho feed đã xếp hạng

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?

Cách chưa tốt
Chạy model nặng trên mọi bài có thể thấy

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)

Cách tốt
Công thức điểm đơn giản

Đ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

Cách tốt nhất
Pipeline nhiều tầng

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?

Cách chưa tốt
Upload qua API server, lưu blob trong DB

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

Cách tốt
S3 + presigned URL + CDN

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

Cách tốt nhất
Xử lý bất đồng bộ nhiều kích thước + CDN theo kích thước

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

Nên nói
  • “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.”
Bẫy hay gặp
  • 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.