Rate Limiter Service ডিজাইন
- ●Rate limiter নির্ধারণ করে একজন client নির্দিষ্ট সময়ে কতগুলো request পাঠাতে পারবে, এবং সীমা পেরোলে 429 দিয়ে আটকায়।
- ●Token Bucket সবচেয়ে জনপ্রিয় algorithm — burst সামলায়, আর গড়ে একটা নির্দিষ্ট rate ধরে রাখে।
- ●অনেকগুলো server থাকলে counter Redis-এ central রাখতে হয়, নাহলে প্রতিটা server আলাদা গুনে সীমা ফাঁক হয়ে যায়।
ধরো তোমার একটা public API আছে, আর একজন ব্যবহারকারী (বা একটা bot) সেকেন্ডে হাজার হাজার request পাঠিয়ে সব server-এর শক্তি খেয়ে ফেলছে। বাকি সবাই তখন service পাচ্ছে না। সমাধান — প্রত্যেককে একটা সীমা বেঁধে দাও: "প্রতি মিনিটে সর্বোচ্চ ১০০টা request"। এই সীমা প্রয়োগকারী system-ই Rate Limiter Service। চলো একে ধাপে ধাপে ডিজাইন করি।
১. সমস্যা বোঝা (Requirements)
Functional requirements:
- প্রতিটা incoming request-এর জন্য সিদ্ধান্ত দেবে: পাস (allow) নাকি আটকাও (reject)।
- সীমা configure করা যাবে — যেমন "প্রতি IP, প্রতি মিনিটে ১০০"; "প্রতি user, প্রতি সেকেন্ডে ১০"।
- সীমা পেরোলে স্পষ্ট error দেবে (429 Too Many Requests) এবং কখন আবার চেষ্টা করা যাবে তা জানাবে।
- বিভিন্ন rule সমর্থন করবে — per-user, per-IP, per-API-key।
Non-functional requirements:
- Low latency — rate limiter প্রতিটা request-এর সামনে বসে, তাই এর নিজের delay প্রায় শূন্য (~1ms) হতে হবে। নাহলে সে নিজেই bottleneck।
- High availability — rate limiter বন্ধ হলে পুরো API বন্ধ হয়ে যেতে পারে।
- Distributed-এ সঠিকতা — অনেকগুলো server থাকলেও সীমা যেন একটাই থাকে।
- Memory efficient — কোটি কোটি user-এর counter অল্প memory-তে রাখতে হবে।
Rate limiter অনেকটা মেট্রোরেলের গেটে রাখা টোকেনের মতো। বুফেতেও নিয়ম — একবারে এক প্লেট। তুমি একসাথে দশ প্লেট নিয়ে গেলে বাকিদের জন্য কিছু থাকবে না। rate limiter ঠিক তেমন — সবাই যেন ন্যায্য ভাগ পায়।
২. স্কেল আন্দাজ (Estimation)
ধরি আমাদের API দিনে ১০ বিলিয়ন request সামলায়, এবং প্রতিটা request-এর সিদ্ধান্ত rate limiter-কেই নিতে হয়।
মোট request/দিন = 10,000,000,000 (10B)
গড় QPS = 10B / 86,400 sec ≈ 115,000 req/sec
Peak QPS (×3) ≈ 350,000 req/sec → প্রতিটাতে rate-check লাগবে
Memory (Token Bucket):
প্রতি user-key: counter + timestamp ≈ 20 bytes
ধরি active key = 100 মিলিয়ন
মোট memory = 100M × 20 bytes ≈ 2 GB → একটা Redis-এই ধরে যায়
Latency budget: প্রতি check < 1 ms (request-এর hot path-এ আছে)
মূল উপলব্ধি — অসংখ্য check, কিন্তু প্রতিটা হালকা ও দ্রুত। তাই in-memory store (Redis) নিখুঁত, আর memory খুবই কম লাগে।
৩. API ডিজাইন
Rate limiter সাধারণত একটা middleware হিসেবে কাজ করে, কিন্তু internally তার একটা check interface থাকে।
Internal check (প্রতি request-এ):
allowRequest(key, rule) → { allowed: true|false, remaining: 42, retryAfter: 0 }
key = "user:123" বা "ip:203.0.113.5"
rule = { limit: 100, windowSec: 60 }
Client যা response পায় (সীমা পেরোলে):
HTTP 429 Too Many Requests
Headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
Retry-After: 30 ← ৩০ সেকেন্ড পরে আবার চেষ্টা করো
Retry-After header খুব গুরুত্বপূর্ণ — এটা client-কে ভদ্রভাবে অপেক্ষা করতে বলে, যাতে সে আবার পাগলের মতো retry না করে।
৪. ডেটা মডেল
Rate limiter-এ স্থায়ী relational data নেই — শুধু প্রতিটা key-এর জন্য সাময়িক counter। তাই একটা KV/in-memory store-ই যথেষ্ট।
| Field | Type | বর্ণনা |
|---|---|---|
| key | string | যেমন user:123 বা ip:203.0.113.5 |
| tokens | float | এখন bucket-এ কত token অবশিষ্ট |
| last_refill | timestamp | সর্বশেষ কখন token refill হয়েছিল |
| (TTL) | expiry | নিষ্ক্রিয় key যাতে নিজে নিজে মুছে যায় |
SQL না NoSQL? এখানে disk-ভিত্তিক SQL DB একদম বেমানান — প্রতিটা request-এ একটা DB write খুবই slow। আমাদের চাই in-memory KV store যাতে atomic operation আছে — Redis। Redis মাইক্রোসেকেন্ডে read/write করে, atomic INCR/Lua script সমর্থন করে, আর key-তে TTL দিয়ে পুরোনো counter আপনা থেকেই মুছে ফেলে।
৫. হাই-লেভেল ডিজাইন
Component আর flow:
- Client — request পাঠায়।
- Load Balancer — request গুলো অনেক app server-এ ভাগ করে।
- Rate Limiter Middleware — প্রতিটা app server-এ বসে, request আসলেই প্রথমে এর কাছে যায়।
- Redis (central counter store) — সব server এখান থেকেই counter পড়ে/লেখে, তাই সীমা সবার কাছে একটাই।
- Backend service — পাস হওয়া request আসল কাজ করে।
Request flow:
- Client request পাঠায়, load balancer একটা app server-এ পাঠায়।
- Rate limiter middleware key বের করে (যেমন
user:123)। - Redis-এ atomic ভাবে চেক করে — এই key-এর bucket-এ token আছে কিনা।
- থাকলে → token কমিয়ে request backend-এ পাঠিয়ে দেয়।
- না থাকলে → request আটকে দিয়ে
429+Retry-Afterফেরত পাঠায়, backend-এ যায়-ই না।
Rate limiter request-এর hot path-এ বসে — তাই Redis ডাউন হলে কী হবে আগে ঠিক করো। দুটো নীতি: "fail-open" (Redis ডাউন হলে সব পাস করে দাও, নিরাপত্তা কমে কিন্তু service চালু থাকে) বা "fail-closed" (সব আটকাও, নিরাপদ কিন্তু service বন্ধ)। বেশিরভাগ public API fail-open বেছে নেয়, কারণ rate limiter-এর জন্য পুরো service বন্ধ হওয়া অগ্রহণযোগ্য।
৬. গভীরে (Deep Dive)
৬.১ Algorithm: Token Bucket
একটা bucket-এ নির্দিষ্ট হারে token জমা হয় (যেমন সেকেন্ডে ১০টা, সর্বোচ্চ ১০০)। প্রতিটা request একটা token খরচ করে — token থাকলে পাস, না থাকলে block। এর সৌন্দর্য — অল্প সময়ের burst সামলায় (জমে থাকা token দিয়ে), কিন্তু দীর্ঘমেয়াদে গড় rate সীমার ভেতর রাখে। নিচের tool-এ নিজে চালিয়ে দেখো — বারবার request পাঠাও, token শেষ হলে কীভাবে block হয়, তারপর token refill হলে আবার পাস:
বিকল্প algorithm:
- Fixed Window — প্রতি মিনিটে গুনে reset। সরল, কিন্তু window-এর সীমানায় double burst-এর সমস্যা (একটা minute-এর শেষে ১০০ + পরের minute-এর শুরুতে ১০০ = ১ সেকেন্ডে ২০০)।
- Sliding Window — গত ঠিক ৬০ সেকেন্ডের request গুনে দেখে, তাই সীমানার সমস্যা নেই। বেশি নিখুঁত কিন্তু একটু বেশি memory/হিসাব লাগে।
৬.২ Counter কোথায় থাকবে — Distributed Rate Limiting
সবচেয়ে বড় ফাঁদ এখানেই। ধরো ৫টা app server, প্রতিটা নিজের memory-তে counter রাখছে। তাহলে একজন user আসলে ৫ × ১০০ = ৫০০ request পাঠাতে পারবে — সীমা ভেঙে গেল!
সমাধান — counter central Redis-এ রাখো, সব server একই জায়গা থেকে গোনে। কিন্তু "পড়ো → হিসাব করো → লেখো" এই তিন ধাপের মাঝে race condition হতে পারে (দুই server একসাথে পড়ে দুজনেই পাস ভাবে)। তাই Redis-এর atomic operation বা একটা Lua script ব্যবহার করো, যা পুরো check-and-decrement একটা atomic ধাপে করে।
খুব high scale-এ Redis-এর latency বাঁচাতে কিছু system হালকা local cache + Redis sync ব্যবহার করে — প্রতিটা server অল্প token নিজে রাখে, মাঝে মাঝে Redis-এর সাথে মিলিয়ে নেয়। এতে সামান্য নির্ভুলতা কমে, কিন্তু latency অনেক কমে।
৬.৩ Key design
key ঠিক করাটা গুরুত্বপূর্ণ। বিভিন্ন স্তরে limit দিতে আলাদা key:
ip:203.0.113.5— anonymous traffic ঠেকাতে।user:123— logged-in user-এর fair use।apikey:abc:endpoint:/search— নির্দিষ্ট API key-এর নির্দিষ্ট endpoint-এর সীমা।
৭. বটলনেক ও স্কেলিং
- Redis একটাই node = bottleneck — sharding করো (key-এর hash অনুযায়ী আলাদা Redis node), যাতে load ছড়ায়। যেহেতু প্রতিটা key স্বাধীন, sharding সহজ।
- Redis availability — replica + automatic failover (Redis Sentinel/Cluster) রাখো, যাতে একটা node মরলেও সীমা প্রয়োগ চলে।
- Latency — Redis কাছাকাছি (same region/AZ) রাখো; প্রয়োজনে local token cache দিয়ে round-trip কমাও।
- Hot key — কোনো একটা key-তে (যেমন এক বিশাল customer) যদি অস্বাভাবিক traffic আসে, সেই key-কে আলাদা handle/shard করো।
৮. সারসংক্ষেপ
Rate limiter ছোট কিন্তু গভীর — এর মূল হলো একটা algorithm (Token Bucket) আর একটা সিদ্ধান্ত: counter কোথায় থাকবে। single server-এ memory-তেই চলে, কিন্তু distributed হলে central Redis + atomic operation ছাড়া সীমা ফাঁক হয়ে যায়। আর সবসময় 429 + Retry-After দিয়ে client-কে ভদ্রভাবে জানাও।
Interviewer সবচেয়ে বেশি দেখতে চায় তুমি distributed সমস্যাটা ধরতে পারো কিনা — "প্রতিটা server আলাদা গুনলে সীমা ৫ গুণ ফাঁক হয়ে যায়, তাই Redis-এ central atomic counter চাই"। সাথে fail-open vs fail-closed-এর trade-off আর সঠিক 429/Retry-After header বললে তুমি পরিণত দেখাবে।
মিনি কুইজ
1. Token Bucket algorithm-এর মূল সুবিধা কী?
2. একাধিক app server থাকলে rate limit counter কোথায় রাখা উচিত?
3. সীমা পেরোলে server কোন HTTP status আর header পাঠায়?