Caching, CDN & proxy — đưa nội dung tới gần user
Các tầng cache từ browser tới origin, Cache-Control vs ETag validation, CDN edge hit/miss, cache invalidation và phân biệt forward vs reverse proxy. Hiểu vì sao một deploy sai header khiến user thấy nội dung cũ hàng tháng.
TL;DR: Cache là bản sao tạm của response đặt gần người dùng hơn origin server, để lần sau khỏi phải đi xa lấy lại. HTTP có nhiều tầng cache xếp chồng: browser cache (trên máy user) → CDN edge (gần user về địa lý) → reverse proxy → origin. Server điều khiển cache bằng hai cơ chế: freshness (Cache-Control: max-age — dùng lại không cần hỏi trong N giây) và validation (ETag + If-None-Match — hỏi server "còn mới không?", nếu còn nhận 304 Not Modified rỗng). CDN nhân bản cơ chế này ở hàng trăm điểm edge. Pitfall lớn nhất: cache nội dung động quá lâu (user thấy data cũ) hoặc invalidation sai khiến phải chờ hết TTL. Phân biệt forward proxy (đại diện cho client) và reverse proxy (đại diện cho server) — hai thứ ngược hướng dù cùng tên "proxy".
Ở bài 02 — HTTP headers quan trọng bạn đã gặp Cache-Control và ETag như các header riêng lẻ. Bài này ghép chúng thành một bức tranh hoàn chỉnh: dữ liệu được cache ở đâu, ai quyết định cache bao lâu, và làm sao xoá cache khi nội dung đổi. Đây là kiến thức nền cho bài 08 — reverse proxy và bài 09 — load balancer.
1. Analogy — Tủ lạnh, cửa hàng tiện lợi và kho trung tâm
Hình dung bạn cần một chai nước:
- Tủ lạnh nhà bạn (browser cache): nếu đã có sẵn, lấy ngay, không tốn công đi đâu. Nhưng nước để lâu có thể hết hạn — bạn cần biết hạn dùng.
- Cửa hàng tiện lợi đầu ngõ (CDN edge): nếu tủ lạnh hết, ra cửa hàng gần — vài phút. Cửa hàng nhập sẵn hàng phổ biến, không phải gọi về kho mỗi lần.
- Kho trung tâm của hãng (origin server): chỉ khi cửa hàng cũng hết mới phải gọi về kho — xa nhất, chậm nhất, đắt nhất.
Mỗi tầng giữ một bản sao gần người mua hơn. Câu hỏi cốt lõi: bản sao đó còn dùng được không, hay đã cũ? Đó chính là bài toán cache invalidation.
| Đời thường | Web cache |
|---|---|
| Tủ lạnh nhà | Browser cache (private, trên máy user) |
| Hạn sử dụng in trên chai | Cache-Control: max-age (freshness) |
| Gọi hỏi "lô này còn tốt không?" | ETag + If-None-Match (validation) |
| Cửa hàng tiện lợi gần nhà | CDN edge node |
| Kho trung tâm | Origin server |
| Thu hồi sản phẩm lỗi | Cache invalidation / purge |
Freshness trả lời "còn hạn không?" (không cần hỏi ai). Validation trả lời "đã đổi chưa?" (hỏi server nhưng câu trả lời rất nhẹ). Cache nhanh nhất là cache không phải hỏi gì cả.
2. Các tầng cache — request đi qua đâu
Một request GET /logo.png có thể được phục vụ ở bất kỳ tầng nào — càng gần user càng nhanh:
Càng gần user, độ trễ càng thấp — cache hit ở tầng đầu là nhanh nhất
- Browser cache (private): lưu trên máy mỗi user, chỉ phục vụ chính user đó. Hit ở đây là nhanh nhất — không có request nào ra mạng.
- CDN edge (shared): mạng phân tán hàng trăm điểm gần user về địa lý. Một bản sao phục vụ mọi user trong khu vực.
- Reverse proxy cache (shared): Nginx/Varnish trước origin, giảm tải cho app server.
- Origin server: nguồn sự thật cuối cùng — chỉ chạm tới khi mọi tầng trên đều miss.
Hai loại cache cần phân biệt: private (chỉ cho một user — chứa dữ liệu cá nhân) và shared/public (phục vụ nhiều user — chỉ dành cho nội dung không nhạy cảm). Đặt sai loại là một pitfall nghiêm trọng (xem mục 7).
3. Freshness — Cache-Control: dùng lại bao lâu không cần hỏi
Cache-Control (RFC 9111) là header chính điều khiển cache. Server gắn nó vào response để nói cho mọi tầng cache biết được phép giữ bản sao bao lâu:
Cache-Control: public, max-age=31536000, immutable
Các directive hay dùng:
| Directive | Ý nghĩa |
|---|---|
max-age=N | Bản sao "fresh" trong N giây — dùng lại không cần hỏi origin |
public | Cache shared (CDN, proxy) được phép lưu |
private | Chỉ browser cache của user được lưu, CDN không được |
no-cache | Được lưu nhưng phải validate với server trước mỗi lần dùng |
no-store | Tuyệt đối không lưu (dữ liệu nhạy cảm: trang ngân hàng) |
immutable | Nội dung không bao giờ đổi — browser khỏi validate kể cả khi reload |
s-maxage=N | max-age riêng cho shared cache (CDN), ghi đè max-age |
Trong max-age, cache phục vụ bản sao ngay lập tức không hỏi origin. Đây là trạng thái nhanh nhất. Khi hết hạn, bản sao thành "stale" và cache phải validate (mục 4) hoặc lấy lại bản mới.
Tên gây hiểu nhầm: no-cache nghĩa là "lưu được, nhưng phải hỏi server trước mỗi lần dùng" — tức luôn validate. Muốn cấm lưu hoàn toàn phải dùng no-store. Nhầm hai cái này khiến dữ liệu nhạy cảm bị lưu trên đĩa, hoặc nội dung tĩnh bị validate mỗi request không cần thiết.
4. Validation — ETag và 304 Not Modified
Khi bản sao hết hạn (stale), cache không vứt đi ngay — nó hỏi origin "bản tôi có còn dùng được không?". Nếu còn, origin trả về 304 Not Modified rỗng (không có body) — tiết kiệm toàn bộ băng thông truyền lại nội dung.
Cơ chế dựa trên ETag (Entity Tag) — một fingerprint của nội dung (thường là hash):
# Lan dau, origin tra:
HTTP/1.1 200 OK
ETag: "a1b2c3d4"
Cache-Control: max-age=60
# Sau 60s (stale), browser hoi lai voi ETag da co:
GET /api/profile HTTP/1.1
If-None-Match: "a1b2c3d4"
# Neu noi dung KHONG doi, origin tra rong:
HTTP/1.1 304 Not Modified
ETag: "a1b2c3d4"
sequenceDiagram
participant B as Browser cache
participant O as Origin
Note over B: Ban sao da stale (het max-age)
B->>O: GET /api/profile (If-None-Match: "a1b2c3d4")
alt Noi dung khong doi
O-->>B: 304 Not Modified (rong, ~200 byte)
Note over B: Dung lai ban cu, gia han freshness
else Noi dung da doi
O-->>B: 200 OK (body moi + ETag moi)
Note over B: Thay the ban cu
endNgoài ETag/If-None-Match, còn cặp cũ hơn Last-Modified/If-Modified-Since dựa trên timestamp. ETag chính xác hơn vì so theo nội dung, không phụ thuộc đồng hồ — nhưng tốn chi phí tính hash. Validation không nhanh bằng freshness vì vẫn phải đi một round-trip tới origin; lợi ích là tiết kiệm băng thông body khi nội dung lớn mà không đổi.
5. CDN — nhân bản cache ở hàng trăm điểm edge
CDN (Content Delivery Network) là mạng các server đặt phân tán khắp thế giới (edge node / Point of Presence). Khi user yêu cầu một tài nguyên, request đi tới edge gần nhất về địa lý thay vì origin có thể ở cách nửa vòng trái đất.
CDN làm hai việc chính:
- Giảm latency: edge gần user → RTT thấp (xem bài 05 module 3 — latency vs throughput về vì sao khoảng cách quyết định độ trễ).
- Giảm tải origin: phần lớn request được phục vụ tại edge, origin chỉ nhận khi edge miss.
Cache hit vs miss tại edge — CDN báo qua header (mỗi nhà cung cấp tên khác nhau):
# Cloudflare:
CF-Cache-Status: HIT # phuc vu tu edge
CF-Cache-Status: MISS # edge phai goi ve origin
# Fastly / Varnish:
X-Cache: HIT
Age: 42 # ban sao da nam o edge 42 giay
- HIT: edge có sẵn bản fresh → trả ngay, không chạm origin.
- MISS: edge không có (hoặc stale) → gọi origin, lưu lại, rồi trả về. Request kế cùng tài nguyên sẽ HIT.
CDN quyết định "hai request có cùng tài nguyên không" bằng cache key — mặc định là URL. Nếu response thay đổi theo header (vd Accept-Encoding, Accept-Language), server phải khai báo Vary: Accept-Encoding để CDN tách cache theo từng biến thể. Quên Vary khiến user nhận bản nén gzip dù browser không hỗ trợ — lỗi khó debug.
6. Cache invalidation — bài toán khó nhất
Có câu nói nổi tiếng trong ngành: "Hai vấn đề khó nhất khoa học máy tính là cache invalidation và đặt tên biến." Khi nội dung đổi nhưng cache vẫn giữ bản cũ, user thấy data lỗi thời. Ba chiến lược xử lý:
1. TTL hết hạn (thụ động): đặt max-age ngắn, chấp nhận user thấy bản cũ tối đa N giây. Đơn giản nhưng không tức thì.
2. Purge/invalidate chủ động: gọi API của CDN để xoá một URL khỏi cache ngay khi nội dung đổi. Chính xác nhưng phải tích hợp vào luồng deploy/update.
3. Cache busting bằng versioned URL (phổ biến nhất cho asset tĩnh): nhúng hash nội dung vào tên file. Khi nội dung đổi, tên file đổi → URL mới → cache cũ tự nhiên không còn được tham chiếu:
<!-- Build tool tao ten file kem hash noi dung -->
<script src="/app.3f8a9c.js"></script>
<!-- Doi noi dung -> hash doi -> URL moi -->
<script src="/app.7b2e1d.js"></script>
Đây là lý do file tĩnh (JS/CSS/ảnh) thường đặt max-age=31536000, immutable (một năm): URL có hash, không bao giờ cần invalidate — chỉ cần đổi tên khi nội dung đổi. Còn HTML trỏ tới chúng thì đặt no-cache để luôn lấy bản mới nhất.
7. Forward proxy vs reverse proxy — cùng tên, ngược hướng
"Proxy" là một trung gian chuyển tiếp request. Nhưng đứng về phía ai quyết định nó là loại nào:
| Forward proxy | Reverse proxy | |
|---|---|---|
| Đại diện cho | Client | Server |
| Client biết có proxy? | Có — client tự cấu hình | Không — client tưởng đang nói chuyện thẳng với server |
| Đặt ở đâu | Gần client (mạng công ty, ISP) | Gần server (datacenter) |
| Mục đích | Lọc nội dung, ẩn IP client, cache cho nhóm user | TLS termination, load balancing, cache, bảo vệ origin |
| Ví dụ | Proxy công ty chặn web, VPN | Nginx, Cloudflare, HAProxy |
flowchart LR
subgraph Forward["Forward proxy - dai dien client"]
C1["Client"] --> FP["Forward proxy"] --> I1["Internet"]
end
subgraph Reverse["Reverse proxy - dai dien server"]
C2["Client"] --> I2["Internet"] --> RP["Reverse proxy"] --> S2["Origin"]
endCùng là cache trung gian, nhưng forward proxy cache cho một nhóm client (mọi nhân viên công ty tải cùng file chỉ tốn băng thông internet một lần), còn reverse proxy cache trước một nhóm server (giảm tải cho origin). Bài 08 đào sâu reverse proxy với Nginx.
8. Pitfall — lỗi cache thường gặp
❌ Nhầm 1: Cache nội dung động bằng public, max-age dài
✅ Một API trả dữ liệu cá nhân (/api/profile) nếu lỡ đặt Cache-Control: public, max-age=3600 → CDN lưu bản của user A và trả cho user B. Đây là lỗi rò rỉ dữ liệu nghiêm trọng. Dữ liệu per-user phải dùng private (chỉ browser) hoặc no-store.
❌ Nhầm 2: Quên Vary khi response phụ thuộc header
✅ Server trả nội dung khác nhau theo Accept-Language nhưng không đặt Vary: Accept-Language → CDN cache bản tiếng Anh và trả cho user yêu cầu tiếng Việt. Khai báo Vary để CDN tách cache theo từng biến thể.
❌ Nhầm 3: Thundering herd khi cache hết hạn đồng loạt
✅ Nếu một tài nguyên hot có max-age=60 và hết hạn đúng lúc 10.000 request đang đổ vào, tất cả cùng miss và cùng gọi origin trong một khoảnh khắc — origin sập. Đây là thundering herd (hay cache stampede). Fix: CDN coalescing (gộp các request miss đồng thời thành một request tới origin — Nginx có proxy_cache_lock), thêm jitter ngẫu nhiên vào TTL, hoặc dùng stale-while-revalidate (trả bản cũ trong khi âm thầm lấy bản mới ở background).
❌ Nhầm 4: Tin rằng deploy xong là user thấy ngay
✅ Bạn sửa bug, deploy. Nhưng index.html đặt max-age=86400 → browser user vẫn dùng bản HTML cũ trỏ tới JS cũ suốt một ngày. HTML entry point nên no-cache (luôn validate), chỉ asset có hash mới cache dài. Sai cấu hình này khiến hotfix "không có tác dụng" với user cũ.
9. Liên hệ các bài khác
- Bài 02 — HTTP headers quan trọng:
Cache-Control,ETag,Varylần đầu xuất hiện ở đó như header riêng lẻ — bài này ghép chúng thành cơ chế cache hoàn chỉnh. - Bài 03 — Cookie, session & CORS: response có cookie cá nhân hoá KHÔNG được cache shared — liên quan trực tiếp tới pitfall
publicvsprivate. - Bài 08 — Reverse proxy với Nginx: reverse proxy cache (
proxy_cache) là hiện thực cụ thể của tầng cache mục 2 và 7 — Nginx vừa proxy vừa cache. - Bài 09 — Load balancer L4 vs L7: CDN edge và reverse proxy đều phải phân tải khi có nhiều origin — load balancing là mảnh ghép tiếp theo.
- Module 3 bài 05 — Latency vs throughput: vì sao đặt edge gần user giảm latency — RTT tỉ lệ với khoảng cách vật lý.
10. 📚 Deep Dive — tài liệu gốc
Spec chính thức:
- RFC 9111 — HTTP Caching — bản hợp nhất mới nhất về
Cache-Control, freshness, validation,Vary. Section 5.2 liệt kê mọi directive. - RFC 9110 §8.8 — Validators —
ETag,Last-Modifiedvà ngữ nghĩa strong/weak validator. - MDN — HTTP Caching — hướng dẫn thực dụng, nhiều ví dụ cấu hình theo loại tài nguyên.
- stale-while-revalidate (RFC 5861) — directive trả bản cũ trong khi revalidate background, giảm thundering herd.
Ghi chú: RFC 9111 Section 4.2 (Freshness) và 4.3 (Validation) là hai phần đáng đọc nhất — giải thích chính xác cache tính tuổi (Age) và quyết định fresh/stale thế nào.
11. Tóm tắt
- Cache là bản sao tạm đặt gần user hơn origin; HTTP có nhiều tầng xếp chồng: browser → CDN edge → reverse proxy → origin.
- Freshness (
Cache-Control: max-age) cho dùng lại không cần hỏi — nhanh nhất. Validation (ETag+If-None-Match→304) hỏi server nhưng tiết kiệm băng thông body. no-cache= phải validate trước mỗi lần dùng;no-store= cấm lưu hoàn toàn — đừng nhầm.- Phân biệt private (chỉ một user) và public/shared (CDN, proxy) — đặt sai gây rò rỉ dữ liệu cá nhân.
- CDN nhân bản cache ở edge gần user, báo
HIT/MISSqua header; cache key mặc định là URL, dùngVaryđể tách biến thể. - Cache invalidation: TTL thụ động, purge chủ động, hoặc versioned URL (hash trong tên file) — asset tĩnh
immutable, HTMLno-cache. - Forward proxy đại diện client (gần client); reverse proxy đại diện server (gần server) — cùng tên, ngược hướng.
- Pitfall lớn: cache nội dung động quá lâu, quên
Vary, thundering herd khi hết hạn đồng loạt, và HTML cache dài làm hotfix không tới user.
12. Tự kiểm tra
Q1Phân biệt freshness và validation. Vì sao một cache hit theo freshness nhanh hơn một response 304?▸
Cache-Control: max-age — trong khoảng thời gian đó, cache phục vụ bản sao ngay lập tức, không liên lạc origin. Validation xảy ra khi bản sao đã stale: cache gửi If-None-Match kèm ETag tới origin để hỏi "còn dùng được không?". Nếu còn, origin trả 304 Not Modified rỗng. Freshness nhanh hơn vì không có round-trip nào ra mạng — còn validation vẫn tốn một RTT tới origin, chỉ tiết kiệm được phần body (không phải truyền lại nội dung lớn). Cache nhanh nhất là cache không phải hỏi gì cả.Q2Một API /api/me trả thông tin tài khoản của user đang đăng nhập. Đặt Cache-Control thế nào, và điều gì xảy ra nếu lỡ đặt public, max-age=600?▸
Cache-Control: private, no-store (hoặc tối thiểu private nếu chấp nhận browser cache). Nếu lỡ đặt public, max-age=600: một shared cache (CDN, reverse proxy) sẽ lưu response của user A và trả lại cho mọi user khác hỏi cùng URL trong 600 giây — user B thấy thông tin tài khoản của user A. Đây là lỗi rò rỉ dữ liệu nghiêm trọng. Quy tắc: dữ liệu per-user không bao giờ được cache ở tầng shared.Q3Vì sao asset tĩnh thường đặt max-age=31536000, immutable còn index.html lại no-cache? Cơ chế nào khiến cấu hình này an toàn?▸
app.3f8a9c.js). Khi nội dung đổi, hash đổi → URL mới hoàn toàn → cache cũ không còn được tham chiếu, không cần invalidate. Vì URL cố định luôn trỏ tới đúng một phiên bản nội dung, đặt immutable + một năm là an toàn (browser khỏi validate kể cả khi reload). Còn index.html là entry point — nó chứa danh sách URL asset hiện tại. Nếu cache HTML dài, user sẽ giữ danh sách URL cũ → trỏ tới JS cũ → hotfix không tới. Đặt no-cache cho HTML để browser luôn validate và nhận danh sách asset mới nhất.Q4CDN trả header CF-Cache-Status: MISS cho request đầu rồi HIT cho request sau cùng URL. Giải thích cơ chế, và vì sao đôi khi vẫn MISS dù vừa truy cập?▸
Cache-Control phù hợp, hoặc có Set-Cookie); (4) cache key khác do Vary trên header mà request này mang giá trị mới.Q5Thundering herd (cache stampede) là gì? Một tài nguyên hot với max-age=60 có thể làm sập origin thế nào, và cách phòng?▸
max-age=60, cứ mỗi 60 giây lại có một đợt bùng nổ request đồng thời tới origin — nếu là 10.000 request/giây, origin có thể quá tải và sập. Cách phòng: (1) request coalescing — cache gộp các miss đồng thời thành một request tới origin (Nginx proxy_cache_lock); (2) jitter — thêm thời gian ngẫu nhiên vào TTL để các bản sao không hết hạn cùng lúc; (3) stale-while-revalidate — trả bản cũ ngay trong khi âm thầm lấy bản mới ở background, user không phải chờ.Q6Forward proxy và reverse proxy đều chuyển tiếp request và đều có thể cache. Khác biệt cốt lõi là gì? Cho một ví dụ mỗi loại.▸
Q7Vì sao server cần đặt header Vary, và điều gì xảy ra nếu một endpoint trả nội dung theo Accept-Language nhưng quên Vary: Accept-Language?▸
Vary cho cache biết response phụ thuộc vào giá trị của những request header nào — cache phải tách bản lưu riêng cho từng giá trị đó. Mặc định cache key chỉ là URL. Nếu một endpoint trả tiếng Việt hay tiếng Anh tuỳ Accept-Language nhưng quên Vary: Accept-Language: cache lưu bản đầu tiên nó thấy (giả sử tiếng Anh) gắn với URL đó, rồi trả bản tiếng Anh cho mọi user — kể cả user gửi Accept-Language: vi. Lỗi này khó debug vì phụ thuộc bản nào tới cache trước. Khai báo Vary đúng để cache tách biến thể theo header.Bài tiếp theo: Reverse proxy với Nginx — cửa ngõ trước backend
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên