পাঠ ৪৪ · ৫১-এর মধ্যে · মডিউল ১১
Home / Courses / System Design / কেস স্টাডি: রেট লিমিটার

কেস স্টাডি: রেট লিমিটার ডিজাইন করা

Case study: designing a rate limiter
১০ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • কেন একটি একক-মেশিন রেট লিমিটিং অ্যালগরিদম মাল্টি-সার্ভার পরিবেশে ব্যর্থ হয়
  • শেয়ার্ড স্টোর দিয়ে কীভাবে সব ইনস্ট্যান্স জুড়ে একটাই সামঞ্জস্যপূর্ণ লিমিট বলবৎ করা যায়
  • রেট লিমিটার কোথায় বসানো উচিত এবং কেন (API গেটওয়ে, L26)
  • fail-open বনাম fail-closed ট্রেড-অফ এবং শেয়ার্ড স্টোরের নেটওয়ার্ক-হপ খরচ

১ · রিকোয়ারমেন্ট

ফাংশনাল
প্রতিটি ক্লায়েন্ট (API কী/IP/ইউজার আইডি অনুযায়ী) একটি নির্দিষ্ট সময়-উইন্ডোতে সর্বোচ্চ N রিকোয়েস্ট করতে পারবে।
নন-ফাংশনাল
লিমিটার নিজে দ্রুত ও সঠিক হতে হবে — এবং একাধিক সার্ভার ইনস্ট্যান্স জুড়ে সঠিক থাকতে হবে, যা L35-এর একক-মেশিন অ্যালগরিদমের চেয়ে একটি নতুন চ্যালেঞ্জ।

২ · মূল সমস্যা — কেন লোকাল কাউন্টার এখানে ভেঙে পড়ে

L35-এ আমরা টোকেন বাকেট ও স্লাইডিং উইন্ডো বাস্তবায়ন করেছিলাম একটি প্রোগ্রামের ভেতরে একটি একক মেমরি-স্থিতি (in-memory state) ব্যবহার করে। কিন্তু একটি বাস্তব API-এর পেছনে সাধারণত লোড ব্যালেন্সারের (L11) পেছনে একাধিক স্টেটলেস অ্যাপ সার্ভার (L12) চলে। যদি প্রতিটি সার্ভার তার নিজস্ব আলাদা ইন-মেমরি কাউন্টার রাখে, তাহলে একই ক্লায়েন্ট ৩টি সার্ভার জুড়ে রাউন্ড-রবিন রাউট হলে, কার্যকরভাবে তার প্রকৃত লিমিট হয়ে যায় N × ৩ (কারণ প্রতিটি সার্ভার আলাদা করে N পর্যন্ত গণনা করে) — এটিই ঠিক সেই সমস্যা যা স্টেটফুল বনাম স্টেটলেস আলোচনায় (L12) দেখা গিয়েছিল।

৩ · সমাধান — শেয়ার্ড স্টোর

সমাধান হলো L12-এর নীতিই আবার প্রয়োগ করা — "সার্ভার নিজে কোনো স্টেট রাখবে না, একটি শেয়ার্ড স্টোর রাখবে।" সব অ্যাপ সার্ভার ইনস্ট্যান্স একটি কেন্দ্রীয়, দ্রুত কী-ভ্যালু স্টোরে (বাস্তবে সাধারণত Redis) পড়ে-লেখে। L35-এর টোকেন বাকেট বা স্লাইডিং উইন্ডো অ্যালগরিদম অপরিবর্তিত থাকে — শুধু তার "স্টেট" (কতগুলো টোকেন বাকি, বা কোন টাইমস্ট্যাম্পগুলো উইন্ডোতে আছে) এখন প্রতিটি সার্ভারের নিজস্ব মেমরির বদলে শেয়ার্ড স্টোরে থাকে।

