Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Caching strategy & CDN

Mở đầu — vì sao bài này quan trọng

Nếu tôi phải chọn ra một yếu tố duy nhất khiến các kết quả performance test bị sai lệch nhiều nhất, tôi sẽ không ngần ngại chỉ vào cache. Không phải scripting, không phải think time, không phải số lượng thread — mà là cache. Lý do rất đơn giản: cache là thứ quyết định phần lớn traffic của bạn có thực sự "chạm" tới backend hay không.

Hãy tưởng tượng bạn viết một script bắn 10.000 request/giây vào trang chủ của một sàn thương mại điện tử. Con số nghe rất hoành tráng. Nhưng nếu 95% số request đó được CDN trả về từ edge cache mà không hề đi tới server gốc, thì bài test của bạn đang đo... tốc độ của CDN, chứ không phải đo khả năng chịu tải của hệ thống. Ngược lại, nếu bạn cấu hình script vô tình bỏ qua toàn bộ cache (mỗi request là một URL độc nhất), backend sẽ "gánh" 100% tải trong khi thực tế production chỉ phải xử lý 5%. Kết quả: bạn hoảng loạn báo cáo "hệ thống sập ở 2.000 user" trong khi production thừa sức chịu 50.000 user.

Đây chính là bài học cốt lõi của Bài 32: cache không phải là chi tiết kỹ thuật phụ, mà là biến số trung tâm của mọi bài performance test. Test đúng cache nghĩa là test đúng thực tế. Test sai cache nghĩa là mọi con số phía sau đều vô nghĩa. Trong bài này chúng ta sẽ mổ xẻ các tầng cache, cách chúng ảnh hưởng tới throughput, và quan trọng nhất — cách thiết kế test để phản ánh đúng hành vi cache của production.

Khái niệm cốt lõi

Các tầng cache — một request đi qua bao nhiêu "trạm nghỉ"

Trước khi test, bạn phải hình dung được đường đi của một request và những nơi nó có thể bị "chặn lại" bởi cache:

User
  ↓
Browser cache (private)      ← cache riêng của từng người dùng
  ↓
CDN / Edge cache (shared)    ← Cloudflare, CloudFront, Fastly, Bunny...
  ↓
Reverse proxy cache          ← Nginx, Varnish
  ↓
Application cache            ← Redis, Memcached (cache tầng app)
  ↓
Database query cache / buffer pool  ← cache trong DB
  ↓
Database (nguồn sự thật)

Mỗi tầng có một đặc điểm quan trọng bạn cần nắm:

  • Browser cacheprivate — mỗi user có bản riêng. Trong load test dùng JMeter/k6 (là HTTP client, không phải trình duyệt thật), tầng này thường KHÔNG tồn tại trừ khi bạn chủ động mô phỏng. Đây là điểm dễ gây sai lệch: người dùng thật tải lại một trang thì logo, CSS, JS đã nằm sẵn trong browser cache; còn công cụ test thì tải lại tất cả mỗi lần.
  • CDN / Edge cacheshared — mọi user chung một bản. Đây là tầng quyết định lớn nhất tới throughput mà backend cảm nhận. Một tài nguyên được cache tốt ở CDN nghĩa là backend gần như không thấy tải.
  • Reverse proxy cache (Nginx proxy_cache, Varnish) nằm ngay trước app, cache những response động có thể tái sử dụng.
  • Application cache (Redis, Memcached) là nơi app lưu kết quả tính toán đắt đỏ: danh sách sản phẩm, session, kết quả query phức tạp.
  • Database cache (buffer pool của MySQL/PostgreSQL) giữ các trang dữ liệu nóng trong RAM.

Cache hit ratio — chỉ số cần đo trong performance test

Khái niệm quan trọng nhất là cache hit ratio — tỷ lệ request được phục vụ từ cache trên tổng số request. Nếu hit ratio là 90%, chỉ 10% request thực sự "làm việc" với backend.

Trong test, bạn cần biết hit ratio thực tế của production để tái tạo đúng. Ví dụ: nếu production có hit ratio 85% ở tầng CDN cho trang danh mục, thì test của bạn cũng phải tạo ra một mô hình dữ liệu sao cho ~85% request rơi vào các URL/sản phẩm "nóng" đã được cache, và 15% rơi vào phần "lạnh".

Cache warm vs cold — trạng thái ban đầu quyết định kết quả

  • Cold cache: cache trống rỗng (vừa deploy, vừa restart, hoặc vừa purge). Request đầu tiên cho mỗi key phải đi tới tận DB. Đây là kịch bản tệ nhất — thường xảy ra ngay sau khi deploy hoặc sau flash sale khi bạn buộc phải purge cache.
  • Warm cache: cache đã đầy các key nóng. Đây là trạng thái bình thường của production đang chạy ổn định.
