Solusi Object Cache di Single Server: APCu!

Drop-in `object-cache.php` ini adalah pengganti (drop-in replacement) sistem cache objek bawaan WordPress, yang menggunakan **APCu** sebagai backend penyimpanan persisten (L2) dan **array PHP** sebagai cache lokal per-request (L1). Ia dirancang untuk server tunggal dengan *shared memory* APCu yang tidak terdistribusi, dan memberikan performa tinggi sekaligus kontrol koherensi yang sangat cermat.

Kami sudah mencoba mulai dari Redis hingga Memcached untuk utilisasi object cache di WordPress, dan tetap pemenangnya di single server adalah APCu. Tentunya tidak dengan metode penulisan kode yang saya dengan drop-in plugin sejenis yang ada di WP Repo. Yang ini jelas berbeda, cukup kompleks tetapi setiap kompleksitasnya sangat beralasan. Jika Anda mempunyai website WordPress yang sangat kompleks (e-commerce dengan ribuan produk atau puluhan ribu customers) drop-in APCu ini jawabannya. Segera hubungi saya untuk instalasi.

## Daftar Isi

1. [Apa itu Drop-in Object Cache?](#apa-itu-drop-in-object-cache)
2. [Arsitektur dan Desain Inti](#arsitektur-dan-desain-inti)
3. [Fitur Unggulan](#fitur-unggulan)
4. [Konfigurasi (Konstanta)](#konfigurasi-konstanta)
5. [Cara Kerja Operasi Utama](#cara-kerja-operasi-utama)
6. [Konkurensi, Koherensi, dan Trade-off](#konkurensi-koherensi-dan-trade-off)
7. [Deployment dan Praktik Terbaik](#deployment-dan-praktik-terbaik)
8. [Sorotan Changelog](#sorotan-changelog)
9. [Kesimpulan](#kesimpulan)

## Apa itu Drop-in Object Cache?

WordPress menyediakan sistem *object caching* internal untuk menyimpan data yang sering diakses (opsi, transient, hasil query, dll.) dalam memori selama satu request. Untuk membuat cache ini persisten antar-request, WordPress mengizinkan **drop-in** berupa file `object-cache.php` yang ditempatkan di direktori `wp-content/`. File ini akan otomatis dipakai menggantikan class `WP_Object_Cache` bawaan.

Drop-in **AOC (Advanced Object Cache) – Varian APCu** pada artikel ini mengimplementasikan **cache dua tingkat (two-tier)**:

– **L1 (request-local)** – array PHP di dalam proses, dengan kapasitas terbatas dan eviction FIFO.
– **L2 (persistent)** – APCu shared memory, tempat data bertahan melampaui satu request.

Dengan pendekatan ini, akses data yang sangat cepat (L1) dipadukan dengan persistensi APCu yang tetap rendah latensi.

## Arsitektur dan Desain Inti

### Kunci Konsep: Namespace dan Generasi (Generation)

Untuk menangani invalidation di lingkungan *single shared memory*, drop-in ini menggunakan **generation counter**:

– **Root Generation** (`root_ns_key`) – counter global yang di-bump saat `flush()` dipanggil, membatalkan seluruh cache dalam satu langkah.
– **Blog Namespace** (`ns_key`) – counter per-blog (multisite) yang memisahkan cache antar site.
– **Group Generation** (`group_counter_key`) – counter per-grup, yang di-bump saat `flush_group($group)`.

Setiap key yang disimpan di APCu dikodekan dengan prefix yang mengandung seluruh counter tersebut, misalnya:

Perubahan salah satu counter akan mengubah seluruh prefiks, sehingga data lama otomatis tidak terbaca lagi (data yatim yang akan kedaluwarsa secara alami). Ini menghindari operasi penghapusan massal yang mahal.

### Grup Global, Non-Persistent, dan Runtime-Only

Drop-in mengenal beberapa tipe grup:

– **Global groups**: data yang dibagikan antar blog di multisite (mis. `users`, `site-options`). Cache key-nya menggunakan namespace global.
– **Non-persistent groups (NP)**: hanya disimpan di L1 (tidak pernah ke APCu). Bawaan meliputi `aoc_np` dan grup yang ditambahkan via `wp_cache_add_non_persistent_groups()`. Grup ini juga dianggap *runtime-only*.
– **Runtime-only keys**: walau grup-nya bersifat persisten, key tertentu dapat diperlakukan sebagai *runtime-only* berdasarkan aturan, misalnya key WooCommerce yang hanya relevan satu request (seperti `*_cache_prefix`, `wc_session_*`, `wc_cart_hash`) jika fitur **WooCommerce strict exclusion** diaktifkan.

Klasifikasi *runtime* vs *persistent* mempengaruhi alur: data runtime tidak pernah diambil/ditulis ke APCu, dan tidak memicu refresh generasi.

### L1 Cache: FIFO dengan Batas Kapasitas Terpadu

Cache lokal (L1) memiliki kapasitas total yang ditentukan oleh:

– `AOC_LOCAL_LIMIT` mengontrol batas masuknya nilai persisten ke L1 (0 = nonaktifkan).
– `AOC_RUNTIME_LOCAL_LIMIT` menjamin kapasitas minimum yang selalu tersedia untuk nilai runtime-only.

Eviction bersifat FIFO dan dilakukan secara batch (persentase dikonfigurasi) untuk mencegah overhead yang terlalu sering.

### L2 APCu: Tipe Data Native dan Fallback

APCu dipilih karena menyimpan tipe data PHP secara native (array, objek) tanpa serialisasi saat `apcu_store/fetch`, asalkan data tersebut dapat disimpan di shared memory. Drop-in juga menyediakan rantai fallback kloning objek jika `clone` gagal: `igbinary` → `serialize` → objek asli (fallback terakhir).

## Fitur Unggulan

Fitur-fitur yang dijanjikan di header file (dan diimplementasikan di kode) antara lain:

1. **Two-tier cache dengan L1 dan L2** – akses super cepat untuk data panas.
2. **Eviction FIFO L1 yang terbatas** – mencegah konsumsi memori berlebihan.
3. **Root generation flush** – flush seluruh cache hanya dengan me-*bump* satu counter.
4. **Namespace per-blog dan grup global** – multisite aman tanpa tabrakan.
5. **Atomic invalidation** – bump counter atomik (menggunakan `apcu_inc` + perbaikan kerusakan counter).
6. **Forced-read generation refresh** – `wp_cache_get(…, $force = true)` memvalidasi generasi terbaru sebelum membaca L2.
7. **Periodic generation refresh** – worker lama (RoadRunner, WP-CLI) secara berkala memeriksa counter agar tidak basi.
8. **Generation refresh sebelum mutasi persisten** – dengan jendela *coalescing* untuk mengurangi fetch counter.
9. **Key-aware mutation preflight** – kunci runtime-only (WooCommerce) tidak memicu refresh generasi, menghemat overhead.
10. **Batch read dengan klasifikasi runtime/persistent** – hanya kunci persisten yang melakukan perjalanan ke APCu.
11. **Fail-closed** – jika refresh generasi gagal, operasi tetap aman: get mengembalikan miss, set/add tidak ditulis ke L2 (untuk mencegah data basi dengan versi generasi salah).
12. **Perbaikan counter otomatis** – dengan *backoff* eksponensial dan lock (`apcu_add`) untuk menghindari race condition.
13. **Status eksplisit** – setiap operasi internal mengembalikan status: `HIT/MISS/BACKEND_ERROR`, `OK/CONTENDED/BACKEND_ERROR`, dll.
14. **Increment atomik dan decrement berbasis CAS** – `incr()` menggunakan `apcu_inc` (atomik), `decr()` menggunakan `apcu_cas` dengan batas bawah 0 (tidak pernah negatif).
15. **Batch API yang ketat** – `get_multiple`, `set_multiple`, `add_multiple`, `delete_multiple` dengan semantik per-key yang jelas.
16. **Native batch APCu opsional** – `apcu_store/add/delete(array)` diaktifkan jika `AOC_APCU_NATIVE_BATCH = true`, dengan *break-on-backend-failure* dan pencacahan kegagalan per-key.
17. **Flush grup selektif L1** – `flush_group()` hanya menghapus entri grup tersebut dari L1, bukan seluruh L1.
18. **Batched L1 eviction** – mencegah fluktuasi performa saat eviction.
19. **Fallback rantai kloning objek** – menjaga isolasi cache (tidak melewatkan referensi) meski objek sulit diklon.
20. **WooCommerce strict key exclusion** – mencegah key-key dinamis WooCommerce membebani APCu.
21. **Strict user privacy groups** – grup sensitif (users, sessions) dapat dipaksa non-persistent.
22. **Forced non-persistent groups** – daftar grup kustom yang tidak pernah ke APCu.
23. **Runtime-only fallback** – jika APCu tidak tersedia saat bootstrap atau mati di tengah jalan, seluruh cache beroperasi hanya di L1 tanpa error fatal.
24. **Throwable-safe** – semua panggilan APCu dibungkus try/catch, mencegah exception tidak tertangkap.
25. **Diagnostik memori APCu** – `get_stats()` menyertakan `apcu_sma_info()` dan `apcu_cache_info()` jika tersedia.

## Konfigurasi (Konstanta)

Semua konstanta harus didefinisikan **sebelum** drop-in dimuat, idealnya di `wp-config.php` atau di file `object-cache.php` itu sendiri (jika disimpan sebagai drop-in, konstanta di dalamnya bisa di-*override* dari luar, namun lebih bersih didefinisikan di `wp-config.php`).

### Tier 1: Tuning Performa

| Konstanta | Nilai Bawaan | Penjelasan |
|———————————-|————–|————————————————————————————————|
| `AOC_LOCAL_LIMIT` | `8000` | Kapasitas yang diinginkan untuk entri persisten di L1. `0` menonaktifkan L1 untuk nilai persisten, namun runtime-only tetap bisa disimpan berkat `AOC_RUNTIME_LOCAL_LIMIT`. |
| `AOC_RUNTIME_LOCAL_LIMIT` | `3000` | Kapasitas minimum gabungan L1 yang dijamin untuk entri runtime-only. Bukan kuota terpisah. |
| `AOC_L1_TTL_MAX` | `30` | Maksimum TTL (detik) entri di L1. `0` hanya aman untuk PHP-FPM (request singkat). Di worker seperti RoadRunner, harus > 0. |
| `AOC_L1_REFILL_TTL` | `5` | TTL entri L1 yang diisi dari L2 (hasil `get`, `incr`, `decr`). Karena APCu tidak menyediakan sisa TTL, nilai pendek ini mencegah L1 menyimpan data lebih lama dari TTL asli L2. |
| `AOC_GENERATION_REFRESH_INTERVAL`| `5` | Interval (detik) maksimum antara refresh generasi di worker/long-running process. `0` untuk refresh setiap request. |
| `AOC_MUTATION_REFRESH_INTERVAL` | `1` | Jendela *coalescing* (detik) untuk refresh generasi sebelum mutasi persisten. `0` = selalu refresh (paling aman, overhead tertinggi). |
| `AOC_LOCAL_EVICT_PERCENT` | `2` | Persentase kapasitas L1 yang dikosongkan saat eviction batch. |
| `AOC_MAX_KEY_LENGTH` | `250` | Panjang maksimal key di APCu; jika terlampaui, akan di-hash. |
| `AOC_DEFAULT_NOEXP_TTL` | `259200` (3 hari) | TTL default untuk entri dengan `expire = 0`. |
| `AOC_KEY_TTL_MAX` | `604800` (7 hari) | TTL maksimum yang diizinkan untuk key APCu. |
| `AOC_BATCH_CHUNK` | `1000` | Ukuran potongan untuk operasi batch L2. |
| `AOC_GROUP_METADATA_LIMIT` | `512` (atau nilai `AOC_SANITIZED_GROUP_LIMIT` jika ada) | Batas maksimum peta metadata grup (sanitasi, versi, timestamp) di memori lokal. |

### Tier 2: Perilaku / Kompatibilitas

| Konstanta | Nilai Bawaan | Penjelasan |
|——————————|————–|————————————————————————————————|
| `AOC_CLONE_ON_WRITE` | `false` | Jika `true`, nilai dikloning saat masuk/update di L1. `false` menghemat CPU; kloning hanya dilakukan saat dibaca (clone-on-read). |
| `AOC_DEEP_CLONE_ARRAYS` | `false` | Jika `true`, array di-klon secara rekursif (mahal). |
| `AOC_DEBUG` | `false` | Aktifkan pencatatan `error_log` untuk kejadian penting. |
| `AOC_APCU_NATIVE_BATCH` | `false` | Gunakan operasi batch native APCu (`apcu_store` dengan array). Hanya aktifkan setelah menguji semantik return di versi APCu Anda. |
| `AOC_WOO_SKIP` | `true` | Aktifkan pengecualian key WooCommerce yang ketat. |
| `AOC_WOO_MODE` | `’strict’` | Mode pengecualian: `’strict’` (key seperti `*_cache_prefix`, `wc_session_*`, dll.) atau `’none’`. |
| `AOC_STRICT_USER_PRIVACY` | `false` | Jadikan grup sensitif (users, usermeta, session_tokens) non-persistent. |
| `AOC_FORCE_NP_GROUPS` | `[]` | Daftar grup kustom yang dipaksa non-persistent (mis. `[‘some_plugin_group’]`). |

### Key Salt

– `AOC_KEY_SALT` – otomatis dihitung dari `DB_HOST`, `DB_NAME`, `table_prefix`, dan `ABSPATH`, atau bisa diatur manual via `WP_CACHE_KEY_SALT`. Nilai ini menjadi bagian dari prefiks semua key untuk mencegah tabrakan antar instalasi yang berbagi pool APCu yang sama. **Jangan diubah setelah cache berjalan.**

## Cara Kerja Operasi Utama

### `wp_cache_get( $key, $group, $force, &$found )`

1. Klasifikasi key sebagai runtime atau persisten (berdasarkan grup NP, Woo strict, dll.).
2. Jika `$force = true` untuk key persisten: lakukan refresh generasi (baca counter dari APCu). Hapus L1, lalu baca langsung dari L2. Abaikan L1.
Jika key runtime: `$force` diabaikan; hanya L1 yang dicek.
3. Jika bukan `$force`: periksa L1. Kalau hit, kembalikan.
Kalau miss: hanya untuk key persisten, mungkin lakukan refresh generasi (sesuai interval), lalu baca L2.
4. Setiap kali baca dari L2 yang hit, simpan salinan ke L1 dengan TTL `AOC_L1_REFILL_TTL`.
5. Seluruh pembacaan dari L1/L2 melalui rantai kloning (`clone_value`) untuk mencegah referensi bocor.

### `wp_cache_set( $key, $data, $group, $expire )`

1. Validasi nilai (bukan resource atau Closure, dan `alloptions` harus array).
2. Jika key runtime: simpan hanya di L1 (dengan TTL mentah dari `$expire`, tanpa penyesuaian L2). Kembalikan `true`.
3. Untuk key persisten: lakukan `prepare_persistent_mutation()` yang mungkin melakukan refresh generasi jika jendela *coalescing* terlampaui.
4. Bangun key APCu dan simpan di L2 dengan `apcu_store` (mode ‘set’) menggunakan TTL yang dihitung (`get_ttl_l2`).
5. Perbarui L1 dengan TTL yang sesuai.
6. Jika penyimpanan L2 gagal, hapus L1 dan kembalikan `false`.

### `wp_cache_add( $key, $data, $group, $expire )`

Hampir identik dengan `set`, tetapi menggunakan `apcu_add` (hanya jika belum ada). Operasi L1 juga mengecek keberadaan sebelumnya.

### `wp_cache_delete( $key, $group )`

1. Hapus dari L1, catat apakah ada.
2. Untuk key persisten: `apcu_delete`. Jika APCu error, kembalikan `false`; jika key tidak ditemukan, tetap anggap sukses jika L1 sempat ada.
3. Key runtime hanya mengandalkan penghapusan L1.

### `wp_cache_incr( $key, $offset, $group )` dan `decr`

– `incr` menggunakan `apcu_inc` yang atomik. Hanya berlaku pada integer.
– `decr` menggunakan `apcu_cas` dengan batas bawah 0 (clamped). Melakukan retry hingga 20 kali jika gagal CAS.
– Kedua operasi memperbarui L1 setelah sukses di L2.

### Operasi Batch (`get_multiple`, `set_multiple`, `add_multiple`, `delete_multiple`)

– **`get_multiple`** memisahkan key runtime dan persisten. Key runtime hanya dicek di L1. Key persisten menjalani refresh generasi (jika force atau interval), L1, lalu batch fetch ke APCu dengan chunking. Jika satu chunk gagal (backend error), seluruh sisa chunk dianggap miss (break-on-first-failure).
– **`set_multiple`** dan **`add_multiple`** juga memisahkan runtime/persistent. Jika `AOC_APCU_NATIVE_BATCH` aktif, operasi L2 menggunakan `apcu_store(array)` / `apcu_add(array)` dengan chunking; jika gagal pada satu chunk, loop dihentikan dan key setelahnya tetap gagal (fail-safe).
– **`delete_multiple`** serupa.

### Flush dan Flush Group

– `flush()`: bump root generation, lalu flush L1 dan bersihkan metadata lokal.
– `flush_group($group)`: bump group generation, bersihkan hanya entri L1 dari grup tersebut, lalu update metadata lokal.
– `flush_runtime()`: hanya menghapus seluruh L1 dan timestamp refresh; tidak menyentuh APCu.

## Konkurensi, Koherensi, dan Trade-off

Drop-in ini tidak menyediakan linearizability penuh, namun memberikan jaminan yang kuat melalui **generation invalidation**:

– Mutasi dan flush yang berjalan bersamaan dapat menyebabkan race condition: sebuah `set` mungkin menulis ke generasi yang baru saja di-bump oleh `flush` concurrent, sehingga data baru tidak akan terbaca (menjadi data yatim). Ini dibiarkan karena overhead koordinasi penuh di APCu terlalu besar untuk sebuah cache. Data yatim akan kedaluwarsa secara alami.
– **Mutation coalescing window** (`AOC_MUTATION_REFRESH_INTERVAL > 0`) berarti mutasi persisten yang terjadi dalam interval pendek mungkin tidak membaca ulang counter APCu, sehingga bisa menulis ke generasi lama yang baru saja di-invalidate. Data tersebut juga menjadi yatim. Trade-off ini mengurangi beban APCu secara signifikan pada skenario penulisan tinggi.
– **L1 refill TTL** menimbulkan jendela stale kecil: jika data APCu kedaluwarsa, L1 mungkin masih menyimpan nilai lama hingga maksimal `AOC_L1_REFILL_TTL` detik (karena TTL L2 tidak diketahui). Dalam praktik, ini aman karena data yang sering diakses biasanya diperbarui sebelum kedaluwarsa.

Dengan kata lain, drop-in ini mengutamakan performa dan keandalan operasional, dengan konsistensi yang cukup untuk penggunaan cache pada umumnya, namun bukan untuk sumber kebenaran transaksional.

## Deployment dan Praktik Terbaik

### Lingkup Penggunaan

– **Single server / single shared APCu pool**. Karena APCu tidak terdistribusi, jika Anda menjalankan banyak server, setiap server memiliki cache terpisah. Gunakan Redis jika memerlukan cache terdistribusi.
– **PHP-FPM**: Sangat cocok. Konstanta `AOC_L1_TTL_MAX = 30` dan `AOC_GENERATION_REFRESH_INTERVAL = 5` aman; sebenarnya L1 otomatis bersih setiap request selesai.
– **Worker mode (RoadRunner, FrankenPHP, WP-CLI, queue worker)**: Harus atur `AOC_L1_TTL_MAX > 0` agar L1 tidak tumbuh tanpa batas. `AOC_GENERATION_REFRESH_INTERVAL` (default 5) menjaga supaya worker tidak melihat data basi terlalu lama.

### Rekomendasi Tuning

1. **`AOC_LOCAL_LIMIT`** – sesuaikan dengan memori yang tersedia. 8000 sudah cukup untuk kebanyakan site. Turunkan jika memori ketat.
2. **`AOC_RUNTIME_LOCAL_LIMIT`** – pastikan cukup menampung data runtime (sesi WooCommerce, nonces, dll.). 3000 umumnya aman.
3. **`AOC_APCU_NATIVE_BATCH`** – uji dulu di staging. Beberapa versi APCu mengembalikan array yang tidak konsisten. Jika stabil, aktifkan untuk throughput lebih tinggi.
4. **`AOC_WOO_SKIP`** – jika menggunakan WooCommerce, biarkan `true` agar ribuan key transient/session tidak membanjiri APCu.
5. **`AOC_FORCE_NP_GROUPS`** – tambahkan grup plugin yang jelas hanya relevan per-request (misal, cache query internal yang di-reset setiap permintaan).
6. **`AOC_DEEP_CLONE_ARRAYS`** – hanya aktifkan jika Anda sering menyimpan array besar dengan objek di dalamnya dan memerlukan isolasi sempurna (mahal CPU).

### Pemasangan

1. Salin file `object-cache.php` ini ke `wp-content/object-cache.php`.
2. (Opsional) Definisikan konstanta kustom di `wp-config.php` sebelum `require_once(ABSPATH . ‘wp-settings.php’)`.
3. Pastikan ekstensi APCu terpasang dan diaktifkan (`php -m | grep apcu`). Konfigurasi APCu (`apc.shm_size`) minimal 128MB disarankan.
4. Pantau statistik melalui `wp_cache_get_stats()` (jika ada plugin atau custom code) atau gunakan debug bar.

### Fallback dan Ketahanan

– Jika saat bootstrap APCu tidak tersedia (ekstensi tidak ada atau `apcu_enabled() === false`), drop-in akan berjalan dalam **runtime-only mode**. Semua cache hanya di L1, tanpa error. Cache akan hilang setiap request.
– Jika APCu gagal di tengah operasi (exception), mode runtime-only diaktifkan untuk operasi selanjutnya; ini mencegah serangkaian error fatal.

## Sorotan Changelog

**6.3.6-apcu** (rilis saat ini)
– Tambahkan `AOC_CLONE_ON_WRITE` (default `false`) – L1 tidak lagi mengkloning saat penyimpanan, hanya saat pembacaan.
– `flush_runtime()` sekarang membersihkan timestamp refresh, sehingga operasi persisten berikutnya memvalidasi ulang generasi.
– Paritas model keamanan dengan Redis v5.13.5 untuk beberapa aspek.

**6.3.5-apcu**
– Perbaikan eviction storm akibat dual-limit – L1 sekarang menggunakan kapasitas total tunggal untuk eviction.

**6.3.4-apcu**
– Perbaikan data loss saat `AOC_LOCAL_LIMIT=0`: runtime keys tetap mendapat kapasitas dari `AOC_RUNTIME_LOCAL_LIMIT`.
– Validasi nilai cache sebelum mutation preflight.

**6.3.3-apcu**
– Perbaikan inkonsistensi `get_multiple` dengan `$force=true` pada runtime keys.
– Penanganan native batch `delete_multiple` yang lebih konsisten.

**6.3.2-apcu** hingga **6.3.0-apcu** menambahkan berbagai fitur seperti batch classification, flush namespace re-resolution, bounded counter backoff, selective L1 group purge, memory diagnostics, dan lainnya.

## Kesimpulan

Drop-in `object-cache.php` berbasis APCu ini memberikan solusi caching objek **berperforma tinggi, tangguh, dan sangat dapat dikonfigurasi** untuk WordPress di server tunggal. Dengan kombinasi L1/L2, invalidation berbasis generasi, penanganan cerdas untuk data runtime-only, serta mekanisme fallback yang aman, ia cocok untuk berbagai skenario mulai dari blog sederhana hingga toko WooCommerce dengan beban tinggi.

Keunggulan utamanya dibandingkan solusi Redis adalah **nol latency jaringan** karena shared memory lokal, serta instalasi yang lebih sederhana tanpa memerlukan service tambahan. Namun perlu diingat bahwa APCu tidak terdistribusi—jika arsitektur Anda multi-server, pertimbangkan Redis atau solusi cache terdistribusi lainnya.

Dengan mengikuti panduan konfigurasi dan memahami trade-off koherensinya, Anda dapat memperoleh dorongan performa signifikan sekaligus menjaga stabilitas aplikasi WordPress Anda.

Views:1

Tinggalkan Balasan