Thuta Learning
System Design
ProjectsProgrammingintermediate

Project: Client တစ်ခုချင်းစီအတွက် Rate Limiter Service

ဒီခန်းပြီးရင် ဘာတတ်သွားမလဲ

  • Project: Client တစ်ခုချင်းစီအတွက် Rate Limiter Service concept ကို နားလည်ရှင်းပြနိုင်ရန်
  • နမူနာ diagram/code ကို ကိုယ်တိုင် လေ့လာပြီး trade-off များကို ခွဲခြမ်းစိတ်ဖြာနိုင်ရန်
  • Tutorial Platform project နှင့် production scenario တွင် မှန်ကန်စွာအသုံးချနိုင်ရန်

နားလည်ထားရမယ့် အချက်

global token bucket တစ်ခုတည်းဟာ server ကို overall overload ကနေ ကာကွယ်ပေးပေမယ့် fairness ပြဿနာ ရှိတယ် - client တစ်ယောက်က request burst ပို့ပြီး shared bucket ကို ဖုန်းကုန်သွားစေရင် ကျန် client တွေအားလုံးလည်း ဘာမှမကျူးလွန်ဘဲနဲ့ throttle ခံရတယ်။ ဒီပြဿနာကို ဖြေရှင်းဖို့ client တစ်ခုချင်းစီကို API key ဒါမှမဟုတ် user ID လို identifier နဲ့ key ထားတဲ့ သီးသန့် token bucket ပေးလိုက်ရင် bucket တွေက အချင်းချင်း သက်ဆိုင်မှုမရှိဘဲ refill/drain ဖြစ်တယ် - client noisy တစ်ယောက်ဟာ သူ့ quota ကိုပဲ ကုန်စေနိုင်တော့တယ်။ implementation အနေနဲ့ natural ဆုံးကတော့ client_id ကနေ TokenBucket instance ဆီ ချိတ်ထားတဲ့ dict တစ်ခု ဖြစ်ပြီး ဒီ client ကို ပထမဆုံးအကြိမ် တွေ့တဲ့အခါမှသာ lazily ဖန်တီးလိုက်ရင် ဘယ်တော့မှ မလာနိုင်တဲ့ client တွေအတွက် bucket ကြိုတင်ဖန်တီးစရာ မလိုတော့ဘူး။ တကယ့် design trade-off ကတော့ ဒီ dict က ဘယ်မှာနေထိုင်လဲဆိုတဲ့ အချက်ဖြစ်တယ် - process တစ်ခုတည်းရဲ့ memory ထဲမှာ သိမ်းထားတာက ရိုးရှင်းပြီး မြန်ပေမယ့် client တစ်ယောက်ရဲ့ request အားလုံးက server instance တစ်ခုတည်းကို ဝင်တဲ့အခါမှသာ အလုပ်လုပ်တယ်။ load balancer နောက်ကွယ်မှာ instance အများကြီးဆီ scale ဖြစ်သွားတဲ့ အခိုက်အတန့်ကနေစပြီး per-client state ကို Redis (atomic increment/expire operation နဲ့) လို shared store ဆီ ရွှေ့ရတော့တယ်၊ မရွှေ့ရင် client တစ်ယောက်ရဲ့ request တွေဟာ instance တစ်ခုစီက bucket ကို ဗလာဖြစ်နေတယ်လို့ ထင်နေတဲ့ instance တွေကို ပျံ့နှံ့ကျဲသွားလို့ ရသင့်ထက်ပိုတဲ့ token ရသွားနိုင်တယ်။

လက်တွေ့ scenario နဲ့ ချိတ်ကြည့်မယ်

ဒါဟာ Tutorial Platform က public API မှာ လိုအပ်လာမယ့် rate limiting ကို model လုပ်တာဖြစ်တယ် - ဥပမာ third-party integration ဒါမှမဟုတ် mobile app ကို lesson content ပေးနေတဲ့ endpoint တစ်ခု။ API key တစ်ခုချင်းစီက သူ့ bucket ကို ရရှိတယ်ဆိုရင် integration partner တစ်ယောက်ကနေ runaway script တစ်ခုက ကျန် endpoint အတူတူသုံးနေတဲ့ learner တွေရဲ့ response time ကို ဆိုးရွားစေလို့ မရနိုင်ဘူး။ အခုတော့ platform ဟာ Next.js deployment တစ်ခုတည်းဖြစ်နေတာမို့ memory ထဲသိမ်းထားတဲ့ dict က တရားဝင် stopgap တစ်ခုအဖြစ် သုံးလို့ရသေးတယ်၊ server instance တစ်ခုထက်ပိုပြီး scale ဖြစ်တဲ့အခိုက်အတန့်ကနေစပြီးတော့ (ဥပမာ Vercel region အများကြီး) ဒီ memory-based version ဟာ instance တစ်ခုချင်းစီမှာ limit ကို silently under-enforce လုပ်သွားလိမ့်မယ် - ဒါကပဲ bucket state ကို Redis ဆီ ရွှေ့ဖို့ trigger ဖြစ်တယ်။

အတူတူ စမ်းရေးကြည့်မယ်

python
import time


class TokenBucket:
    def __init__(self, capacity: int, refill_rate: float):
        self.capacity = capacity
        self.refill_rate = refill_rate  # tokens per second
        self.tokens = float(capacity)
        self.last_refill = time.monotonic()

    def _refill(self):
        now = time.monotonic()
        elapsed = now - self.last_refill
        self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_rate)
        self.last_refill = now

    def allow(self) -> bool:
        self._refill()
        if self.tokens >= 1:
            self.tokens -= 1
            return True
        return False


