Memilih antara caching in-memory lokal dan cache terdistribusi seperti Redis pada Go Fiber bukan sekadar perbandingan latensi sub-mikrodetik melawan hitungan milidetik. Pilihan ini berdampak langsung pada beban Garbage Collector (GC) runtime Go, arsitektur scaling horizontal pod Kubernetes, konsistensi status aplikasi, dan tagihan infrastruktur cloud.
Karakteristik Teknis: Overhead Memory & GC vs Network Round-Trip
In-memory cache menyimpan data langsung di alokasi heap atau buffer proses Go Fiber. Akses memori pointer berlangsung instan tanpa overhead serialization atau TCP handshaking. Namun, jika menyimpan jutaan objek Go dengan pointer di heap runtime, GC Go akan memindai setiap objek saat fase mark-and-sweep, yang memicu lonjakan CPU dan latensi Stop-the-World (STW).
Solusi in-memory modern seperti BigCache atau custom ring-buffer mengalokasikan byte array raksasa yang dihindarkan dari pemindaian pointer GC (off-heap simulation via indeks integer). Solusi bawaan seperti fiber/storage/memory cocok untuk key-value sederhana tetapi tetap menggunakan map standar Go yang berisiko memperberat kerja GC pada kapasitas data besar.
Sebaliknya, Redis memindahkan alokasi memori ke proses terpisah di luar pod Go. Latensi pembacaan data naik dari ~50 nanodetik (lokal RAM) menjadi 0.5–2 milidetik karena adanya overhead serialization (JSON/Protobuf/MessagePack), transfer data via socket jaringan (TCP/Unix socket), dan protokol RESP. Keuntungannya: heap Go Fiber tetap kecil dan siklus GC berjalan cepat dan dapat diprediksi.
Tantangan Konsistensi dan Horizontal Scaling
Ketika aplikasi Go Fiber di-deploy ke Kubernetes dan di-scale menggunakan Horizontal Pod Autoscaler (HPA), in-memory cache menjadi terisolasi (cache silo) pada masing-masing pod replica.
- Cache Incoherence: Pod A memperbarui entri produk ID 100, tetapi Pod B masih menyimpan data lama di RAM lokalnya. Request dari user yang diarahkan oleh Load Balancer ke Pod B akan membaca stale data.
- Invalidasi Kompleks: Menghapus atau memperbarui cache di seluruh pod memerlukan mekanisme pub/sub (misalnya Redis Pub/Sub, Kafka, atau internal gRPC mesh), yang meniadakan tujuan awal menghindari ketergantungan infrastruktur eksternal.
- Cold Start Berulang: Setiap kali HPA membuat pod baru, cache lokal pod tersebut kosong, memicu lonjakan query langsung ke database primer.
Redis bertindak sebagai Single Source of Truth yang terpusat. Ketika Pod A memperbarui entri data, perubahan tersebut langsung terbaca oleh seluruh pod replica Go Fiber secara seragam.
Analisis Biaya Operasional: Managed Redis vs RAM Node Kubernetes
Aspek finansial sering kali menjadi faktor penentu:
- RAM Node Kubernetes: Memperbesar RAM worker node untuk menampung cache lokal relatif lebih murah per gigabyte. Namun, memori yang dialokasikan ke pod Go Fiber berisiko memicu termination oleh Linux Out-Of-Memory (OOM) Killer jika batas
limits.memoryterlampaui saat beban puncak. - Managed Redis (AWS ElastiCache / GCP Memorystore): Menyediakan automatic failover, snapshot backup, dan isolasi beban operasional dari node aplikasi. Namun, biayanya jauh lebih tinggi per gigabyte dibanding RAM compute node standar, terutama jika menggunakan setup Multi-AZ dengan replica read-only.
Implementasi: Interface Abstraction & Mitigasi Cache Stampede
Gunakan abstraction layer pada layer storage agar backend cache dapat ditukar tanpa merombak handler Fiber. Pola singleflight digunakan untuk mencegah cache stampede (thundering herd) ketika banyak request meminta key yang sama secara bersamaan saat cache kedaluwarsa.
package main
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/gofiber/fiber/v2"
"golang.org/x/sync/singleflight"
)
type CacheService interface {
Get(ctx context.Context, key string) ([]byte, error)
Set(ctx context.Context, key string, val []byte, ttl time.Duration) error
}
type Product struct {
ID string `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
}
type ProductHandler struct {
cache CacheService
group singleflight.Group
}
func (h *ProductHandler) GetProduct(c *fiber.Ctx) error {
ctx := c.Context()
productID := c.Params("id")
cacheKey := fmt.Sprintf("product:%s", productID)
// 1. Coba ambil dari cache
if cachedData, err := h.cache.Get(ctx, cacheKey); err == nil && cachedData != nil {
c.Set("X-Cache", "HIT")
return c.Send(cachedData)
}
// 2. Mitigasi Cache Stampede dengan singleflight
val, err, _ := h.group.Do(cacheKey, func() (interface{}, error) {
// Simulasi pembacaan DB primer
product, err := fetchProductFromDB(productID)
if err != nil {
return nil, err
}
payload, err := json.Marshal(product)
if err != nil {
return nil, err
}
// Simpan kembali ke cache secara non-blocking
_ = h.cache.Set(context.Background(), cacheKey, payload, 5*time.Minute)
return payload, nil
})
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Product not found"})
}
c.Set("X-Cache", "MISS")
return c.Send(val.([]byte))
}
func fetchProductFromDB(id string) (*Product, error) {
return &Product{ID: id, Name: "Server RAM Module", Price: 120.50}, nil
}
Matriks Keputusan Arsitektur
Gunakan acuan matriks berikut untuk menentukan pilihan cache:
| Metrik / Kriteria | In-Memory Cache (BigCache / Fiber Memory) | Distributed Cache (Redis) |
|---|---|---|
| Latensi Pembacaan | Sub-mikrodetik (< 1µs) | Sub-milidetik hingga 2ms |
| Dampak GC Go | Tinggi jika pointer; rendah jika zero-allocation byte buffer | Nol (proses terpisah dari Go runtime) |
| Skalabilitas Horizontal | Siloed, butuh sticky session atau pub/sub sync | Mudah, semua pod membaca state yang sama |
| Integritas Data | Eventually consistent rentan stale data | Konsistensi tinggi (Strong / Immediate) |
| Biaya Infrastruktur | Rendah (terintegrasi di pod worker node) | Tinggi (dedicated instance, managed licensing) |
| Skenario Ideal | Reference static data, master lookup, single-node service | Session storage, rate limiting terpusat, shared state API |
Jika beban transaksi membutuhkan data yang mutlak konsisten antar pod replica, gunakan Redis. Jika aplikasi adalah microservice read-heavy dengan dataset kecil dan toleran terhadap data stale beberapa detik, in-memory cache lokal memberikan throughput maksimal tanpa beban biaya infrastruktur tambahan.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!