AizuDemy

Build Rate Limiter Terdistribusi Pakai Redis Lua & Go di VPS

Build Rate Limiter Terdistribusi Pakai Redis Lua & Go di VPS
IKLAN
IDCloudHost
🎧
Dengarkan Artikel Ini
Suara AI Otomatis β€’ 6 mnt baca baca
⚑ TL;DR

Poin Kunci Artikel Ini:

  • API microservices tiba-tiba down gara-gara ada bot iseng yang nge-hit endpoint ribuan kali per detik?
  • Atau pas deploy service baru ke 5 instance, rate limiting jadi kacau karena tiap instance punya counter sendiri-sendiri?
  • Ini masalah klasik yang bikin pusing kalau pakai in-memory rate limiter di arsitektur terdistribusi.
πŸ“‹ Daftar Isi Materi Tutup β–΄

API microservices tiba-tiba down gara-gara ada bot iseng yang nge-hit endpoint ribuan kali per detik? Atau pas deploy service baru ke 5 instance, rate limiting jadi kacau karena tiap instance punya counter sendiri-sendiri? Ini masalah klasik yang bikin pusing kalau pakai in-memory rate limiter di arsitektur terdistribusi.

Masalahnya Dimana Sih?

Gini, kalau tiap instance service punya counter rate limit sendiri, user gampang banget bypass limit dengan cara hit instance yang berbeda. Misalnya limit 100 request/menit per user, tapi punya 5 instance yang di-load balance. Total user bisa dapet 500 request/menit karena tiap instance nggak saling ngobrol.

Terus kenapa nggak pakai database MySQL aja buat nyimpen counter? Bisa, tapi lambat banget. Setiap request harus write ke database, latency langsung naik drastis. Makanya Redis jadi solusi yang pas: cepat, atomic, dan bisa jadi single source of truth untuk semua instance.

Pilih Algoritma yang Pas

Ada tiga pilihan umum:

  • Fixed Window: Counter reset tiap periode tetap (misal tiap menit). Masalahnya bisa kena burst traffic di ujung window. User bisa dapet 200 request dalam 2 detikβ€”100 di detik terakhir window lama, 100 di detik pertama window baru.
  • Sliding Window: Counter dihitung dari waktu sekarang mundur N detik. Lebih halus tapi lebih ribet karena harus track timestamp tiap request.
  • Token Bucket: Ada bucket berisi token yang diisi ulang dengan rate konstan. Request konsumsi token. Balance antara simplicity dan fairness, cocok buat production.

Kita pakai Token Bucket karena implementasinya efisien di Redis dan gampang di-tune untuk berbagai case.

Setup & Implementasi Token Bucket

Kenapa pakai Lua script? Karena Redis bisa eksekusi Lua secara atomic. Operasi check token + decrement + update timestamp jadi satu transaksi yang nggak bisa di-interrupt. Ini penting banget buat hindarin race condition di concurrent request.

Install Dependencies

# Install Redis di Ubuntu/Debian VPS
sudo apt update
sudo apt install redis-server -y
sudo systemctl enable redis-server

# Setup Go project
go mod init ratelimiter-demo
go get github.com/gofiber/fiber/v2
go get github.com/redis/go-redis/v9

Lua Script untuk Token Bucket

Buat file token_bucket.lua:

local key = KEYS[1]
local max_tokens = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local requested = tonumber(ARGV[3])
local now = tonumber(ARGV[4])

local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1])
local last_refill = tonumber(bucket[2])

if tokens == nil then
    tokens = max_tokens
    last_refill = now
else
    local elapsed = now - last_refill
    local new_tokens = math.min(max_tokens, tokens + (elapsed * refill_rate))
    tokens = new_tokens
    last_refill = now
end

if tokens >= requested then
    tokens = tokens - requested
    redis.call('HMSET', key, 'tokens', tokens, 'last_refill', last_refill)
    redis.call('EXPIRE', key, 3600)
    return 1
else
    return 0
end

Script ini cek token yang tersisa, hitung refill berdasarkan waktu yang udah lewat, kurangi token kalau request diperbolehkan, dan set TTL 1 jam untuk cleanup otomatis.

Build Middleware di Go Fiber

package main

import (
    "context"
    "crypto/sha256"
    "fmt"
    "os"
    "time"

    "github.com/gofiber/fiber/v2"
    "github.com/redis/go-redis/v9"
)

var (
    rdb *redis.Client
    ctx = context.Background()
    luaScript string
)

func init() {
    rdb = redis.NewClient(&redis.Options{
        Addr:         "localhost:6379",
        PoolSize:     100,
        MinIdleConns: 10,
    })

    scriptBytes, _ := os.ReadFile("token_bucket.lua")
    luaScript = string(scriptBytes)
}