ক্লায়েন্ট Client API গেটওয়ে (L26) + Rate Limiter গেটওয়ে ইনস্ট্যান্স ১ Instance 1 গেটওয়ে ইনস্ট্যান্স ২ Instance 2 শেয়ার্ড স্টোর Redis-like (one counter)
গেটওয়ের প্রতিটি ইনস্ট্যান্স একই শেয়ার্ড স্টোরকে জিজ্ঞাসা করে — তাই একটি ক্লায়েন্টের জন্য একটাই ধারাবাহিক গণনা থাকে, কোন ইনস্ট্যান্স রিকোয়েস্ট ধরল তা নির্বিশেষে।

৪ · প্লেসমেন্ট — API গেটওয়েতে কেন

রেট লিমিটার সাধারণত API গেটওয়েতে (L26) বসানো হয়, প্রতিটি ব্যাকএন্ড মাইক্রোসার্ভিসে আলাদা করে বসানোর বদলে — একটি একক জায়গায় সব ইনকামিং ট্রাফিকের জন্য সামঞ্জস্যপূর্ণ নীতি প্রয়োগ হয়, প্রতিটি সার্ভিসকে নিজে থেকে লিমিটিং লজিক ডুপ্লিকেট করতে হয় না, এবং অতিরিক্ত ট্রাফিক ব্যাকএন্ডে পৌঁছানোর আগেই বাতিল হয় (রিসোর্স বাঁচায়)।

৫ · ট্রেড-অফ — শেয়ার্ড স্টোরের খরচ ও fail-open নীতি

শেয়ার্ড স্টোর যোগ করার মূল্য — প্রতিটি রিকোয়েস্টে একটি অতিরিক্ত নেটওয়ার্ক হপ (লিমিটার স্টোরকে জিজ্ঞাসা করা), এবং এই স্টোর নিজেই এখন একটি ক্রিটিক্যাল ডিপেন্ডেন্সি — যদি এটি ডাউন হয়, পুরো সিস্টেমের রেট লিমিটিং ভেঙে পড়ে। এই পরিস্থিতিতে বেশিরভাগ বাস্তব সিস্টেম fail-open নীতি নেয় — স্টোর অনুপলব্ধ হলে ট্রাফিক ব্লক না করে সবকিছু পাস করতে দেওয়া হয় (সাময়িকভাবে আনলিমিটেড রেখে), কারণ পুরো API বন্ধ করে দেওয়া (fail-closed) সাধারণত রেট লিমিট সাময়িকভাবে না থাকার চেয়ে অনেক বেশি খারাপ ফলাফল।

Python
def sliding_window_allow(store, client_id, now, window_size, limit):
    """L35-এর স্লাইডিং উইন্ডো, কিন্তু স্টেট একটি শেয়ার্ড dict-এ (Redis-এর স্ট্যান্ড-ইন)।"""
    timestamps = store.setdefault(client_id, [])
    cutoff = now - window_size
    store[client_id] = [t for t in timestamps if t > cutoff]
    if len(store[client_id]) < limit:
        store[client_id].append(now)
        return True
    return False

def local_allow(local_store, instance_id, client_id, now, window_size, limit):
    """ভুল পদ্ধতি: প্রতিটি ইনস্ট্যান্সের নিজস্ব আলাদা, বিচ্ছিন্ন কাউন্টার।"""
    key = (instance_id, client_id)
    timestamps = local_store.setdefault(key, [])
    cutoff = now - window_size
    local_store[key] = [t for t in timestamps if t > cutoff]
    if len(local_store[key]) < limit:
        local_store[key].append(now)
        return True
    return False

LIMIT = 5
WINDOW = 10  # সময় ইউনিট, উইন্ডোর মধ্যে সব রিকোয়েস্ট থাকবে

# "user-42"-এর ৮টি রিকোয়েস্ট, লোড ব্যালেন্সার দিয়ে ৩টি ইনস্ট্যান্স জুড়ে রাউন্ড-রবিন রাউট হচ্ছে
requests = [(t, ["server-A", "server-B", "server-C"][t % 3]) for t in range(8)]

