DISTRIBUTED-SYSTEMS RATE-LIMITING API-SECURITY SCALABILITY REDIS MICROSERVICES BACKEND WEB-DEVELOPMENT CONCURRENCY SYSTEM-DESIGN BEST-PRACTICES

Distributed Rate Limiting: Mengontrol Akses API di Aplikasi Skala Besar dengan Redis

⏱️ 11 menit baca
👨‍💻

Distributed Rate Limiting: Mengontrol Akses API di Aplikasi Skala Besar dengan Redis

1. Pendahuluan

Bayangkan aplikasi web Anda semakin populer. Traffic membludak, dan API Anda diserbu ribuan (bahkan jutaan) request per detik. Terdengar seperti mimpi, bukan? Tapi di balik euforia itu, ada potensi masalah besar: beban berlebih pada server, database yang kewalahan, hingga serangan Denial of Service (DoS) yang bisa membuat aplikasi Anda tumbang.

Di sinilah Rate Limiting berperan. Secara sederhana, rate limiting adalah mekanisme untuk membatasi jumlah request yang dapat diterima sebuah API dari user atau IP tertentu dalam periode waktu tertentu. Ini seperti penjaga gerbang yang memastikan tidak ada terlalu banyak orang masuk sekaligus, menjaga ketertiban dan kapasitas.

Namun, dunia web modern jarang sekali hanya menggunakan satu server. Kita hidup di era microservices, cloud computing, dan horizontal scaling. Artinya, aplikasi Anda mungkin berjalan di puluhan, ratusan, atau bahkan ribuan instance server yang terdistribusi. Ini memunculkan tantangan baru: Bagaimana cara menerapkan rate limiting yang konsisten dan akurat di seluruh instance server tersebut? Inilah yang kita sebut Distributed Rate Limiting.

Tanpa distributed rate limiting yang efektif, setiap instance server akan menghitung limitnya sendiri, dan secara kolektif, aplikasi Anda bisa menerima request jauh di atas batas yang diinginkan. Artikel ini akan membawa Anda menyelami bagaimana cara membangun sistem distributed rate limiting yang kokoh, dengan fokus pada implementasi praktis menggunakan Redis, sebuah in-memory data store yang sangat cocok untuk kasus ini.

2. Mengapa Distributed Rate Limiting Menjadi Krusial?

Sebelum kita masuk ke “bagaimana”, mari pahami “mengapa” ini sangat penting di arsitektur modern:

Dalam sistem terdistribusi, tantangannya adalah bagaimana semua instance aplikasi Anda “berkomunikasi” dan “berbagi” informasi tentang berapa banyak request yang sudah diterima dari user atau IP tertentu secara global. Jika setiap instance bekerja sendiri, limit yang Anda tetapkan (misalnya 100 request per menit) bisa jadi 100 request per menit per instance, yang artinya total request bisa 1000 request per menit jika Anda memiliki 10 instance. Ini jelas bukan yang Anda inginkan.

3. Redis: Sang Penjaga Gerbang Terdistribusi

Redis adalah pilihan yang sangat populer untuk distributed rate limiting karena beberapa alasan:

📌 Konsep Penting: Dalam distributed rate limiting, Redis akan bertindak sebagai centralized counter atau shared state yang diakses oleh semua instance aplikasi Anda.

4. Implementasi Distributed Rate Limiting dengan Redis (Sliding Window Counter)

Ada beberapa algoritma rate limiting yang bisa diimplementasikan dengan Redis, seperti Fixed Window, Sliding Window Log, dan Sliding Window Counter. Untuk artikel ini, kita akan fokus pada Sliding Window Counter karena menawarkan keseimbangan yang baik antara akurasi dan efisiensi, dan relatif mudah diimplementasikan.

Algoritma Sliding Window Counter: Algoritma ini membagi waktu menjadi “jendela” kecil (misalnya per detik) dan melacak request dalam jendela tersebut. Untuk menentukan apakah sebuah request diizinkan dalam satu “periode” (misalnya 60 detik), kita menghitung jumlah request dari jendela saat ini dan jendela sebelumnya, dengan mempertimbangkan proporsi waktu yang telah berlalu di jendela sebelumnya.

