Pertanyaan yang dikirim DevOps klien pada 1 Oktober sebenarnya soal memori. Status pm2 yang dia lampirkan memperlihatkan proses produksi di 1,1 GB setelah 42 jam hidup, dan staging di 186 MB karena baru di-restart, dengan catatan bahwa staging biasanya tumbuh sampai sekitar 500 MB. Status host-nya ikut terlampir: RAM 24,7%, CPU 4,9%, DISK 87,46%.
Yang ditanyakan memori, padahal angka yang perlu dikejar justru yang terakhir. Jawaban yang saya susun: RAM aman dan cukup dipantau tren hariannya di produksi selama beberapa hari, sedangkan disk diperiksa lebih dulu. Ada tiga tempat yang saya minta dicek, yaitu log pm2 beserta rotasinya, .next/cache/images di kedua app, dan revisi deploy lama yang disimpan agen deploy di server.
Folder yang ternyata memakan disk tidak ada di daftar itu.
Yang memenuhi disk
Tindak lanjutnya datang pukul 15.11 di hari yang sama. Menurut angka dari DevOps klien, .next produksi berukuran 5,8G, dan 5,6G di antaranya adalah .next/cache. Isi cache itu terbagi dua: fetch-cache 5,1G dan images 593M. .next staging 1,8G, jadi dua app itu bersama-sama memegang 5,8 ditambah 1,8, yaitu 7,6G.
Dari angka produksi saja, semua isi .next yang bukan cache, termasuk hasil build-nya sendiri, cuma 5,8 dikurangi 5,6, sekitar 0,2G. fetch-cache sendirian 5,1 dari 5,8G, kira-kira 88 persen. Cache gambar yang masuk daftar tersangka hanya 593M, dan di saran berikutnya saya nilai aman. Yang besar justru folder yang sama sekali tidak saya sebut.
Ditulis supaya bertahan, tidak pernah dihapus
Saya lalu memeriksa Next 16.3.6 yang terpasang di repo. Cache berkasnya menulis setiap entri fetch ke .next/cache/fetch-cache sebagai berkas datar bernama 64 karakter heksadesimal, dan tidak ada bagian yang menghapus entri itu. Manifest tag-nya hidup di memori proses sebagai Map, bukan sebagai berkas di folder tersebut. Alasan entri fetch disimpan di .next/cache tertulis di komentar kodenya sendiri:
// next 16.3.6, dist/server/lib/incremental-cache/file-system-cache.js baris 316-319
case _responsecache.IncrementalCacheKind.FETCH:
// we store in .next/cache/fetch-cache so it can be persisted
// across deploys
rootDir = _path.default.join(this.serverDistDir, '..', 'cache', 'fetch-cache');Folder ini memang dirancang untuk melintasi deploy, dan .next/cache secara umum sengaja bertahan melewati build. Masing-masing sifat itu masuk akal. Digabung dengan tidak adanya penghapusan, hasilnya folder yang hanya bisa bertambah.
Di server ini, bertahannya cache bahkan sempat jadi bagian dari saran saya sendiri. Saat membereskan dua deploy produksi yang gagal pada 15 September, saya menyarankan ke DevOps klien sebuah hook sebelum tahap instalasi yang menghapus .next/server, karena artefak deploy membawa folder itu utuh, tapi membiarkan .next/cache karena di situlah data cache untuk CMS. Apakah hook itu benar-benar dipasang tidak pernah terkonfirmasi.
Setiap pergantian kunci meninggalkan berkas mati
Panjang 64 karakter itu cocok dengan bentuk kuncinya. Kunci fetch cache di versi ini adalah hash SHA-256 dalam bentuk heksadesimal, dan hitungannya pas: SHA-256 menghasilkan 256 bit, satu karakter heksadesimal membawa 4 bit, jadi 256 dibagi 4 sama dengan 64. Yang di-hash adalah string JSON yang, di antara bagian lain, memuat prefiks utama, URL, dan objek header permintaan. Potongan dari index.js di folder yang sama, dengan baris lain diganti // ...:
// next 16.3.6, dist/server/lib/incremental-cache/index.js, dipotong
// baris 84
return crypto1.createHash('sha256').update(cacheString).digest('hex');
// baris 87-89
// this should be bumped anytime a fix is made to cache entries
// that should bust the cache
const MAIN_KEY_PREFIX = 'v4';
// baris 288-306
const cacheString = JSON.stringify([
MAIN_KEY_PREFIX,
// ...
url,
// ...
headers,
// ...
]);
return hashString(cacheString);Komentar di atas MAIN_KEY_PREFIX menjelaskan polanya: prefiks itu dinaikkan setiap kali ada perbaikan yang harus membuang cache. Membuang di sini artinya berhenti membaca, bukan menghapus. Entri dengan prefiks lama tetap tinggal di disk.
Di server ini dua perubahan semacam itu datang berurutan. Next 16.3.6 tayang di produksi pada 28 September, dan pesan commit upgrade-nya sudah mencatat akibatnya: prefiks kunci fetch cache pindah dari v3 ke v4 di 16.3, entri yang ditulis build 16.2 tidak dipakai lagi, dan tiap lingkungan akan mengalami satu putaran miss ke CMS dan ke API data pasar sesudah deploy. Pemakaian kuota sepanjang satu jam sesudah rilis tetap normal, tanpa lonjakan akibat cache dingin. Jadi di jam pertama itu ongkos pergantian prefiks tidak terlihat di pemakaian API. Yang tertinggal ada di disk, berupa entri v3 yang tidak dibaca siapa pun lagi.
Lalu token CMS diganti. Header permintaan ikut masuk kunci, jadi pergantian token berarti satu putaran Data Cache dingin lagi, dan semua kunci untuk CMS berubah. Pergantian token itu punya ceritanya sendiri di staging, dan bagian yang relevan di sini cuma satu: listing di staging sempat masih terisi semata-mata karena entri fetch yang ditulis dengan token lama ikut terbawa melintasi deploy. Dengan prefiks v4 sejak 16.3.6 dan token yang sudah berganti, semua entri yang ditulis sebelum 28 sampai 29 September sudah mati.
Catatan pelajaran yang saya tulis dari kasus ini mendaftar empat jenis perubahan kunci yang meninggalkan berkas mati: upgrade Next, pergantian token, teks query baru, dan nilai URL karangan bot. Yang terakhir bahkan tidak menunggu rilis, karena URL ikut masuk bahan kunci. Di produksi, folder itu sampai di 5,1 GB.
Saran pembersihan yang tidak pernah dijalankan
Karena entrinya berkas datar, pemangkasannya tidak perlu memahami isi cache sama sekali. Saran yang saya susun pada 1 Oktober, dengan <app> sebagai folder aplikasi:
# sekali, di kedua app, tanpa restart
find <app>/.next/cache/fetch-cache -type f -mtime +3 -delete
# lalu harian lewat cron, ambangnya 7 hari
find <app>/.next/cache/fetch-cache -type f -mtime +7 -deletePemangkasan berdasarkan umur berkas cukup di sini karena entri yang masih hidup ditulis ulang setiap kali di-refresh, sehingga waktu modifikasinya terus baru, sedangkan entri yang ikut terhapus hanya diambil ulang sekali. Restart tidak diperlukan. Cache gambar yang 593M saya biarkan.
Hasil yang saya harapkan sesudahnya, dan ini perkiraan, bukan pengukuran: halaman dan sitemap produksi tidak berubah, grafik per jam pemakaian API pasar tidak melonjak karena yang terhapus sebagian besar kunci v3 yang mati dan kunci CMS dari sebelum pergantian token, dan pemakaian disk turun dari 87%.
Drafnya saya kirim ke DevOps klien hari itu juga. Dia menyanggupi menjalankan pembersihan di kedua app dan menambah cron harian dengan -mtime +7, tapi saat itu belum ada konfirmasi bahwa semuanya sudah dijalankan.
Kabar yang datang kemudian justru sebaliknya. Dari DM dengan DevOps klien yang saya catat pada 5 Oktober, pembersihan tersebut tidak pernah dijalankan dan cron-nya tidak dipasang. Sebagai gantinya, menurut laporan yang sama, penyimpanan server dinaikkan ke 50 GB pada Jumat 2 Oktober. Saya tidak punya angka disk sesudah penambahan itu, jadi tidak ada yang bisa saya pastikan soal persentasenya sekarang. Yang pasti, penyebabnya tidak ikut pergi.
Cache yang tidak dihapus justru menahan halaman
Pada 5 Oktober baru ketahuan bahwa langganan API data pasar yang dipakai situs ini sudah mati sejak sore sebelumnya. Grafik dan listing tetap berputar karena Data Cache Next mengembalikan jawaban 200 yang basi, dan fetcher aplikasi terus menulis ulang cache-nya sendiri beserta salinan cadangan 24 jam yang baru dari jawaban itu. Perilaku Data Cache terhadap jawaban basi adalah cerita tersendiri; yang penting di sini adalah di mana jawaban itu disimpan.
Siang itu masih ada satu hal yang belum diketahui, yaitu apakah deploy produksi mempertahankan .next/cache/fetch-cache. Karena itu saran saya adalah tidak mengulang build selama API pasar mati. Deploy hari itu yang akhirnya menjawab. Sesudahnya data yang beku tidak terganggu: listing dan grafik menampilkan baris dan titik yang sama, dan salinan cadangan terakhir terus ditulis ulang oleh proses yang baru. Di server ini, fetch cache di disk terbukti bertahan melewati deploy produksi.
Butir "konfirmasi pembersihan dan cron" di daftar saya lalu ditutup. Tidak ada yang akan menghapus cache yang sedang menjaga halaman pasar tetap terisi selama gangguan, dan pertumbuhannya yang tanpa pemangkasan saya catat sebagai topik untuk nanti. Sehari kemudian, catatan dari pemeriksaan yang sengaja skeptis menambahkan peringatan: pembersihan fetch-cache oleh DevOps klien selama gangguan itu akan membuat listing, grafik, dan cadangan ticker harga kosong. Itu peringatan, bukan sesuatu yang dicoba. Intinya tetap mudah diikuti: find hanya melihat umur berkas dan tidak tahu entri mana yang sedang menjadi satu-satunya salinan data.
Cache gambar punya batas, bawaannya setengah disk kosong
Folder images yang 593M itu diperlakukan lain oleh Next, karena untuknya ada batas. Catatan saya pada 1 Oktober menyebut bahwa Next 16.3.6 membatasi cache gambar di disk pada separuh ruang disk kosong saat server mulai kalau images.maximumDiskCacheSize tidak diisi, dan batas tetap saya tulis sebagai perubahan kode opsional di kemudian hari. Hari itu juga batas opsional tersebut masuk ke daftar yang saya parkir dengan catatan "dicatat aja dulu".
Kodenya ada di dua berkas:
// next 16.3.6, dist/server/image-optimizer.js baris 700-704, dipotong
this.cacheDir = (0, _path.join)(/* turbopackIgnore: true */ distDir, 'cache', 'images');
// ...
// Eagerly start LRU initialization for filesystem cache
if (!cacheHandler && nextConfig.images.maximumDiskCacheSize !== 0 && nextConfig.experimental.isrFlushToDisk) {
// ...
}
// next 16.3.6, dist/server/lib/disk-lru-cache.external.js baris 39-41
// Since config was not provided, default to 50% of available disk space
const { bavail, bsize } = await _fs.promises.statfs(cacheDir);
maxSize = Math.floor(bavail * bsize / 2);Di komentar konfigurasi yang saya tulis belakangan, rumusannya menjadi "saat pertama dipakai", dan rumusan itu lebih cocok dengan kodenya. Di 16.3.6, ImageOptimizerCache dibuat di dalam handleNextImageRequest, yaitu penangan permintaan gambar, dan konstruktornya langsung memulai inisialisasi LRU disk.
Bedanya dengan fetch cache jelas. Cache gambar punya LRU dan batas, sementara fetch cache tidak pernah menghapus apa pun. Hanya saja batas bawaan itu bermasalah kalau satu server menjalankan lebih dari satu app. Staging dan produksi di sini berjalan di satu server kecil, masing-masing dengan cache sendiri. Kalau tiap app boleh memakai separuh disk kosong, setengah ditambah setengah berarti keduanya bersama-sama bisa menghabiskan seluruh disk.
Batas yang diparkir itu akhirnya masuk sebagai commit pada 9 Oktober: cache disk pengoptimal gambar dibatasi 2 GiB per app. Lewat batas itu, varian yang paling lama tidak dipakai dihapus dan dibuat lagi saat diminta berikutnya, dan nilainya dikunci oleh uji konfigurasi. Versi yang disederhanakan, dengan nama konstanta disamarkan:
// next.config.ts, disederhanakan, nama konstanta disamarkan
const IMAGE_CACHE_LIMIT_BYTES = 2 * 1024 ** 3; // 2.147.483.648 byte per app
const nextConfig = {
images: {
maximumDiskCacheSize: IMAGE_CACHE_LIMIT_BYTES,
},
};Dengan dua app, batas gabungan cache gambar di server itu 2 kali 2 GiB, yaitu 4 GiB, berapa pun ukuran disknya. Commit tersebut ikut dalam merge ke main yang tayang di produksi pada hari yang sama, dan semua pemeriksaan staging serta produksi untuk rilis itu lulus. Verifikasi untuk batas ini sendiri masih sebatas uji konfigurasi tadi. Belum ada angka ukuran cache gambar atau pemakaian disk sesudahnya.
Yang sudah terpasang dan yang belum
Pemangkasan .next/cache/fetch-cache tidak pernah dipasang. Penggantinya, disk yang diperbesar ke 50 GB, adalah laporan dari sisi klien yang angkanya belum saya lihat. Penyebabnya masih di tempat: Next 16.3.6 menulis entri fetch dan tidak pernah menghapusnya, jadi selama tidak ada yang memangkas folder itu, upgrade Next berikutnya, token berikutnya, teks query baru, dan nilai URL karangan bot akan terus menambah berkas mati. Batas cache gambar 2 GiB per app sudah ada di kode, dengan uji konfigurasi sebagai satu-satunya verifikasi.
Kalau kamu menjalankan Next sendiri dengan cache berkas bawaan, ada dua hal dari kasus ini yang menurut saya layak dipegang. fetch-cache tidak punya penjaga ukuran, jadi pemangkasannya harus kamu sediakan sendiri, dan pemangkasan berdasarkan umur berkas masuk akal karena entri yang hidup terus ditulis ulang. Tapi folder yang sama bisa menjadi satu-satunya salinan data ketika API hulu mati, jadi jadwal pemangkasan otomatis perlu ditimbang terhadap kemungkinan itu. Saran find di atas belum pernah dijalankan di server ini, jadi efeknya pada disk maupun pada halaman belum terukur.