print("=== শেয়ার্ড স্টোর (সঠিক — Redis-স্টাইল, একটাই গ্লোবাল গণনা) ===")
shared_store = {}
for now, instance in requests:
    allowed = sliding_window_allow(shared_store, "user-42", now, WINDOW, LIMIT)
    print(f"t={now}  {instance:<10} -> {'ALLOWED' if allowed else 'DENIED '}")

print("\n=== পার-ইনস্ট্যান্স লোকাল কাউন্টার (ভুল — প্রতিটি ইনস্ট্যান্স আলাদাভাবে গোনে) ===")
local_store = {}
for now, instance in requests:
    allowed = local_allow(local_store, instance, "user-42", now, WINDOW, LIMIT)
    print(f"t={now}  {instance:<10} -> {'ALLOWED' if allowed else 'DENIED '}")

    
শেয়ার্ড স্টোরে limit=৫ ধরে প্রথম ৫টি রিকোয়েস্ট (t=০-৪) ALLOWED, বাকি ৩টি (t=৫,৬,৭) DENIED — নির্বিশেষে কোন সার্ভার রিকোয়েস্টটি হ্যান্ডল করেছে। কিন্তু পার-ইনস্ট্যান্স লোকাল কাউন্টারে প্রতিটি সার্ভার (A, B, C) নিজে নিজে মাত্র ৩টি বা ২টি রিকোয়েস্ট দেখে (৫-এর নিচে) — তাই সবগুলো ৮টি রিকোয়েস্টই ALLOWED হয়ে যায়, যদিও উদ্দেশ্য ছিল সর্বোচ্চ ৫টি। এটাই দেখায় কেন লোকাল কাউন্টার একটি লোড-ব্যালেন্সড পরিবেশে "নীরবে" ভেঙে পড়ে — কোনো এরর নেই, শুধু ভুল ফলাফল।
মূল কথা · Key takeaway

একটি রেট লিমিটিং অ্যালগরিদম (L35) সঠিক হলেও, তার স্টেট কোথায় থাকে সেটাই আসল সিস্টেম-ডিজাইন প্রশ্ন একাধিক সার্ভারের পরিবেশে। শেয়ার্ড স্টোর সঠিকতা নিশ্চিত করে কিন্তু একটি নতুন নেটওয়ার্ক-হপ ও নতুন ক্রিটিক্যাল ডিপেন্ডেন্সি যোগ করে — fail-open নীতি সেই ঝুঁকির একটি বাস্তবসম্মত ব্যবস্থাপনা।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ কেন পার-ইনস্ট্যান্স লোকাল রেট লিমিটিং একটি লোড-ব্যালেন্সড, মাল্টি-সার্ভার পরিবেশে "নীরবে" ভেঙে পড়ে?

কারণ কোনো এরর হয় না — প্রতিটি সার্ভার নিজের হিসেবে সঠিকভাবেই কাজ করে, শুধু প্রতিটি আলাদা করে গোনে। ক্লায়েন্টের রিকোয়েস্ট লোড ব্যালেন্সার (L11) দিয়ে বিভিন্ন ইনস্ট্যান্সে ভাগ হয়ে গেলে, প্রতিটি ইনস্ট্যান্স তার নিজের সীমার মধ্যেই থাকে, কিন্তু সামগ্রিকভাবে ক্লায়েন্ট তার উদ্দিষ্ট লিমিটের চেয়ে বহুগুণ বেশি রিকোয়েস্ট পার করিয়ে ফেলতে পারে — এটি ধরা পড়ে না যতক্ষণ না কেউ আসল, গ্লোবাল সংখ্যা পরীক্ষা করে দেখে।

প্র ০২ কেন রেট লিমিটারকে API গেটওয়েতে (L26) বসানো হয়, প্রতিটি ব্যাকএন্ড মাইক্রোসার্ভিসে আলাদা করে বসানোর বদলে?

