TCP, HTTP & Web cho Backend/Caching, CDN & proxy — đưa nội dung tới gần user
18/29
Bài 18 / 29~22 phútHTTP & WebMiễn phí lượt xem

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-ControlETag 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 proxybà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ườngWeb cache
Tủ lạnh nhàBrowser cache (private, trên máy user)
Hạn sử dụng in trên chaiCache-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âmOrigin server
Thu hồi sản phẩm lỗiCache invalidation / purge
💡 Cách nhớ

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:

Browser
Browser cache
0ms
CDN
CDN edge
~10ms
Reverse proxy
Reverse proxy
~50ms
Origin
Origin server

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=NBản sao "fresh" trong N giây — dùng lại không cần hỏi origin
publicCache shared (CDN, proxy) được phép lưu
privateChỉ 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-storeTuyệt đối không lưu (dữ liệu nhạy cảm: trang ngân hàng)
immutableNội dung không bao giờ đổi — browser khỏi validate kể cả khi reload
s-maxage=Nmax-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.

⚠️ no-cache không có nghĩa là không cache

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
    end

Ngoà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:

  1. 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ễ).
  2. 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.
📌 Cache key — CDN phân biệt response thế nào

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 proxyReverse proxy
Đại diện choClientServer
Client biết có proxy?Có — client tự cấu hìnhKhông — client tưởng đang nói chuyện thẳng với server
Đặt ở đâuGần client (mạng công ty, ISP)Gần server (datacenter)
Mục đíchLọc nội dung, ẩn IP client, cache cho nhóm userTLS termination, load balancing, cache, bảo vệ origin
Ví dụProxy công ty chặn web, VPNNginx, 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"]
    end

Cù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

10. 📚 Deep Dive — tài liệu gốc

📚 RFC và tài liệu caching

Spec chính thức:

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-Match304) 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/MISS qua header; cache key mặc định là URL, dùng Vary để 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, HTML no-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

Tự kiểm tra
Q1
Phân biệt freshness và validation. Vì sao một cache hit theo freshness nhanh hơn một response 304?
Freshness dựa trên 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ả.
Q2
Mộ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?
Phải đặt 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.
Q3
Vì 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?
Asset tĩnh (JS/CSS/ảnh) dùng versioned URL — tên file chứa hash nội dung (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.htmlentry 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.
Q4
CDN 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?
MISS: edge node không có bản fresh của tài nguyên → phải gọi về origin, lưu bản sao lại, rồi trả về. HIT: request sau cùng URL được phục vụ thẳng từ bản đã lưu tại edge, không chạm origin. Vì sao đôi khi vẫn MISS dù vừa truy cập: (1) CDN có nhiều edge node — request của bạn lần này có thể tới một POP khác chưa có bản sao; (2) bản sao đã hết TTL (stale) nên bị coi như miss; (3) response không cacheable (thiếu 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.
Q5
Thundering 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?
Thundering herd xảy ra khi một tài nguyên phổ biến hết hạn cache đúng lúc rất nhiều request đang đổ vào: tất cả cùng phát hiện bản sao stale, cùng MISS, và cùng gọi origin trong một khoảnh khắc. Với 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ờ.
Q6
Forward 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.
Khác biệt nằm ở đại diện cho phía nào. Forward proxy đại diện cho client: đặt gần client, client tự cấu hình để đi qua nó, dùng để lọc nội dung / ẩn IP client / cache cho một nhóm user (ví dụ: proxy công ty chặn web, hoặc VPN). Reverse proxy đại diện cho server: đặt gần origin, client không biết nó tồn tại (tưởng đang nói thẳng với server), dùng để TLS termination / load balancing / cache / bảo vệ origin (ví dụ: Nginx, Cloudflare, HAProxy). Cùng tên "proxy" nhưng ngược hướng: forward bảo vệ và phục vụ client, reverse bảo vệ và phục vụ server.
Q7
Vì 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

Đặt 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

Bài tiếp theo

Reverse proxy với Nginx — cửa ngõ trước backend