Sai lầm kinh điển: chạy test trên cold cache rồi báo cáo con số thảm hại như thể đó là hiệu năng thật; hoặc ngược lại, "warm up" quá kỹ rồi báo cáo con số đẹp long lanh mà production không bao giờ đạt được ngay sau deploy. Bạn phải test CẢ HAI kịch bản và báo cáo riêng.

Cacheable vs non-cacheable — hiểu HTTP cache headers

Khả năng một response được cache phụ thuộc vào HTTP headers:

  • Cache-Control: public, max-age=3600 → CDN được phép cache 1 giờ.
  • Cache-Control: private → chỉ browser cache, CDN bỏ qua.
  • Cache-Control: no-store → tuyệt đối không cache (dùng cho trang giỏ hàng, thanh toán, dữ liệu cá nhân).
  • ETag / Last-Modified → cho phép revalidation (304 Not Modified) — request vẫn tới server nhưng response rất nhẹ.
  • Vary: Accept-Encoding, Cookie → cache tách theo giá trị header, nếu Vary: Cookie thì mỗi cookie khác nhau tạo một bản cache riêng (dễ vô hiệu hóa cache).
Khi phân tích một hệ thống trước khi test, việc đầu tiên nên làm là curl -I các URL chính để đọc header và biết tài nguyên nào cacheable.

Tình huống thực tế

Tình huống 1 — Tiki và cú "throughput ảo" 40.000 RPS

Một team QA (bối cảnh giả định dựa trên mô hình một sàn TMĐT lớn kiểu Tiki) chạy load test cho trang chủ trước mùa sale. Họ dùng k6 bắn thẳng vào https://tiki-demo.vn/ và vui mừng thấy hệ thống chịu được 40.000 RPS với p95 latency chỉ 45ms. Team hạ tầng lại nhíu mày: "Backend của chúng ta chỉ có 20 pod, không đời nào xử lý nổi 40.000 RPS thật."

Điều tra ra: trang chủ được Cloudflare cache với max-age=60. Toàn bộ 40.000 RPS được edge trả về, backend chỉ nhận đúng ~1 request mỗi 60 giây để refresh. Bài test đang đo Cloudflare, không đo hệ thống.

Diễn giải: response header có cf-cache-status: HIT. Nếu team đọc header này ngay từ đầu, họ đã biết. Cách sửa: (1) test riêng phần dynamic (API /api/products/{id} với danh sách ID phản ánh phân bố truy cập thật), (2) nếu muốn test backend gốc, trỏ thẳng vào origin (bypass CDN) hoặc thêm header Cache-Control: no-cache — nhưng phải ý thức rằng lúc đó ta đang test "kịch bản xấu nhất khi CDN chết", không phải kịch bản bình thường.

Bài học rút ra: Luôn kiểm tra cf-cache-status / X-Cache trong response. Một con số throughput cao mà không kèm cache hit ratio là con số vô nghĩa.

Tình huống 2 — Startup fintech và cache-busting vô tình

Một startup ví điện tử ở TP.HCM test API danh sách giao dịch. Để "chắc chắn dữ liệu luôn mới", một bạn dev thêm timestamp vào mọi request: /api/transactions?t=1687849200123. Mỗi request có t khác nhau nên không request nào trùng URL, khiến Redis cache (key theo URL) miss 100%.

Kết quả: ở 500 concurrent user, database CPU vọt lên 98%, latency p95 nhảy lên 4 giây. Team báo cáo "hệ thống chỉ chịu được 500 user". Sếp hoảng, tính thuê thêm server.

Diễn giải: Production thực tế KHÔNG gắn timestamp — client gọi /api/transactions?page=1 và Redis cache theo user_id + page, hit ratio ~70%. Cái timestamp chỉ là "sáng tạo" trong lúc viết script test, và nó đã phá sạch tầng application cache, biến bài test thành bài "stress test database trần trụi".

Sau khi bỏ timestamp và mô phỏng đúng phân bố page (80% user chỉ xem page 1), hệ thống chịu được 3.500 user với p95 dưới 300ms.

Bài học rút ra: Bất kỳ tham số nào trong URL/body làm request trở nên độc nhất (timestamp, random UUID, cache-buster) đều có thể vô hiệu hóa toàn bộ cache. Trước khi tin vào kết quả, hãy kiểm tra cache hit ratio ở tầng app/DB trong lúc test.

