Scale đọc
Phục vụ lượng đọc gấp hàng trăm lần lượng ghi: index, read replica và replication lag, cache nhiều tầng, CDN, read model denormalized theo CQRS, cùng cách xử lý cache stampede và hot key.
Bài toán: đọc nhiều hơn ghi rất nhiều
Đa số sản phẩm có tỉ lệ đọc/ghi lệch hẳn về phía đọc. Một bài đăng được viết một lần, được xem hàng nghìn lần. Một link rút gọn tạo một lần, được mở hàng triệu lần. Trang sản phẩm được cập nhật vài lần mỗi ngày nhưng có hàng trăm nghìn lượt xem. Tỉ lệ 100:1 tới 1000:1 là bình thường (con số minh hoạ của khoá học). Building Microservices lấy ví dụ một catalog service có hơn 100 lượt đọc cho mỗi lượt ghi.1
Khi lượng đọc tăng, database chính là thứ gãy đầu tiên. API server thì không trạng thái, thêm instance là xong. Database thì không nhân bản dễ như vậy: sách phân biệt scale service không trạng thái (khá đơn giản) với scale database (tuỳ loại database).1 Mục tiêu của pattern: giữ độ trễ đọc thấp và ổn định khi lượt đọc tăng 10 lần, 100 lần, mà không làm hỏng tính đúng của dữ liệu ở những chỗ cần đúng.
Các công cụ, xếp từ rẻ tới đắt:
- Index và query tốt hơn.
- Read replica.
- Cache trong ứng dụng, cache phân tán, CDN.
- Read model riêng, denormalized, cập nhật bất đồng bộ (CQRS).
Cách ngây thơ: nâng cấp máy database
Câu trả lời đầu tiên của nhiều người: database chậm thì lên máy to hơn. Từ 4 vCPU lên 16 vCPU, RAM từ 16 GB lên 64 GB.
Cách này (vertical scaling, hay scale up) thật ra hợp lý ở giai đoạn đầu và bạn nên nói ra trong phỏng vấn; Building Microservices gọi đây là một "quick win" tốt. Nhưng nó có giới hạn:23
- Trần cứng. Chỉ mở rộng được tới giới hạn của máy, và chi phí thường cao vì cần linh kiện đặc biệt; một máy lớn có thể đắt hơn hai máy nhỏ cùng tổng sức mạnh.23
- Không sửa được query tệ. Một query full scan 10 triệu dòng vẫn chậm trên máy 64 vCPU, chỉ đỡ chậm hơn chút.
- Một điểm lỗi. Mọi lượt đọc đổ vào một máy. Máy đó bảo trì là toàn bộ hệ thống đứng (sách: scale up không cải thiện khả năng chịu lỗi nếu chỉ có một máy).2
- Không giảm độ trễ mạng. Khoảng cách địa lý vẫn quyết định độ trễ: Grokking ghi round-trip US East tới Cape Town khoảng 225 ms.4
Giải pháp 1: Index và tối ưu query
Trước khi thêm hạ tầng, hãy chắc rằng mỗi query đọc ít dữ liệu nhất có thể.
-- feed của một user: lọc theo author, sắp theo thời gian
CREATE INDEX idx_posts_author_created ON posts (author_id, created_at DESC);
EXPLAIN ANALYZE
SELECT id, title, created_at FROM posts
WHERE author_id = 42 ORDER BY created_at DESC LIMIT 20;
-- mong đợi: Index Scan using idx_posts_author_created, vài chục dòng được đọc
Các nguyên tắc ngắn:
- Index theo đúng thứ tự cột của điều kiện lọc rồi tới sắp xếp: index chỉ giúp sắp xếp khi nó là tiền tố của thứ tự sort, và nên đặt cột so khớp chính xác trước, cột khoảng sau.5
- Phân trang bằng cursor (
WHERE created_at < $cursor) thay vìOFFSET 100000: skip lớn phải tìm rồi bỏ qua từng kết quả nên chậm, và sách khuyên tính truy vấn trang kế từ kết quả trang trước.6 - Chỉ lấy cột cần (
SELECT id, title), cân nhắc covering index để khỏi đọc bảng. - Đếm lớn (
COUNT(*)trên hàng triệu dòng) thì lưu sẵn thành một cột counter: sách MongoDB nói index không chứa đủ metadata để đếm có điều kiện nhanh hơn việc chạy query thật.7
Chi tiết xem Database index. Về năng lực, bảng "Important Rates" của Grokking ghi một máy MySQL xử lý khoảng 1.000 QPS (xấp xỉ, phụ thuộc loại query, cấu hình máy và index).8 Nhiều hệ thống nhỏ chỉ cần tới đây (nhận định của khoá học).
Giải pháp 2: Read replica và replication lag
Primary nhận mọi lượt ghi, rồi gửi WAL/binlog sang một hoặc nhiều replica. Ứng dụng gửi lượt đọc sang replica. Primary-secondary phù hợp khi tải nặng về đọc: thêm follower để chia tải đọc, nhưng sao chép tới quá nhiều follower có thể làm primary thành điểm nghẽn.9
Replication lag
Replication thường bất đồng bộ để primary không phải chờ. Replica vì vậy chậm hơn primary một khoảng. Sách MongoDB nói secondary thường chỉ chậm vài ms nhưng không có gì đảm bảo: có thể trễ hàng phút, hàng giờ do tải, cấu hình sai hay lỗi mạng.10 Grokking ch.9 liệt kê replication lag là một vấn đề phải xử lý khi replicate bất đồng bộ.9
Sách MongoDB mô tả đúng lỗi này: ghi thành công, đọc ngay nhưng không thấy vì đọc từ secondary chưa kịp replicate.10 Ví dụ: người dùng sửa tên, trang reload đọc từ replica, thấy tên cũ, tưởng lưu thất bại và sửa lần nữa.
Read-your-writes
Sách khuyên không gửi đọc tới secondary khi ứng dụng cần đọc lại chính dữ liệu vừa ghi (trừ khi ghi chờ replicate tới mọi secondary).10 Phía CQRS, Microservices Patterns mô tả hai cách: trả token/version từ lệnh ghi để query biết view đã cập nhật chưa, hoặc UI tự cập nhật model cục bộ từ kết quả lệnh.11 Các cách còn lại trong bảng là gợi ý thiết kế của khoá học.12
Các cách đảm bảo người dùng luôn thấy thay đổi của chính mình:
| Cách | Cách làm | Trade-off |
|---|---|---|
| Đọc từ primary sau khi ghi | ghi cờ last_write_at vào session; trong 5–10 s sau đó, đọc của user này đi primary | đơn giản, tăng tải primary một chút |
| Đọc theo vị trí log | lưu LSN sau khi ghi; chỉ đọc replica nào đã replay tới LSN đó | chính xác, cần hỗ trợ từ driver/proxy |
| Đọc dữ liệu của chính mình từ primary | trang "hồ sơ của tôi" luôn đọc primary, trang công khai đọc replica | dễ hiểu, phân loại theo màn hình |
| Cập nhật ngay trên client | UI hiện giá trị mới từ response của lần ghi | không cần đọc lại, chỉ áp dụng cho chính người ghi |
Lưu ý: Những đọc phục vụ quyết định quan trọng (kiểm tra số dư trước khi trừ, kiểm tra còn ghế) phải đọc primary, hoặc tốt hơn là gộp vào câu lệnh ghi có điều kiện. Xem Tranh chấp tài nguyên.
Giới hạn: replica chỉ giúp đọc. Mỗi replica giữ toàn bộ dữ liệu, nên khi ghi quá tải hoặc dữ liệu không còn vừa một node, bạn cần sharding.9 Building Microservices còn nhận xét rằng ngày nay nên nhìn tới caching trước, vì thường cải thiện hiệu năng lớn hơn với ít công hơn.1
Giải pháp 3: Cache nhiều tầng và CDN
Cache là kho lưu tạm trong bộ nhớ, trả dữ liệu nhanh hơn; cache hit trả ngay, cache miss thì đọc DB rồi ghi lại cache.13 Building Microservices phân biệt cache phía client, proxy (reverse proxy, CDN) và phía server, và hệ thống thực tế thường trộn cả ba.14 Redis in Action §2.3 và §2.4 có ví dụ cache cả trang và cache từng dòng dữ liệu bằng Redis; cache trang 5 phút có thể đưa độ trễ từ khoảng 20–50 ms xuống một round trip tới Redis (dưới 1 ms nếu kết nối cục bộ, dưới 5 ms trong cùng data center).15 Grokking ch.16 bàn cách thiết kế một cache phân tán.13
| Tầng | Độ trễ đọc (ước tính của khoá học, cần đo thực tế) | Hợp với | Rủi ro |
|---|---|---|---|
| Trình duyệt / app | 0 ms | asset, dữ liệu cá nhân ít đổi | khó xóa khi cần |
| CDN | 5–30 ms tại edge | ảnh, JS, trang công khai, API công khai | dữ liệu cũ tới hết TTL |
| Cache trong process | dưới 0,1 ms | config, dữ liệu nhỏ cực nóng | mỗi instance một bản, không đồng bộ |
| Redis | 0,5–1 ms | session, object, kết quả query | thêm hạ tầng, cache stampede |
| Replica DB | vài ms | mọi thứ còn lại | lag |
Cách dùng phổ biến nhất là cache-aside:
async function getProduct(id: string) {
const hit = await redis.get(`product:${id}`);
if (hit) return JSON.parse(hit);
const row = await db.query("SELECT * FROM products WHERE id = $1", [id]);
await redis.set(`product:${id}`, JSON.stringify(row), "EX", 300 + Math.floor(Math.random() * 60));
return row;
}
// khi ghi: cập nhật DB trước, rồi DEL product:{id}
TTL là metadata gắn với mỗi entry để loại dữ liệu cũ.16 Phần ngẫu nhiên (jitter) để các key không hết hạn cùng lúc, và việc xóa key thay vì ghi đè khi ghi là kỹ thuật thực hành của khoá học.17 Chi tiết về các chiến lược và invalidation xem Caching.
CDN cho nội dung động
CDN không chỉ cho ảnh. Response giống nhau cho mọi người đều cache được ở edge. Building Microservices mô tả điều khiển cache của HTTP (Cache-Control với TTL, Expires, ETag cùng conditional GET trả 304) và CDN đưa request tới cache gần client.18 Với Grokking, proxy server của CDN được đặt gần người dùng và phục vụ phần lớn từ RAM, mỗi object có TTL do origin gán; hết hạn thì proxy hỏi lại origin.19 Giá trị cụ thể như s-maxage=60, stale-while-revalidate=300 và mức cũ 60 giây chấp nhận được là ví dụ của khoá học, tuỳ bài toán.17
Cache stampede
Một key cực nóng hết hạn. Trong lúc chưa ai kịp ghi lại, rất nhiều request cùng miss và cùng đánh vào database. Building Microservices gọi tình huống nhiều request dồn vào origin khi cache biến mất là "thundering herd", có thể đè sập origin vì origin thường chỉ được thiết kế cho một phần nhỏ lưu lượng.20 Database chậm lại, nhiều request miss hơn, vòng xoáy bắt đầu.
Cách chống (sách nêu hướng "hiding the origin": origin tự nạp cache bất đồng bộ, request lỗi thì fail fast;20 các kỹ thuật dưới đây là thực hành của khoá học17):
- Request coalescing / single-flight: trong một process, chỉ một promise đi lấy dữ liệu cho mỗi key, các request khác chờ promise đó. Giữa nhiều process, dùng một khóa ngắn
SET lock:product:42 1 NX PX 2000(Redis in Action §6.2 trình bày khoá phân tán bằng Redis và khoá có timeout),21 ai không lấy được khóa thì đợi chút rồi đọc lại cache hoặc trả bản cũ. - Stale-while-revalidate: lưu kèm thời điểm "mềm" hết hạn. Quá hạn mềm thì vẫn trả bản cũ, một request nền làm mới.
- Làm mới chủ động: với vài nghìn key nóng biết trước (trang chủ, top sản phẩm), job nền làm mới trước khi hết hạn.
- Jitter TTL như trong code trên.
const inflight = new Map<string, Promise<unknown>>();
function singleFlight<T>(key: string, load: () => Promise<T>): Promise<T> {
if (!inflight.has(key)) inflight.set(key, load().finally(() => inflight.delete(key)));
return inflight.get(key) as Promise<T>;
}
Hot key
Giả sử một key được đọc 500.000 lần/giây (bài đăng của người nổi tiếng, sản phẩm flash sale). Key đó nằm trên một node, mà Grokking ghi cache server xử lý khoảng 100.000 tới 1 triệu QPS.8 Thêm node không giúp vì key không chia ra được. Grokking ch.16 nói rõ: thêm replica giảm tải cho shard nóng, hoặc shard tiếp trong dải key nóng, hoặc replicate động cho key cụ thể.22
- Cache trong process với TTL 1–5 s cho key nóng. 50 instance API thì Redis chỉ còn thấy khoảng 50 lượt đọc mỗi vài giây cho key đó.
- Nhân bản key (dạng replicate động theo key mà sách nhắc tới): ghi thành
post:42#0tớipost:42#9, đọc ngẫu nhiên một bản. Tải chia cho 10 node. - Đẩy lên CDN nếu dữ liệu công khai.
- Phát hiện key nóng bằng cách lấy mẫu lượt đọc ở client hoặc dùng công cụ thống kê của Redis, rồi bật các cơ chế trên chỉ cho những key đó.
Giải pháp 4: Read model denormalized và CQRS
Có những màn hình mà index, replica hay cache đều không cứu được: dữ liệu cần ghép từ nhiều bảng hoặc nhiều service, có lọc và sắp xếp phức tạp. Microservices Patterns §7.1.5 nói API composition tốn thêm tài nguyên, giảm availability (ví dụ 99,5% mỗi service, gọi 4 provider còn khoảng 97,5%) và thiếu nhất quán giao dịch, và nên chuyển sang CQRS khi cần in-memory join các tập dữ liệu lớn.23 Ví dụ trang "lịch sử đơn hàng" cần đơn từ Order service, trạng thái giao từ Shipping, tên quán từ Restaurant. Gọi 3 service rồi join trong bộ nhớ (API composition) đơn giản nhưng chậm hơn và hỏng khi một service hỏng.23
Microservices Patterns ch.7 đề xuất CQRS: tách mô hình ghi (bảng chuẩn hóa, giữ luật nghiệp vụ) khỏi mô hình đọc (view denormalized, thiết kế theo đúng màn hình). View được cập nhật bằng cách nghe event từ phía ghi.24 Lợi ích sách nêu: query hiệu quả cho dữ liệu của nhiều service, hỗ trợ nhiều loại query khác nhau, và tách mối quan tâm.24
// view updater: idempotent theo eventId, upsert theo orderId
consumer.on("OrderShipped", async (e) => {
await view.updateOne(
{ _id: e.orderId, lastEventSeq: { $lt: e.seq } },
{ $set: { shippingStatus: "SHIPPED", shippedAt: e.at, lastEventSeq: e.seq } });
});
Chọn kho cho view theo kiểu query: Microservices Patterns bảng 7.1 gợi ý document store (MongoDB, DynamoDB) hoặc key-value (Redis) cho tra cứu theo khoá, Elasticsearch cho text query, graph database cho graph query, RDBMS cho báo cáo SQL.25 Redis sorted set cho feed là cách dùng Redis trong Redis in Action ch.8 (home timeline).26
Trade-off:
- View có độ trễ giữa lúc phía ghi publish event và lúc view cập nhật; client vừa ghi rồi query ngay có thể thấy bản cũ. Kết hợp với kỹ thuật read-your-writes ở trên.11
- Thêm code, thêm hạ tầng và database cần vận hành; event handler phải idempotent vì event có thể bị giao lại (sách dùng max(eventId) lưu trong record; ví dụ
seqở trên là biến thể minh hoạ).1127 - Cần cơ chế dựng lại view khi đổi schema hoặc sửa lỗi. Broker như Kafka chỉ giữ event theo thời hạn cấu hình nên phải đọc cả event đã lưu trữ (ví dụ trên S3), và dựng tăng dần bằng snapshot cộng event sau đó.28
Một dạng đơn giản của ý tưởng này là fan-out trong news feed: Grokking ch.32 lưu mapping <Post_ID, User_ID> trong newsfeed cache, dựng feed từ cache bài và user rồi đẩy tới client theo một cách fan-out.29
Scale theo quy mô
Giả định chung của khoá học: tỉ lệ đọc/ghi 100:1, mỗi user hoạt động khoảng 20 lượt đọc API mỗi ngày, đỉnh ×5. Mọi con số là ước tính, ngưỡng thiết kế theo quy tắc ước lượng của khoá học.30
| Quy mô | QPS đọc đỉnh | Thiết kế đọc |
|---|---|---|
| ~10k user | 200k lượt/ngày ≈ 2,3 QPS trung bình, ~12 QPS đỉnh | một Postgres, index đúng. Không cần cache. CDN cho asset tĩnh |
| ~1M user | 20M lượt/ngày ≈ 230 QPS, ~1.200 QPS đỉnh | Postgres primary + 1–2 replica, Redis một node (vài GB) cho object nóng, CDN cho trang công khai. 2–3 instance API |
| 50M+ user | 1 tỷ lượt/ngày ≈ 11.600 QPS, ~60.000 QPS đỉnh | CDN gánh phần lớn nội dung công khai. Cụm Redis nhiều node, hit rate mục tiêu 95%+, cache trong process cho hot key. DB shard, mỗi shard có replica. View CQRS cho feed, tìm kiếm, lịch sử |
Một phép tính đáng nói ở mức 50M+: 60.000 QPS với hit rate 95% nghĩa là 3.000 QPS xuống database, chia cho vài shard có replica thì mỗi máy chỉ vài trăm QPS. Nhưng nếu cụm Redis mất một nửa và hit rate tụt xuống 50%, database nhận 30.000 QPS và sụp. Cache ở quy mô này là một phần của năng lực hệ thống, phải được dự phòng như database.
RAM cho Redis: 10 triệu object nóng, key 30 B, value 1 KB, theo công thức của khoá học (quy tắc ước lượng của khoá học):30 10M × (30 + 1.000 + 100) × 1,3 ≈ 15 GB. Một vài node là đủ, thêm replica cho mỗi node.
Trong phỏng vấn: Đi theo thứ tự rẻ tới đắt và nói rõ con số. "Ở 1.200 QPS đỉnh, một primary và hai replica đủ, mình thêm Redis cho trang sản phẩm vì nó chiếm 70% lượt đọc. Giỏ hàng và thanh toán luôn đọc primary." Người phỏng vấn đánh giá cao việc bạn biết chỗ nào không được cache.
Sai lầm hay gặp: Thêm cache trước khi kiểm tra index. Cache che một query 800 ms, cho tới lúc cache trống sau deploy và database nhận toàn bộ query 800 ms cùng lúc.
Bài nào dùng pattern này
Các bài dưới đây áp dụng pattern; nền tảng lý thuyết xem các chương Grokking về cache, CDN và newsfeed.31
- URL shortener: tỉ lệ đọc/ghi cực lệch, cache và CDN cho redirect.
- News feed và Instagram: feed tính sẵn, hot key với tài khoản nổi tiếng.
- Yelp: read replica, cache theo vùng, view tìm kiếm.
- YouTube: CDN cho video và metadata, đếm lượt xem tách riêng.
- Tìm kiếm bài đăng và Tổng hợp tin tức: read model trong search index.
- Flash sale: trang sản phẩm nóng, chống cache stampede khi mở bán.
- Distributed cache: tự thiết kế tầng cache.
Đọc thêm: Caching, Database index, Sharding, Redis, Elasticsearch, Change Data Capture, Scale ghi.
Nguồn trích dẫn
-
Building Microservices (file .zip, tải về giải nén) · ch.11 Microservices at Scale · mục "Scaling Databases" và "Scaling for Reads" (tr. 221–223). ↩ ↩2 ↩3
-
Building Microservices (file .zip, tải về giải nén) · ch.11 · mục "Go Bigger" (tr. 217). ↩ ↩2 ↩3
-
Grokking Modern System Design · ch.4 Non-functional System Characteristics · mục "Scalability", phần "Vertical scalability" và "Horizontal scalability". ↩ ↩2
-
Grokking Modern System Design · ch.11 Content Delivery Network (CDN) · mục "Problem statement" (độ trễ khứ hồi US East tới Cape Town khoảng 225 ms). ↩
-
MongoDB: The Definitive Guide 2e (file .zip, tải về giải nén) · ch.5 Indexing · mục "Introduction to Indexing" và "Introduction to Compound Indexes" (tr. 81–89; quy tắc "exact matches first, ranges last"). ↩
-
MongoDB: The Definitive Guide 2e (file .zip, tải về giải nén) · ch.4 Querying · mục "Avoiding Large Skips" và "Paginating results without skip" (tr. 70). ↩
-
MongoDB: The Definitive Guide 2e (file .zip, tải về giải nén) · ch.7 Aggregation · mục "count" (tr. 146). ↩
-
Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations · mục "Standard numbers to remember" (bảng "Important Rates": MySQL 1.000 QPS, cache server 100.000 tới 1 triệu QPS). ↩ ↩2
-
Grokking Modern System Design · ch.9 Databases · bài "Data Replication" · mục "Synchronous versus asynchronous replication" và "Single leader/primary-secondary replication". ↩ ↩2 ↩3
-
MongoDB: The Definitive Guide 2e (file .zip, tải về giải nén) · ch.11 Connecting to a Replica Set from Your Application · mục "Sending Reads to Secondaries" (tr. 205). ↩ ↩2 ↩3
-
Microservices Patterns (file .zip, tải về giải nén) · ch.7 · §7.2.4 "The drawbacks of CQRS" (Dealing with the replication lag) và §7.3.2 "Enabling a client application to use an eventually consistent view" (tr. 236, 241). ↩ ↩2 ↩3
-
Chưa có nguồn trong thư viện: bảng các cách read-your-writes (cờ last_write_at, đọc theo LSN) là gợi ý thiết kế của khoá học; thư viện chỉ có hai cách trong footnote trước. ↩
-
Grokking Modern System Design · ch.16 Distributed Cache · bài mở đầu · mục "Problem statement" và bảng "Caching at Different Layers of a System". ↩ ↩2
-
Building Microservices (file .zip, tải về giải nén) · ch.11 · mục "Caching" và "Client-Side, Proxy, and Server-Side Caching" (tr. 225–226). ↩
-
Redis in Action · ch.2 · §2.3 Web page caching (tr. 30–31). ↩
-
Grokking Modern System Design · ch.16 Distributed Cache · bài "Background of Distributed Cache" · mục "Cache invalidation" (TTL, active/passive expiration). ↩
-
Chưa có nguồn trong thư viện: jitter TTL, single-flight, stale-while-revalidate, lock chống stampede và giá trị header cụ thể là thực hành của khoá học, không có đoạn sách tương ứng. ↩ ↩2 ↩3
-
Building Microservices (file .zip, tải về giải nén) · ch.11 · mục "Caching in HTTP" (tr. 226–228). ↩
-
Grokking Modern System Design · ch.11 CDN · mục "Content consistency in CDN" (Time-to-live) và "Evaluation" (Performance). ↩
-
Building Microservices (file .zip, tải về giải nén) · ch.11 · mục "Hiding the Origin" (tr. 228–229). ↩ ↩2
-
Redis in Action · ch.6 · §6.2 Distributed locking (tr. 116–126). ↩
-
Grokking Modern System Design · ch.16 Distributed Cache · bài "Evaluation of a Distributed Cache Design" · mục "Scalability". ↩
-
Microservices Patterns (file .zip, tải về giải nén) · ch.7 · §7.1.5 "The benefits and drawbacks of the API composition pattern" (tr. 227–228). ↩ ↩2
-
Microservices Patterns (file .zip, tải về giải nén) · ch.7 · §7.2 "Using the CQRS pattern", §7.2.3 "The benefits of CQRS" (tr. 235–236). ↩ ↩2
-
Microservices Patterns (file .zip, tải về giải nén) · ch.7 · §7.3.1 "Choosing a view datastore", bảng 7.1 "Query-side view stores" (tr. 237–238). ↩
-
Redis in Action · ch.8 Building a simple social network · §8.2 Home timeline (tr. 188). ↩
-
Microservices Patterns (file .zip, tải về giải nén) · ch.7 · §7.3.2 "Data access module design" · mục "Idempotent event handlers" (tr. 240–241). ↩
-
Microservices Patterns (file .zip, tải về giải nén) · ch.7 · §7.3.3 "Adding and updating CQRS views" (tr. 241–242). ↩
-
Grokking Modern System Design · ch.32 Design a Newsfeed System · mục "The newsfeed publishing service". ↩
-
Quy tắc ước lượng của khoá học · /hoc/chon-server (hệ số tải đỉnh, RAM Redis ×1,3). ↩ ↩2
-
Grokking Modern System Design · ch.16 Distributed Cache; ch.11 CDN; ch.32 Design a Newsfeed System. ↩