Redis-এ একটি Sliding Window Rate Limiter তৈরি করা
গত প্রান্তিকে দুইবার সকাল ৯টার আগেই আমাদের YouTube API কোটা শূন্য হয়ে গিয়েছিল। এর ফলে আমাদের অর্ধেক অঞ্চলের ট্রেন্ডিং ফিডগুলো stale হয়ে পড়েছিল।
সমস্যাটি ট্রাফিক বৃদ্ধির কারণে ছিল না। সমস্যাটি ছিল আমাদের rate limiter-এর কারণে।
আমরা একটি fixed-window counter ব্যবহার করতাম। এর ফলে যদি API কলের দুটি বড় বিস্ফোরণ (burst) উইন্ডোর সীমানার ঠিক কাছাকাছি সময়ে ঘটে, তবে সেগুলো সহজেই পার হয়ে যেত। ৮টি অঞ্চল বিশিষ্ট একটি গ্লোবাল পাইপলাইনের ক্ষেত্রে এই সীমানা ওভারল্যাপ প্রায়ই ঘটত। এটি আমাদের নির্ধারিত দৈনিক কোটাকে একটি বাজেট লিকের মতো করে তুলেছিল।
আমি fixed window-এর পরিবর্তে Redis sorted sets ব্যবহার করে একটি sliding window log ব্যবহার করেছি।
এটি যেভাবে কাজ করে:
- একটি টাইমস্ট্যাম্পসহ রিকোয়েস্ট রেকর্ড করতে
ZADDব্যবহার করুন। - উইন্ডোর বাইরে থাকা পুরনো এন্ট্রিগুলো মুছে ফেলতে
ZREMRANGEBYSCOREব্যবহার করুন। - উইন্ডোতে ঠিক কতটি রিকোয়েস্ট বাকি আছে তা গণনা করতে
ZCARDব্যবহার করুন। - অব্যবহৃত (idle) কীগুলো স্বয়ংক্রিয়ভাবে পরিষ্কার করতে
PEXPIREব্যবহার করুন।
এই পদ্ধতিটি অত্যন্ত নির্ভুল। এখানে কোনো সীমানা অতিক্রম করার ভয় নেই।
আমি এটি Redis-এ একটি Lua script ব্যবহার করে বাস্তবায়ন করেছি। এটি নিশ্চিত করে যে পুরো চেক এবং রেকর্ড প্রক্রিয়াটি atomic। আপনি যদি এই ধাপগুলো অ্যাপ্লিকেশন কোডে চালান, তবে race conditions-এর কারণে অতিরিক্ত রিকোয়েস্ট ভেতরে চলে আসতে পারে।
প্রোডাকশনের জন্য মূল প্রযুক্তিগত সিদ্ধান্তসমূহ:
- Redis TIME ব্যবহার করুন: আপনার অ্যাপ্লিকেশন সার্ভার থেকে টাইমস্ট্যাম্প ব্যবহার করবেন না। সার্ভারগুলোর মধ্যে সময়ের পার্থক্য (clock skew) উইন্ডোর নির্ভুলতা নষ্ট করে দেয়।
- Weighted costs: সব API কল সমান নয়। একটি সার্চ কল হয়তো ১০০ ইউনিট খরচ করতে পারে, যেখানে একটি ভিডিও লিস্টের খরচ মাত্র ১ ইউনিট। আমার স্ক্রিপ্টটি প্রতিটি কলের জন্য একাধিক মেম্বার ইনসার্ট করার মাধ্যমে এটি হ্যান্ডেল করে।
- Precise Retry-After: sorted set-এর সবচেয়ে পুরনো এন্ট্রিটি দেখে সিস্টেমটি নির্ভুলভাবে গণনা করতে পারে কখন ক্যাপাসিটি খালি হবে।
- Fail-safe logic: আমি EVALSHA-এর সাথে একটি EVAL fallback ব্যবহার করি। যদি রিস্টার্টের সময় Redis স্ক্রিপ্ট ক্যাশ মুছে যায়, তবে অ্যাপ্লিকেশনটি তা সুন্দরভাবে সামলে নেয়।
এর বিনিময়ে কিছুটা মেমরি খরচ হয়। প্রতিটি রিকোয়েস্ট প্রায় ১০০ বাইট জায়গা নেয়। ১০,০০০ ইউনিটের দৈনিক কোটার জন্য এটি মাত্র ১ এমবি (1 MB) মেমরি। বেশিরভাগ ক্ষেত্রে, এই নির্ভুলতার জন্য এই খরচটি সার্থক।
এই পরিবর্তনের পর থেকে আমাদের কোটা আর কখনও শেষ হয়নি। আমাদের জবগুলো এখন 403 error দেখানোর পরিবর্তে সুন্দরভাবে থেমে যায়।
যদি আপনার rate limiter ঘড়ির একটি নির্দিষ্ট সময় (round number) অনুযায়ী রিসেট হয়, তবে আপনার কোনো লিমিট নেই। বরং আপনার একটি লুপহোল (loophole) আছে।