Kedengarannya rumit? Jangan khawatir, Redis membuatnya lebih mudah. Kita akan menggunakan Redis INCR (increment) dan EXPIRE untuk melacak request per “jendela” kecil.

🎯 Contoh Skenario: Kita ingin membatasi user ke 100 request per 60 detik (1 menit).

  1. Identifikasi Klien: Setiap request perlu diidentifikasi. Ini bisa berupa IP address, API Key, atau User ID. Kita akan gunakan user_id:123 sebagai contoh.
  2. Definisikan Jendela Waktu: Periode kita adalah 60 detik. Kita bisa membaginya menjadi jendela yang lebih kecil, misalnya 1 detik.
  3. Lacak Request: Untuk setiap request dari user_id:123, kita akan menyimpan counter di Redis.

Langkah-langkah Implementasi:

Pertama, kita butuh sebuah fungsi isRateLimited(userId, limit, windowSizeInSeconds):

// Contoh di Node.js dengan library 'ioredis'
const Redis = require('ioredis');
const redis = new Redis({
  host: 'localhost', // Ganti dengan host Redis Anda
  port: 6379,
});

/**
 * Mengecek apakah sebuah user/klien terkena rate limit.
 * Menggunakan algoritma Sliding Window Counter dengan Redis.
 *
 * @param {string} userId - ID unik untuk user/klien (misal: IP, API Key, User ID)
 * @param {number} limit - Batas request yang diizinkan dalam windowSizeInSeconds
 * @param {number} windowSizeInSeconds - Ukuran jendela waktu dalam detik (misal: 60 detik)
 * @returns {Promise<boolean>} True jika terkena rate limit, False jika diizinkan
 */
async function isRateLimited(userId, limit, windowSizeInSeconds) {
  const now = Date.now(); // Waktu saat ini dalam milidetik
  const currentWindow = Math.floor(now / 1000); // Jendela waktu saat ini (per detik)
  const prevWindow = currentWindow - 1; // Jendela waktu sebelumnya

  // Key Redis untuk jendela saat ini dan sebelumnya
  const currentKey = `rate_limit:${userId}:${currentWindow}`;
  const prevKey = `rate_limit:${userId}:${prevWindow}`;

  // Pipeline untuk operasi Redis yang atomic
  const pipeline = redis.pipeline();

  // 1. Increment counter untuk jendela saat ini
  pipeline.incr(currentKey);
  // 2. Set TTL untuk jendela saat ini (agar otomatis dihapus setelah periode tertentu)
  // Kita set TTL 2x ukuran jendela untuk memastikan data jendela sebelumnya masih ada saat dibutuhkan
  pipeline.expire(currentKey, windowSizeInSeconds * 2);

  // 3. Ambil nilai counter dari jendela sebelumnya
  pipeline.get(prevKey);

  const results = await pipeline.exec();

  // Hasil dari pipeline:
  // results[0][1]: nilai setelah incr currentKey
  // results[1][1]: hasil expire (1 jika berhasil, 0 jika key tidak ada/sudah ada TTL)
  // results[2][1]: nilai dari prevKey (bisa null jika belum ada)

  const currentCount = parseInt(results[0][1], 10);
  const prevCount = parseInt(results[2][1] || '0', 10); // Jika null, anggap 0

  // Hitung berapa proporsi waktu yang sudah berlalu di jendela saat ini
  const timeInCurrentWindow = now % (windowSizeInSeconds * 1000); // Waktu yang sudah berlalu dalam periode 1 menit
  const weightPrevWindow = 1 - (timeInCurrentWindow / (windowSizeInSeconds * 1000)); // Bobot jendela sebelumnya

  // Total request yang dihitung dalam periode sliding window
  const totalRequests = currentCount + (prevCount * weightPrevWindow);

  console.log(`User ${userId}: Current Window Count: ${currentCount}, Previous Window Count: ${prevCount}, Weighted Prev Count: ${prevCount * weightPrevWindow}, Total Weighted Requests: ${totalRequests}`);

  return totalRequests > limit;
}

// --- Contoh Penggunaan ---
async function main() {
  const userId = 'user_api_key_123';
  const limit = 100; // 100 request
  const window = 60; // dalam 60 detik (1 menit)

  for (let i = 0; i < 110; i++) { // Coba 110 request
    const limited = await isRateLimited(userId, limit, window);
    if (limited) {
      console.warn(`⚠️ Request ${i + 1} untuk ${userId} DITOLAK (Rate Limited)!`);
      // Dalam aplikasi nyata, kirim HTTP 429 Too Many Requests
    } else {
      console.log(`✅ Request ${i + 1} untuk ${userId} DITERIMA.`);
    }
    // Simulate some delay for realism, or remove for rapid testing
    // await new Promise(resolve => setTimeout(resolve, 50));
  }

  // Coba setelah beberapa waktu
  console.log('\n--- Menunggu 30 detik ---');
  await new Promise(resolve => setTimeout(resolve, 30 * 1000));

  console.log('\n--- Mencoba lagi setelah 30 detik ---');
  for (let i = 0; i < 20; i++) {
    const limited = await isRateLimited(userId, limit, window);
    if (limited) {
      console.warn(`⚠️ Request ${i + 1} untuk ${userId} DITOLAK (Rate Limited)!`);
    } else {
      console.log(`✅ Request ${i + 1} untuk ${userId} DITERIMA.`);
    }
  }

  redis.quit();
}

main();

Penjelasan Kode:

💡 Tips Praktis:

5. Pertimbangan Lebih Lanjut

⚠️ Race Condition dan Atomicity

Salah satu alasan utama menggunakan Redis adalah kemampuannya untuk melakukan operasi atomic. Bayangkan jika incr dan get tidak atomic dan dilakukan oleh dua instance server secara bersamaan. Bisa saja satu instance membaca nilai lama, sementara instance lain sudah mengupdate nilai, menyebabkan perhitungan yang salah. redis.pipeline() membantu memastikan bahwa serangkaian operasi ini dieksekusi secara berurutan di server Redis, meminimalisir race condition.

📈 Skalabilitas Redis

Untuk aplikasi dengan traffic sangat tinggi, Anda mungkin perlu memikirkan skalabilitas Redis itu sendiri. Beberapa opsi:

🛡️ Mengatasi Burst Traffic

Algoritma Sliding Window Counter yang kita bahas cukup baik, tetapi mungkin masih rentan terhadap burst traffic di awal jendela waktu. Algoritma seperti Token Bucket atau Leaky Bucket seringkali lebih baik dalam menghaluskan burst ini. Redis juga bisa digunakan untuk mengimplementasikan algoritma-algoritma tersebut, misalnya dengan menggunakan SET dan EXPIRE untuk token, atau LIST untuk queue di leaky bucket.

✅ Keamanan

Pastikan koneksi aplikasi Anda ke Redis aman (misalnya menggunakan SSL/TLS) dan Redis itu sendiri dilindungi (tidak terekspos ke internet publik, menggunakan autentikasi).

Kesimpulan

Distributed Rate Limiting adalah komponen fundamental untuk membangun aplikasi modern yang tangguh, aman, dan skalabel. Dengan memahami tantangan unik yang muncul di lingkungan terdistribusi dan memanfaatkan kekuatan Redis, Anda bisa melindungi API dan infrastruktur Anda dari penyalahgunaan dan beban berlebih.

Implementasi Sliding Window Counter dengan Redis menawarkan solusi yang efisien dan akurat untuk sebagian besar kasus penggunaan. Ingatlah untuk selalu memantau performa dan perilaku rate limiting Anda, serta menyesuaikan strategi sesuai kebutuhan aplikasi dan pola traffic Anda. Dengan demikian, Anda bisa memastikan aplikasi Anda tetap stabil dan responsif, bahkan saat popularitasnya meroket!

🔗 Baca Juga