func RateLimitMiddleware(maxTokens, refillRate int) fiber.Handler {
    return func(c *fiber.Ctx) error {
        userID := c.Get("X-User-ID")
        if userID == "" {
            userID = c.IP()
        }

        key := fmt.Sprintf("ratelimit:%x", sha256.Sum256([]byte(userID)))
        now := time.Now().Unix()

        result, err := rdb.Eval(ctx, luaScript, []string{key},
            maxTokens, float64(refillRate)/60.0, 1, now).Result()

        if err != nil {
            return c.Status(500).JSON(fiber.Map{
                "error": "Rate limiter unavailable",
            })
        }

        if result.(int64) == 0 {
            return c.Status(429).JSON(fiber.Map{
                "error": "Rate limit exceeded",
            })
        }

        return c.Next()
    }
}

func main() {
    app := fiber.New()

    app.Use(RateLimitMiddleware(100, 60)) // 100 token, refill 60/min

    app.Get("/api/data", func(c *fiber.Ctx) error {
        return c.JSON(fiber.Map{"message": "Success"})
    })

    app.Listen(":3000")
}

Handling Race Condition

Lua script udah atomic, jadi nggak perlu distributed lock tambahan untuk operasi rate limiting. Tapi kalau butuh koordinasi lebih complex (misal multi-key operation), pakai library kayak redsync untuk distributed mutex.

Production Best Practice

Konfigurasi Redis untuk High-Throughput

Edit /etc/redis/redis.conf:

maxmemory 2gb
maxmemory-policy allkeys-lru
timeout 300
tcp-keepalive 60
save ""
# Disable RDB kalau nggak butuh persistence
# Rate limiter data bisa hilang pas restart tanpa masalah

Set maxmemory-policy allkeys-lru biar Redis otomatis evict key lama kalau memory penuh. Data rate limiter sifatnya ephemeral, jadi kehilangan data pas restart bukan masalah besar.

Monitoring dengan Prometheus

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
)

var (
    rateLimitHits = promauto.NewCounterVec(prometheus.CounterOpts{
        Name: "ratelimit_requests_total",
    }, []string{"status"})
)

func RateLimitMiddleware(maxTokens, refillRate int) fiber.Handler {
    return func(c *fiber.Ctx) error {
        // ... kode sebelumnya ...

        if result.(int64) == 0 {
            rateLimitHits.WithLabelValues("rejected").Inc()
            return c.Status(429).JSON(fiber.Map{
                "error": "Rate limit exceeded",
            })
        }

        rateLimitHits.WithLabelValues("allowed").Inc()
        return c.Next()
    }
}

Testing Concurrent Request

# Install hey
go install github.com/rakyll/hey@latest

# Test 1000 request dari 50 concurrent user
hey -n 1000 -c 50 -H "X-User-ID: user123" http://localhost:3000/api/data

# Monitoring Redis
redis-cli --stat
redis-cli --bigkeys

Troubleshooting & Strategi Fallback

Connection pool exhausted? Naikkan PoolSize di Redis client config. Monitor metric PoolStats untuk track connection usage.

Redis down? Implementasi circuit breaker biar service nggak crash total:

func RateLimitMiddleware(maxTokens, refillRate int) fiber.Handler {
    return func(c *fiber.Ctx) error {
        // ... operasi rate limit ...

        if err != nil {
            // Log error tapi allow request (fail open)
            log.Printf("Redis error: %v", err)
            return c.Next()
        }
        // ... sisa kode ...
    }
}

Strategi fail-open ini controversial, tapi lebih baik daripada reject semua traffic pas Redis down. Untuk production critical, consider Redis Sentinel atau Cluster untuk high availability.

Langkah Selanjutnya

Token Bucket dengan Redis Lua kasih balance terbaik antara performa, konsistensi, dan simplicity untuk rate limiting terdistribusi. Implementasi ini bisa handle ribuan request per detik tanpa bottleneck, dan semua instance service bakal enforce limit yang sama.

Kalau traffic udah masuk level puluhan ribu RPS, pertimbangkan Redis Cluster untuk horizontal scaling. Atau kalau butuh logic yang lebih complex (multi-tier limits, dynamic quota, global + per-endpoint limits), lirik Envoy rate limit service yang lebih feature-rich meski setup-nya lebih ribet.

Yang bisa langsung dicoba: Deploy implementasi ini ke staging environment, test dengan traffic real pakai wrk atau hey, dan monitor Redis memory usage selama seminggu. Kalau stabil, baru push ke production dengan gradual rollout. Jangan lupa backup config Redis dan setup alerting untuk monitor rejected request rateβ€”kalau tiba-tiba spike, bisa jadi ada serangan atau limit yang terlalu ketat.

A
Aizu Dev

Tim penulis AizuDemy yang menyajikan tutorial teknologi, cloud, dan pengembangan perangkat lunak dalam Bahasa Indonesia.

πŸ’¬ Komentar (0)

Tulis Komentar

πŸ“– Artikel Terkait