Caching Strategies — DevOps

Mengapa Caching Penting? Caching menyimpan hasil komputasi atau data yang sering diakses agar tidak perlu diproses ulang. Ini mengurangi latency dan beban…

Caching artinya menyimpan salinan jawaban di tengah jalan, supaya pertanyaan yang sama gak perlu sampai ke sumber aslinya. Bayangin toko roti dengan banyak cabang. Roti tawar yang sama untuk semua orang dipajang di etalase cabang dekat rumahmu, jadi kamu gak perlu ke pabrik. Di pintu dapur pabrik ada rak berisi roti yang baru keluar oven, buat kurir yang datang beberapa detik kemudian. Di kulkas dapur ada adonan siap panggang, jadi gak tiap roti mulai dari tepung. Tiap roti punya tanggal kedaluwarsa, kue ulang tahun bertuliskan namamu gak pernah dipajang, dan saat resep berubah, roti lama ditarik dari etalase. catatan-api punya layer yang sama persis: CDN (etalase cabang), nginx proxy_cache di web-1 (rak di pintu dapur), Redis (kulkas), dan Postgres (gudang tepung).

🧪 Tebakan cepat: CDN sudah menyala di depan catatan-api dan /catatan/populer boleh disimpan 60 detik. Dalam satu menit ada 60 klik dari Facebook ke /catatan/populer?fbclid=..., dan tiap klik punya fbclid sendiri. Berapa dari 60 request itu yang sampai ke web-1? Tebak dulu, jawabannya ada di bagian "CDN: s-maxage, Cache Rule, dan cache key".

Empat layer di depan catatan-api

Request berjalan dari luar ke dalam, dan berhenti di layer pertama yang punya salinannya:

Layer Di mana Menyimpan apa Berapa lama
CDN (Cloudflare) edge Jakarta, Surabaya, Singapura jawaban publik yang sama untuk semua orang sesuai s-maxage dari header
nginx proxy_cache disk web-1 jawaban dari Node yang boleh dibagi 10 detik (di config)
Redis memori, di samping app data hasil query, per key EX 60 per key
Postgres disk database data asli selamanya

Sebelum CDN masih ada cache browser, di perangkat pengunjung sendiri (dibahas di pelajaran HTTP Caching: Cache-Control, ETag, Validation). Dari layer yang kamu kelola di server, CDN yang paling dekat dengan user: secara jarak dan secara urutan.

Satu menit traffic catatan-api

Lab-nya mengirim 10 request per detik selama 1 menit, jadi 600 request: 4 ke /assets/app.3f9a1c.js, 3 ke /catatan/populer, 1 ke /catatan/populer?fbclid=..., dan 2 ke /catatan (daftar catatan milik user yang login, 20 user bergantian). Pengunjung masuk lewat 3 edge. Waktunya contoh: edge 15 ms, edge ke web-1 30 ms, Redis 2 ms, query populer 150 ms. Hasilnya:

Yang menyala Dijawab edge Sampai ke web-1 Sampai ke Node Query Postgres Rata-rata
belum ada cache 0 (0%) 600 360 360 97 ms
CDN 414 (69%) 186 183 183 45,6 ms
CDN + nginx 414 (69%) 186 126 126 30,9 ms
CDN + nginx + Redis 414 (69%) 186 126 21 26,6 ms
semua + cache key tanpa query 474 (79%) 126 121 21 23,6 ms

Hit ratio adalah persen request yang dijawab dari cache: hit / total × 100. 414 dari 600 jadi 69%. Tiap layer menangkap yang lolos dari layer sebelumnya, jadi Postgres turun dari 360 query jadi 21.

CDN: s-maxage, Cache Rule, dan cache key

CDN menurut pada header Cache-Control yang dikirim app:

app.get("/catatan/populer", async (req, res) => {
  res.set("Cache-Control", "public, max-age=0, s-maxage=60");
  res.json(await ambilPopuler());
});

app.get("/catatan", wajibLogin, async (req, res) => {
  res.set("Cache-Control", "private, no-store");
  res.json(await ambilCatatanUser(req.user.id));
});

Di Cloudflare, path tanpa ekstensi file statis seperti /catatan/populer gak di-cache sama sekali (statusnya DYNAMIC) sampai kamu membuat Cache Rule yang menandainya eligible for cache.

Cache key adalah nama yang dipakai CDN untuk memutuskan dua request itu "file yang sama". Bawaannya URL lengkap, termasuk query string. Jadi /catatan/populer?fbclid=IwAR1000 dan ?fbclid=IwAR1001 adalah dua key berbeda, dan tiap key baru selalu MISS. Ke-60 request itu sampai ke web-1 semua. Itu jawaban tebakannya. Atur cache key path ini supaya mengabaikan query string, dan hit ratio-nya naik dari 69% jadi 79%. Sisanya 20% adalah /catatan, yang memang gak boleh disimpan di CDN.

