Build Rate Limiter Terdistribusi Pakai Redis Lua & Go di VPS
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.
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.




π¬ Komentar (0)
Tulis Komentar