Tình huống 3 — Sàn giáo dục và bài test "warm nhầm"

Một nền tảng học trực tuyến chuẩn bị ra mắt khóa học mới, dự kiến 10.000 học viên đăng nhập cùng lúc lúc 20h. Team test bằng JMeter, chạy lặp đi lặp lại cùng một kịch bản trên cùng một tập 50 tài khoản. Sau vài lần chạy, Redis đã cache toàn bộ session, profile, danh sách khóa học của 50 user đó → kết quả cực đẹp: 10.000 user, p95 = 120ms.

Đêm ra mắt, hệ thống lag nặng ở phút đầu tiên. Vì sao? Production có 10.000 user khác nhau, mỗi người có profile riêng chưa từng vào cache. Phút đầu tiên là một cú cold cache stampede — hàng nghìn cache miss đồng thời dội vào DB.

Diễn giải: Bài test warm cache với 50 user không phản ánh được hiện tượng "10.000 người lạ ập vào cùng lúc". Cache hit ratio trong test là ~99%, còn trong thực tế phút đầu chỉ ~5%.

Bài học rút ra: Dữ liệu test phải có độ đa dạng (cardinality) tương xứng với production. Nếu production có 10.000 user độc nhất trong phút cao điểm, tập dữ liệu test cũng cần hàng nghìn user độc nhất (dùng CSV Data Set trong JMeter hoặc SharedArray trong k6), và cần test cả kịch bản cold-start.

Hướng dẫn từng bước

Đây là quy trình chuẩn để "test đúng cache" cho một hệ thống:

Bước 1 — Lập bản đồ cache (cache mapping). Với mỗi endpoint quan trọng, chạy curl -I <url> và ghi lại: có Cache-Control không, max-age bao nhiêu, có cf-cache-status/X-Cache/Age không, có Vary không. Kết quả là một bảng: URL → tầng nào cache → TTL bao nhiêu.

Bước 2 — Lấy hit ratio thực tế của production. Hỏi team hạ tầng hoặc đọc dashboard CDN/Redis: trang chủ hit bao nhiêu %, API sản phẩm bao nhiêu %. Đây là "mục tiêu" mà mô hình dữ liệu test phải tái tạo.

Bước 3 — Thiết kế phân bố dữ liệu theo Pareto (80/20). Traffic thật hầu như luôn lệch: một số ít sản phẩm/trang "nóng" chiếm phần lớn lượt xem. Trong k6, ví dụ chọn ID theo phân bố lệch:

import { SharedArray } from 'k6/data';
const hotIds  = new SharedArray('hot',  () => JSON.parse(open('./hot.json')));   // 20% ID nóng
const coldIds = new SharedArray('cold', () => JSON.parse(open('./cold.json')));  // 80% ID lạnh