nginx proxy_cache di web-1

CDN punya banyak edge, dan tiap edge MISS sendiri-sendiri. nginx di web-1 (dari pelajaran Reverse Proxy & Load Balancing) menyimpan satu salinan untuk semuanya:

# /etc/nginx/sites-available/catatan-api
proxy_cache_path /var/cache/nginx/catatan levels=1:2 keys_zone=catatan:10m max_size=100m inactive=10m;

server {
    server_name catatan.example.com;

    location /catatan/populer {
        proxy_pass http://127.0.0.1:3000;
        proxy_cache catatan;
        proxy_cache_key $scheme$host$uri;
        proxy_cache_valid 200 10s;
        proxy_ignore_headers Cache-Control;
        add_header X-Cache-Status $upstream_cache_status;
    }
}

Hasilnya, 63 request populer yang lolos dari CDN tinggal 6 yang sampai ke Node: satu tiap 10 detik. App PHP (Laravel lewat PHP-FPM) memakai pasangannya, fastcgi_cache, dengan cara yang sama.

Redis: cache-aside untuk data aplikasi

Request yang sampai ke Node masih bisa gak menyentuh Postgres. Pola paling umum namanya cache-aside (atau lazy loading): cek cache dulu, kalau gak ada (miss) ambil dari database lalu simpan ke cache untuk request berikutnya.

async function ambilCatatanUser(userId) {
  const key = `catatan:user:${userId}`;
  const simpanan = await redis.get(key);
  if (simpanan !== null) return JSON.parse(simpanan); // HIT
  const { rows } = await db.query("SELECT id, judul, isi FROM catatan WHERE user_id = $1", [userId]);
  await redis.set(key, JSON.stringify(rows), { EX: 60 }); // MISS: simpan 60 detik
  return rows;
}

Deploy: purge dari dalam ke luar

Bagian tersulit dari caching adalah cache invalidation: memastikan salinan dihapus atau diganti begitu data aslinya berubah. Ada tiga cara: tunggu TTL habis, hapus saat data berubah (event-based, misalnya redis.del di route POST /catatan), atau ganti nama file (versioning, seperti app.8b21d4.js).

Saat deploy, CDN bisa dikosongkan lebih awal dengan purge. Di Lab, deploy versi baru setelah 30 detik tanpa purge membuat 192 jawaban di menit berikutnya masih versi lama, karena edge mengisi ulang salinannya dari nginx dan Redis yang juga masih menyimpan versi lama. Dengan purge, angkanya 0. Urutannya dari dalam ke luar:

# bagian akhir deploy.sh
redis-cli DEL catatan:populer
sudo find /var/cache/nginx/catatan -type f -delete
curl -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/purge_cache" \
  -H "Authorization: Bearer $CF_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"files":["https://catatan.example.com/catatan/populer"]}'

Aset gak perlu di-purge, karena nama file barunya belum pernah ada di cache mana pun.

Di latihan: membaca header Cache-Control

parseCacheControl(header) mengurai header jadi object, seperti yang dilakukan CDN. Pecah di koma, trim() tiap bagian, lalu: bagian yang mengandung = dipecah jadi key dan angka (Number), sisanya jadi true. Urutan key mengikuti urutan di header:

Panggilan Hasil
parseCacheControl("public, max-age=86400") { public: true, "max-age": 86400 }
parseCacheControl("private, no-cache, no-store") { private: true, "no-cache": true, "no-store": true }
parseCacheControl("public, max-age=3600, s-maxage=7200") { public: true, "max-age": 3600, "s-maxage": 7200 }

⚠️ Jebakan yang sering ketemu

TL;DR: Cache menyimpan salinan supaya request berhenti sebelum sampai ke sumber. Untuk catatan-api urutannya CDN (paling dekat dengan user), nginx proxy_cache di web-1, Redis di samping app, lalu Postgres; di Lab, 360 query per menit turun jadi 21. Hit ratio = hit / total × 100. CDN menurut Cache-Control: public, max-age=31536000, immutable untuk aset ber-hash, s-maxage untuk shared cache, private, no-store untuk data per user. Cache key bawaannya URL lengkap, jadi query string acak membuat MISS. Redis memakai cache-aside (cek cache, miss ambil dari database, simpan dengan EX). Masalah utama caching adalah cache invalidation: pakai TTL, hapus saat data berubah, atau ganti nama file, dan saat deploy purge dari dalam ke luar.

Yang akan kamu pelajari