একটি একক জায়গায় বসালে সব ইনকামিং ট্রাফিকের জন্য সামঞ্জস্যপূর্ণ নীতি একবারই প্রয়োগ হয়, প্রতিটি সার্ভিসকে একই লজিক ডুপ্লিকেট করতে হয় না (কোড রিপিটেশন ও অসামঞ্জস্যতার ঝুঁকি কমে), এবং সবচেয়ে গুরুত্বপূর্ণ — অতিরিক্ত ট্রাফিক ব্যাকএন্ড সার্ভিসগুলোতে পৌঁছানোর আগেই প্রত্যাখ্যাত হয়, ফলে তাদের রিসোর্স (CPU, DB কানেকশন) বাঁচে।

প্র ০৩ শেয়ার্ড রেট-লিমিটার স্টোর নিজেই ডাউন হয়ে গেলে, কেন বেশিরভাগ সিস্টেম fail-open (ট্রাফিক পাস করা) বেছে নেয়, fail-closed (সব ব্লক) না করে?

কারণ রেট লিমিটারের কাজ হলো অপব্যবহার/ওভারলোড থেকে সুরক্ষা দেওয়া — এটি নিজে একটি প্রধান ফিচার নয়। যদি স্টোর ডাউন হওয়ার কারণে পুরো API বন্ধ হয়ে যায় (fail-closed), তাহলে একটি ছোট, সেকেন্ডারি কম্পোনেন্টের ব্যর্থতা পুরো সার্ভিসের ব্যর্থতায় রূপ নেয় — যা সাধারণত সাময়িকভাবে রেট-লিমিট ছাড়াই ট্রাফিক চলতে দেওয়ার চেয়ে অনেক বেশি ক্ষতিকর। fail-open এই ট্রেড-অফকেই স্বীকার করে।

অনুশীলন

  1. হিসাব করুন: একটি সিস্টেমে ৩টি সার্ভার ইনস্ট্যান্স আছে, প্রতিটির লোকাল লিমিট প্রতি মিনিটে ১০০ রিকোয়েস্ট। যদি একটি ক্লায়েন্টের রিকোয়েস্ট নিখুঁতভাবে ৩টি ইনস্ট্যান্স জুড়ে সমান ভাগ হয়, লোকাল কাউন্টার ব্যবহার করলে ওই ক্লায়েন্ট সর্বোচ্চ কত রিকোয়েস্ট/মিনিট পার করাতে পারবে, উদ্দিষ্ট ১০০-এর বদলে?

    প্রতিটি ইনস্ট্যান্স স্বাধীনভাবে ১০০ পর্যন্ত অনুমতি দেবে, তাই সর্বোচ্চ = ৩ × ১০০ = ৩০০ রিকোয়েস্ট/মিনিট — উদ্দিষ্ট ১০০-এর চেয়ে ৩ গুণ বেশি। এটাই N × instance_count সমস্যা, যা শুধু একটি শেয়ার্ড স্টোর দিয়েই সমাধান করা যায়।

  2. চিন্তা করুন: শেয়ার্ড স্টোর চেক করতে গড়ে ২ms অতিরিক্ত সময় লাগে, এবং লিমিটার ছাড়া একটি টিপিক্যাল রিকোয়েস্টের গড় লেটেন্সি ২০ms। এই ২ms কি একটি গ্রহণযোগ্য ট্রেড-অফ? কখন এটি অগ্রহণযোগ্য হয়ে উঠতে পারে?

    ২ms / ২০ms = ১০% লেটেন্সি বৃদ্ধি — বেশিরভাগ সিস্টেমের জন্য সঠিকতার বিনিময়ে গ্রহণযোগ্য। তবে যদি মূল সিস্টেমটি নিজেই অত্যন্ত লো-লেটেন্সি হয় (যেমন ৫ms-এর নিচের একটি রিয়েল-টাইম সিস্টেম), তাহলে ২ms একটি বড় আপেক্ষিক (৪০%+) বৃদ্ধি হয়ে যায় — সেক্ষেত্রে ইন-মেমরি লোকাল ক্যাশিং সহ একটি হাইব্রিড পদ্ধতি (approximate local counting + periodic sync) বিবেচনা করা যেতে পারে, নির্ভুলতার সামান্য আপোষে লেটেন্সি বাঁচাতে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
কেস স্টাডি: URL শর্টনার ডিজাইন করা