CDN cho static assets và cache busting
Bài trước bạn đã đặt Cache-Control, ETag, Vary lên response và biết rằng max-age dài thì nhanh nhưng dễ cũ. Bài này lấy đúng bộ header đó áp cho static assets (JS, CSS, ảnh, font) khi có CDN ở giữa, và trả lời câu hỏi khó chịu nhất: làm sao vừa cache một năm vừa phát hành bản mới ngay khi deploy?
Mục tiêu bài này:
- Giải thích CDN làm gì với static assets, bằng ví dụ tủ lạnh.
- Đặt
max-agedài kèmimmutablecho file có hash trong tên. - Đặt hash nội dung vào tên file để mỗi bản phát hành là một URL mới.
- Biết vì sao
index.htmlphải đi con đường ngược lại. - Tránh bẫy
?v=2và biết thứ tự deploy an toàn.
CDN là tủ lạnh đặt ở mỗi tòa nhà
Origin server là cái chợ. CDN là tủ lạnh đặt ở tầng trệt của mỗi tòa nhà (edge). Lần đầu ai đó cần sữa thì vẫn phải đi chợ, tức cache miss, edge về origin lấy hàng. Những lần sau mở tủ là có, tức cache hit tại edge, và user ở Hà Nội không phải chờ gói tin đi vòng sang Mỹ.
Static assets hợp với mô hình này hơn bất cứ thứ gì: cùng một nội dung cho mọi user (không phụ thuộc cookie hay user id), ít thay đổi, nhưng được đọc rất nhiều lần.
flowchart TD
Browser["👤<br/>Trình duyệt"] --> Edge{"🧊<br/>Edge cache"}
Edge -->|"HIT"| Browser
Edge -->|"MISS"| Origin["🏠<br/>Origin server"]
Origin --> Edge
Vì cache key ở edge là URL (cộng thêm vài header trong Vary), nên câu hỏi thiết kế thật ra rất gọn: làm sao để nội dung mới có URL mới.
Cache lâu và phát hành nhanh: tách bằng URL
Hai mong muốn này đối nghịch nhau nếu bạn gắn cache vào một URL cố định. max-age dài thì user không thấy bản mới, max-age ngắn thì mất lợi ích của CDN. Cách thoát là đổi chỗ neo: không neo cache vào "file tên gì" mà neo vào "nội dung nào".
Nếu tên file chứa hash của nội dung, ví dụ app.3f9c1a2b.js, thì khi code đổi, hash đổi, URL đổi. File cũ vẫn nằm trong cache nhưng không còn ai trỏ tới. Với URL như vậy bạn được phép đặt:
Cache-Control: public, max-age=31536000, immutable
max-age=31536000là _____ (31.536.000 giây)._____báo trình duyệt rằng trong khoảng đó đừng revalidate, kể cả khi user bấm F5, vì nội dung sau URL này sẽ không bao giờ đổi.
Đây là điểm khác biệt lớn so với bài trước: ETag vẫn hữu ích, nhưng với asset có hash thì ta cố tình không muốn phát sinh request revalidate nào cả.
Hash trong tên file: cache busting bền vững
Cache busting nghĩa là làm cho URL thay đổi khi nội dung thay đổi, để "đập" cache một cách có chủ đích. Tên file nên chứa _____ chứ không phải số version do người viết tay, vì hash chỉ đổi khi file thật sự đổi.
Một bảng nhỏ đủ dùng cho hầu hết dự án:
| Asset | Header gợi ý | Lý do |
|---|---|---|
index.html |
no-cache |
luôn revalidate, phải thấy hash mới |
app.<hash>.js, style.<hash>.css |
public, max-age=31536000, immutable |
URL bất biến |
logo.<hash>.png, font |
như trên | ảnh và font gần như không đổi |
favicon.ico (không hash) |
public, max-age=86400 |
chấp nhận trễ một ngày |
| API response | no-store hoặc TTL ngắn |
dữ liệu riêng của từng user |
HTML thì phải đi ngược lại
index.html là thứ trỏ tới các hash. Nếu HTML bị cache một năm, user sẽ mãi thấy HTML cũ trỏ tới app.3f9c1a2b.js cũ, và bản deploy mới coi như vô hình. Nên HTML dùng _____, nghĩa là vẫn được lưu nhưng mỗi lần dùng phải hỏi lại server. Nhờ ETag, lần hỏi lại đó thường chỉ là một 304 Not Modified vài chục byte.
Đừng nhầm no-cache với no-store: no-cache cho phép lưu và bắt revalidate, còn no-store là không lưu gì cả, và bạn mất luôn lợi ích 304.
Bẫy ?v=2 và thứ tự deploy
Cách cũ là style.css?v=2. Nó chạy được, nhưng nhiều CDN được cấu hình bỏ qua query string khi tính cache key (để gộp các URL tracking), nên ?v=2 vẫn trả bản cũ. Hash trong path không bị bẫy này.
Thứ tự deploy cũng là một phần của cache busting. Asset phải lên _____, HTML lên sau:
- Build sinh ra
app.3f9c1a2b.jsvới hash mới - Upload asset mới lên origin, để CDN kéo về với max-age dài
- Kiểm tra asset mới trả 200 từ CDN
- Cuối cùng upload
index.htmlmới trỏ tới hash mới - Giữ asset cũ trên CDN thêm vài ngày, đừng xóa ngay
Lý do của thứ tự này rất thực tế: nếu HTML mới lên trước, user tải HTML trỏ tới app.3f9c1a2b.js trong khi file đó chưa tồn tại, kết quả là 404 và trang trắng. Ngược lại thì luôn an toàn vì HTML cũ vẫn trỏ tới file cũ còn đó.
Ví dụ rollback: một user đang mở tab từ sáng, HTML cũ trỏ app.1a2b3c4d.js. Bạn deploy bản mới rồi xóa asset cũ. User F5 và nhận 404. Giữ asset cũ thì rollback chỉ đơn giản là đưa index.html cũ trở lại.
Kiểm tra bằng mắt: đọc header từ CDN
Cache busting sai thường không báo lỗi, nó chỉ lặng lẽ trả bản cũ. Cách nhanh nhất là nhìn header:
$ curl -I https://cdn.example.com/app.3f9c1a2b.js
cache-control: public, max-age=31536000, immutable
x-cache: HIT
age: 84321
x-cache: HIT nghĩa là edge đang phục vụ từ cache, không về origin. age: 84321 là số giây object đã nằm trong cache, so với max-age để biết khi nào nó hết hạn. Nếu bạn thấy age gần bằng max-age trong khi vừa deploy bản mới, gần như chắc chắn bạn đang thiếu hash trong tên file.
Những lỗi khiến CDN vẫn trả bản cũ
- Đặt
immutablecho file không có hash. User kẹt bản cũ suốt một năm và F5 không cứu được, vìimmutablebảo trình duyệt đừng hỏi lại. Quy tắc: tên file không đổi thì đừng chomax-agedài. - Xóa asset cũ ngay sau khi deploy. Tab đang mở và bản rollback đều cần file cũ. Giữ ít nhất vài ngày, hoặc theo TTL của
index.html. - Purge toàn bộ CDN mỗi lần deploy. Purge là van xả khẩn cấp, không phải quy trình. Nếu phải purge thường xuyên, vấn đề nằm ở tên file, không nằm ở CDN.
- Cache HTML quá lâu. Đây là lỗi tốn thời gian debug nhất, vì mọi thứ trên CDN đều đúng, chỉ có danh sách hash là cũ.
- Đoán thay vì đo. Trước khi kết luận, hãy
curl -Imột file asset và một file HTML, socache-controlvàx-cachevới bảng ở trên.
Chốt lại ba dòng
- Static assets nên có
max-agerất dài (một năm) kèmimmutable, và điều kiện bắt buộc là hash nội dung nằm trong tên file. index.htmldùngno-cacheđể luôn revalidate, nhờ đó user nhận được danh sách hash mới mà không phải tải lại toàn bộ.- Deploy theo thứ tự asset trước, HTML sau, giữ asset cũ vài ngày; cache busting là thiết kế URL, không phải thao tác purge.
Bài tiếp theo là checkpoint của module: bạn sẽ gặp lại TTL, invalidation theo sự kiện, Cache-Control, ETag, Vary và chính cách đặt hash này trong một bộ câu hỏi tình huống.
Front: Vì sao static assets là ứng viên số một cho CDN? Back: Cùng một nội dung cho mọi user, ít thay đổi, nhưng được đọc rất nhiều lần.
Front: max-age=31536000 tương đương bao lâu? Back: Một năm (31.536.000 giây).
Front: Header immutable nói gì với trình duyệt? Back: Đừng revalidate trong thời gian max-age, kể cả khi user bấm F5.
Front: Điều kiện bắt buộc để được đặt max-age một năm kèm immutable? Back: Tên file chứa hash nội dung, nên URL đó không bao giờ đổi nội dung.
Front: Vì sao index.html nên dùng no-cache? Back: Vì nó trỏ tới các hash mới, cần revalidate để user thấy bản deploy mới.
Front: no-cache khác no-store ở điểm nào? Back: no-cache vẫn lưu nhưng bắt revalidate; no-store không lưu gì cả.
Front: Cơ chế nào giúp lần revalidate của HTML thường chỉ tốn một 304? Back: ETag: server trả 304 Not Modified nếu nội dung chưa đổi.
Front: Cache busting bằng hash trong tên file khác ?v=2 thế nào? Back: Hash nằm trong path nên CDN không thể bỏ qua; nhiều CDN cấu hình bỏ query string khi tính cache key.
Front: Vì sao phải upload asset trước rồi mới upload HTML? Back: Tránh HTML mới trỏ tới file chưa tồn tại, gây 404 và trang trắng.
Front: Vì sao giữ asset cũ trên CDN thêm vài ngày? Back: Tab đang mở với HTML cũ vẫn trỏ tới file đó, và rollback cần nó.
Front: x-cache: HIT trong response của CDN nghĩa là gì? Back: Edge trả nội dung từ cache, không phải về origin.
Front: Header age: 84321 cho biết điều gì? Back: Object đã nằm trong cache 84.321 giây, so với max-age để biết khi nào hết hạn.
Front: Đặt immutable cho file không có hash thì hậu quả là gì? Back: User kẹt bản cũ suốt max-age và F5 không cứu được.