class RateLimiterService:
    def __init__(self, capacity: int, refill_rate: float):
        self.capacity = capacity
        self.refill_rate = refill_rate
        self._buckets = {}  # client_id -> TokenBucket

    def _get_bucket(self, client_id: str) -> TokenBucket:
        if client_id not in self._buckets:
            self._buckets[client_id] = TokenBucket(self.capacity, self.refill_rate)
        return self._buckets[client_id]

    def allow_request(self, client_id: str) -> bool:
        return self._get_bucket(client_id).allow()


if __name__ == "__main__":
    limiter = RateLimiterService(capacity=3, refill_rate=0)  # no refill during demo

    print("Client A (bursty, sends 5 requests):")
    for i in range(5):
        print(f"  request {i + 1}: {'allowed' if limiter.allow_request('client-a') else 'THROTTLED'}")

    print("Client B (sends 2 requests):")
    for i in range(2):
        print(f"  request {i + 1}: {'allowed' if limiter.allow_request('client-b') else 'THROTTLED'}")
You should see
Client A ရဲ့ ပထမဆုံး request 3 ခုက allowed ဖြစ်ပြီး bucket ဗလာဖြစ်သွားတဲ့အခါ ကျန် 2 ခုက THROTTLED ဖြစ်သွားတယ်၊ Client B ကတော့ သူ့ဟာသူ သီးသန့် bucket ရှိနေတာမို့ request 2 ခုစလုံး allowed ဖြစ်တယ်။

၅ မိနစ် စမ်းကြည့်

`remove_idle_clients(idle_seconds)` method တစ်ခု ထပ်ထည့်ပြီး `last_refill` timestamp က ပေးထားတဲ့ threshold ထက် ကြာနေတဲ့ client တွေရဲ့ bucket ကို ဖယ်ရှားပေးပါ - ဒါမှ service ရဲ့ အသက်တာကာလ တစ်လျှောက် client ID အသစ်တွေ ဆက်တိုက်ဝင်လာနေတဲ့အခါ dict က အကန့်အသတ်မရှိ မကြီးထွားတော့ဘူး။

သတိလေးတစ်ချက်

Client တိုင်းအတွက် shared TokenBucket တစ်ခုတည်းသုံးထားရင် (client_id တစ်ခုချင်းစီအတွက် သီးသန့်မဟုတ်ဘဲ) - client တစ်ယောက်တည်းက burst request ပို့လိုက်ရင် shared bucket ကုန်သွားပြီး ကျန်တဲ့ ကောင်းမွန်တဲ့ client တွေကိုပါ throttle ဖြစ်စေနိုင်တယ်။

Per-client bucket dict ကို အကန့်အသတ်မရှိ တိုးလာခွင့်ပြုထားရင် - client_id အသစ်တိုင်းက bucket အသစ်ဖန်တီးပြီး ဘယ်တော့မှ မဖြုတ်ဘူးဆိုရင် API key ကွဲပြားမှု သန်းနှင့်ချီရှိလာတဲ့ service တစ်ခုအတွက် memory ဟာ အကန့်အသတ်မရှိ ယိုစိမ့်သွားနိုင်တယ်။

Wikipedia — Token bucketSystem Design

ဒီနေရာမှာ လူအများမှားတတ်တယ်

  • Client တိုင်းအတွက် shared TokenBucket တစ်ခုတည်းသုံးထားရင် (client_id တစ်ခုချင်းစီအတွက် သီးသန့်မဟုတ်ဘဲ) - client တစ်ယောက်တည်းက burst request ပို့လိုက်ရင် shared bucket ကုန်သွားပြီး ကျန်တဲ့ ကောင်းမွန်တဲ့ client တွေကိုပါ throttle ဖြစ်စေနိုင်တယ်။
  • Per-client bucket dict ကို အကန့်အသတ်မရှိ တိုးလာခွင့်ပြုထားရင် - client_id အသစ်တိုင်းက bucket အသစ်ဖန်တီးပြီး ဘယ်တော့မှ မဖြုတ်ဘူးဆိုရင် API key ကွဲပြားမှု သန်းနှင့်ချီရှိလာတဲ့ service တစ်ခုအတွက် memory ဟာ အကန့်အသတ်မရှိ ယိုစိမ့်သွားနိုင်တယ်။
  • Design decision တစ်ခုကို production system ပေါ် တိုက်ရိုက်မကျင့်သုံးမီ load/traffic assumption များကို အရင်အတည်ပြုပါ။

လေ့ကျင့်ခန်း

`remove_idle_clients(idle_seconds)` method တစ်ခု ထပ်ထည့်ပြီး `last_refill` timestamp က ပေးထားတဲ့ threshold ထက် ကြာနေတဲ့ client တွေရဲ့ bucket ကို ဖယ်ရှားပေးပါ - ဒါမှ service ရဲ့ အသက်တာကာလ တစ်လျှောက် client ID အသစ်တွေ ဆက်တိုက်ဝင်လာနေတဲ့အခါ dict က အကန့်အသတ်မရှိ မကြီးထွားတော့ဘူး။

You'll know it worked when: Client A ရဲ့ ပထမဆုံး request 3 ခုက allowed ဖြစ်ပြီး bucket ဗလာဖြစ်သွားတဲ့အခါ ကျန် 2 ခုက THROTTLED ဖြစ်သွားတယ်၊ Client B ကတော့ သူ့ဟာသူ သီးသန့် bucket ရှိနေတာမို့ request 2 ခုစလုံး allowed ဖြစ်တယ်။

Project: Client တစ်ခုချင်းစီအတွက် Rate Limiter Service | Thuta Learning