export default function () { // 85% request rơi vào ID nóng → khớp hit ratio production const id = Math.random() < 0.85 ? hotIds[Math.floor(Math.random() * hotIds.length)] : coldIds[Math.floor(Math.random() * coldIds.length)]; http.get(https://api.example.vn/products/${id}); }

Bước 4 — Quyết định test qua CDN hay origin. Có ba lựa chọn, và bạn nên làm rõ mục tiêu:

  • Test end-to-end (qua CDN): đo trải nghiệm thật của user, cache có hiệu lực. Dùng khi trả lời "user cảm thấy nhanh không?".
  • Test origin trực tiếp (bypass CDN): đo sức chịu tải của backend khi cache không giúp gì. Dùng khi trả lời "backend chịu được bao nhiêu nếu CDN sự cố?". Trỏ IP origin hoặc gửi Cache-Control: no-cache.
  • Test cả hai và so sánh — đây là cách bài bản nhất.
Bước 5 — Chạy kịch bản cold cache có chủ đích. Purge cache (hoặc dùng key chưa từng tồn tại) rồi đo. Đây là kịch bản "sau deploy" hoặc "sau khi buộc phải purge lúc sale". Báo cáo con số này RIÊNG.

Bước 6 — Giám sát cache trong lúc test. Song song với việc bắn tải, theo dõi: CDN cache hit ratio, Redis keyspace_hits/keyspace_misses (lệnh redis-cli info stats), DB QPS. Nếu DB QPS bằng RPS của test, nghĩa là cache đang miss 100% — dừng lại kiểm tra ngay.

Bước 7 — Xử lý browser cache khi cần realism. JMeter/k6 không có browser cache. Nếu muốn mô phỏng "user quay lại", bạn có thể chỉ gọi các tài nguyên động và bỏ qua static assets (CSS/JS/ảnh) — vì trong thực tế chúng đã nằm trong browser cache của user cũ. Ngược lại, để mô phỏng user mới hoàn toàn, hãy tải cả static.

Lỗi thường gặp & mẹo

Lỗi 1 — Không đọc response header. Chạy test cả buổi mà không biết bao nhiêu % request là cache HIT. Mẹo: thêm một Response Assertion / check để đếm X-Cache: HIT vs MISS, hoặc log riêng cf-cache-status.

Lỗi 2 — Cache-buster vô tình. Timestamp, random query param, UUID trong URL làm cache miss 100%. Mẹo: rà lại toàn bộ URL trong script, hỏi "cái này có làm URL độc nhất không?".

Lỗi 3 — Dữ liệu test quá ít cardinality. Lặp 20 sản phẩm, 50 user → cache warm giả tạo, kết quả đẹp ảo. Mẹo: tập dữ liệu test nên đủ lớn để hit ratio khớp production, không phải khớp 100%.

Lỗi 4 — Chỉ test warm cache. Bỏ quên cold-start là bỏ quên đúng thời điểm hệ thống dễ sập nhất. Mẹo: luôn có một bài "post-deploy cold cache" trong bộ test.

Lỗi 5 — Quên Vary: Cookie phá cache. Nếu app set cookie session cho cả trang tĩnh, CDN có thể tạo bản cache riêng cho từng cookie → hit ratio về gần 0. Mẹo: kiểm tra header Vary khi mapping cache; đây thường là "bug hạ tầng" mà performance test giúp phát hiện.

Lỗi 6 — Test bypass CDN rồi báo cáo như con số bình thường. Bạn đo backend trần và tưởng đó là hiệu năng user thấy. Mẹo: ghi rõ trong report "test này bypass CDN — đại diện kịch bản CDN failure".

Mẹo vàng: Với mỗi bài test, hãy viết một dòng ở đầu report: "Cache hit ratio trong test: X% (production: Y%)". Nếu X lệch nhiều so với Y, mọi con số phía sau đều đáng nghi.

Bài tập thực hành

  • Cache mapping: Chọn một website TMĐT Việt Nam bất kỳ (ví dụ trang bạn hay mua sắm). Dùng curl -I với 3 loại URL: trang chủ, trang sản phẩm, và trang giỏ hàng/tài khoản. Lập bảng: URL nào cacheable, TTL bao nhiêu, tầng nào cache (đọc cf-cache-status/X-Cache/Cache-Control). Giải thích vì sao trang giỏ hàng thường là no-store.
  • Phân bố Pareto trong k6: Viết một script k6 gọi API /products/{id} với 100 ID, trong đó 20 ID chiếm 80% lượt gọi. Thêm một check đếm tỷ lệ X-Cache: HIT. Chạy và xác nhận hit ratio ~80%.
  • So sánh cold vs warm: Với cùng một endpoint, chạy 2 lần: lần 1 ngay sau khi purge cache (cold), lần 2 sau khi đã chạy 5 phút (warm). So sánh p95 latency và DB QPS giữa hai lần. Viết 3 câu kết luận về khoảng cách hiệu năng.
  • Săn cache-buster: Cho trước một script test có ?t=${Date.now()} trong URL. Giải thích bằng lời tại sao script này làm sai lệch kết quả, và sửa lại cho đúng.

Tóm tắt

Cache là biến số trung tâm — không phải chi tiết phụ — của mọi performance test. Một request đi qua nhiều tầng cache (browser, CDN, reverse proxy, application, database), mỗi tầng có thể "chặn" request trước khi nó chạm backend. Nếu bạn không hiểu và không kiểm soát cache, con số throughput bạn báo cáo có thể cao gấp 8 lần thực tế (test qua CDN mà tưởng là backend) hoặc thấp thảm hại (cache-buster làm miss 100%).

Ba nguyên tắc cần khắc cốt ghi tâm: (1) Đo cache hit ratio trong test và so với production — lệch nhiều là báo động đỏ; (2) Thiết kế dữ liệu test theo phân bố lệch 80/20 với cardinality đủ lớn để tái tạo đúng hành vi cache; (3) Test cả warm và cold cache riêng biệt, vì cold-start ngay sau deploy hoặc sau purge chính là lúc hệ thống mong manh nhất. Ba tình huống ở trên — throughput ảo qua CDN, cache-buster vô tình, và warm nhầm với ít user — là ba cái bẫy phổ biến nhất mà chính bạn có thể vừa nhận ra mình từng mắc. Test đúng cache, và mọi con số phía sau mới đáng tin.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi