System Design

تسلط بر محدودیت نرخ برای طراحی سیستم‌های مقیاس‌پذیر

در منظره مدرن سیستم‌های توزیع‌شده، محافظت از زیرساخت شما در برابر سوءاستفاده و اطمینان از استفاده عادلانه بین مشتریان قانونی حیاتی است. محدودیت نرخ تنها یک ویژگی امنیتی نیست؛ بلکه جزء حیاتی در دسترس بودن و پایداری است. این پست به بررسی الگوهای معماری، الگوریتم‌ها و استراتژی‌های پیاده‌سازی ضروری برای ساخت سیستم‌های محدودکننده نرخ مقاوم می‌پردازد که می‌توانند میلیون‌ها درخواست را بدون کاهش عملکرد مدیریت کنند.

الگوریتم‌های اصلی برای کنترل ترافیک

قبل از پیاده‌سازی یک راه‌حل، درک الگوریتم‌های زیربنایی که نحوه شمارش و محدود کردن درخواست‌ها را تعیین می‌کنند، حیاتی است. هر الگوریتم مبادلات متفاوتی از نظر پیچیدگی، دقت و تجربه کاربری ارائه می‌دهد. رایج‌ترین رویکرد، شمارنده پنجره ثابت است. این روش زمان را به بازه‌های ثابت (مثلاً یک دقیقه) تقسیم می‌کند و درخواست‌ها را در آن پنجره می‌شمارد. اگرچه پیاده‌سازی آن ساده است، اما از مشکل «مرز» رنج می‌برد، جایی که یک موج ترافیک درست قبل و بعد از بازنشانی پنجره می‌تواند ظرفیت مؤثر را دو برابر کند. برای رفع این مشکل، الگوریتم «پنجره لغزان» (Sliding Window Log) زمان‌مهر هر درخواست را ثبت می‌کند. سپس تعداد درخواست‌ها در N ثانیه گذشته را با فیلتر کردن لاگ محاسبه می‌کند. این روش دقیق‌تر است اما برای فیلتر کردن و حذف ورودی‌های قدیمی در مقیاس بزرگ، به حافظه و قدرت محاسباتی بیشتری نیاز دارد. برای سیستم‌های با عملکرد بالا، الگوریتم‌های «سطل توکن» (Token Bucket) یا «سطل نشتی» (Leaky Bucket) ترجیح داده می‌شوند. سطل توکن اجازه می‌دهد تا ترافیک به صورت موجی ارسال شود، با ذخیره حداکثر تعداد توکن. هر درخواست یک توکن مصرف می‌کند و توکن‌ها با نرخ ثابتی شارژ مجدد می‌شوند. این روش موج‌های ترافیک را هموار می‌کند در حالی که یک محدودیت نرخ متوسط را حفظ می‌کند و برای APIهایی که موج‌های گاه‌به‌گاه در آن‌ها قابل قبول است، ایده‌آل می‌باشد.

استراتژی‌های پیاده‌سازی توزیع‌شده

در یک برنامه تک‌موجودی (Monolithic)، شمارنده‌های حافظه داخلی کافی هستند. با این حال، در یک معماری میکروسرویس توزیع‌شده، شما به یک ذخیره‌سازی وضعیت متمرکز و سازگار برای هماهنگ کردن محدودیت‌های نرخ در چندین نمونه سرویس نیاز دارید. Redis به دلیل سرعت و عملیات اتمی خود، استاندارد صنعتی برای این منظور است. در زیر یک مثال عملی از پیاده‌سازی الگوریتم سطل توکن با استفاده از Redis و اسکریپت‌نویسی Lua برای اطمینان از اتمی بودن آورده شده است. اتمی بودن در اینجا حیاتی است تا از جلوگیری از شرایط رقابتی که در آن چندین درخواست ممکن است همزمان همان موجودی را ببینند، اطمینان حاصل شود.
-- Redis Lua Script for Token Bucket
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])

local function get_bucket()
    local data = redis.call('HMGET', key, 'tokens', 'last_refill')
    local tokens = tonumber(data[1])
    local last_refill = tonumber(data[2])
    
    if tokens == nil then
        return capacity, now
    end
    
    local elapsed = math.max(0, now - last_refill)
    local new_tokens = math.min(capacity, tokens + (elapsed * refill_rate))
    return new_tokens, now
end

local tokens, last_refill = get_bucket()

if tokens >= requested then
    tokens = tokens - requested
    redis.call('HMSET', key, 'tokens', tokens, 'last_refill', last_refill)
    redis.call('EXPIRE', key, 60)
    return 1
else
    return 0
end
این اسکریپت تعداد توکن‌های فعلی را بررسی می‌کند، شارژ مجدد را بر اساس زمان سپری شده محاسبه می‌کند و مقدار درخواست شده را کسر می‌کند. اگر توکن‌های کافی موجود باشد، 1 (مجاز) را برمی‌گرداند؛ در غیر این صورت، 0 (رد شده) را برمی‌گرداند. این رویکرد با اجرای منطق در سمت سرور در داخل نمونه Redis، تأخیر شبکه را به حداقل می‌رساند.

مدیریت موارد حاشیه‌ای و تجربه کاربری

پیاده‌سازی محدودیت نرخ تنها نبرد نیمه است؛ ارتباط محدودیت‌ها با مشتریان نیز به همان اندازه مهم است. وقتی درخواستی رد می‌شود، سرور باید کد وضعیت 429 Too Many Requests را برگرداند. نکته حیاتی این است که باید هدرهایی مانند `Retry-After` را برای اطلاع دادن به مشتری درباره مدت زمان انتظار، و همچنین `X-RateLimit-Limit`، `X-RateLimit-Remaining` و `X-RateLimit-Reset` را برای ایجاد شفافیت شامل کنید. علاوه بر این، به دانه‌بندی (Granularity) محدودیت‌های خود فکر کنید. آیا باید بر اساس آدرس IP، کلید API یا شناسه کاربر محدود کنید؟ محدود کردن بر اساس IP در صورتی که چندین کاربر از یک دروازه NAT مشترک استفاده می‌کنند، مستعد مثبت کاذب است. محدود کردن بر اساس کلید API یا شناسه کاربر دقیق‌تر است اما به سیستم‌های احراز هویت قوی نیاز دارد. اغلب یک رویکرد ترکیبی بهترین گزینه است: محدودیت‌های سخت‌گیرانه برای ترافیک ناشناس (بر اساس IP) و محدودیت‌های بالاتر و سخاوتمندانه‌تر برای کاربران احراز هویت شده.

نتیجه‌گیری

محدودیت نرخ یک جنبه اساسی در طراحی سیستم است که تخصیص منابع، امنیت و تجربه کاربری را متعادل می‌کند. با انتخاب الگوریتم مناسب برای الگوهای ترافیک خود و بهره‌گیری از ابزارهایی مانند Redis برای سازگاری توزیع‌شده، می‌توانید APIهای مقاومی بسازید که از زیرساخت بک‌اند شما محافظت می‌کنند. به یاد داشته باشید که بهترین محدودکننده نرخ، یکی است که شفاف، قابل پیکربندی و به طور یکپارچه در لایه مشاهده‌پذیری گسترده‌تر شما ادغام شده باشد، به شما امکان می‌دهد روندهای استفاده را نظارت کنید و سیاست‌ها را به صورت پویا با رشد برنامه خود تنظیم نمایید.
Share: