رفتن به متن راهنما
همه‌ی راهنماها

محدودیت نرخ

الگوریتم محدودیت نرخ را انتخاب کنید، بفهمید چرا تلاش مجدد مثل تلاش اول هزینه دارد و رد شدنی را که با صبر رفع می‌شود از رد شدنی که هرگز رفع نمی‌شود تشخیص دهید.

برنامه Reqtide PHP 0.1.0آخرین بررسی زمان خواندن 6 دقیقه

در این صفحه

این محدودیت از یک پردازش PHP محافظت می‌کند. ده worker هرکدام سهمیهٔ کامل را دارند، و در PHP-FPM پایپ‌لاینی که داخل یک درخواست ساخته شود، هر درخواست را با سهمیهٔ کامل شروع می‌کند. محدودیت‌های Reqtide را ببینید.

بلوک‌های کد از examples/09-choosing-policies.php (opens in a new tab) و examples/11-rate-limit-charges.php (opens in a new tab) گرفته شده‌اند. اسکریپتی آن‌ها را اجرا می‌کند و خروجی را با خروجی ثبت‌شده مقایسه می‌کند. عددها فقط رفتار را قابل‌دیدن می‌کنند و پیشنهاد نیستند.

سه الگوریتم، یک فراخوانی builder

متد rateLimit(algorithm, cost) الگوریتم خود را از یکی از سه متد کارخانه‌ای (factory) می‌گیرد. قطعه کد زیر برای هر الگوریتم دقیقاً جای یک تماس در ساعت می‌گذارد و نشان می‌دهد تماس دوم رد می‌شود. شروع آن این تنظیم مشترک است:

PHP
/** Every pipeline below starts from the same two required options. */
$base = static fn(): PipelineBuilder => PipelineBuilder::profile('choosing', 'v1')
    ->partition('shop', 'prod', 'catalog-api')
    ->attemptTimeout(Duration::seconds(2), AttemptTimeoutRequirement::Cooperative);

$ok = static fn(): string => 'ok';
$context = static fn(CompiledPipeline $pipeline): ExecutionContext => $pipeline->newContext(Duration::seconds(30));
PHP
echo "-- 1. Rate limit: three algorithms, same builder call\n";
$algorithms = [
    'token bucket' => PipelineBuilder::tokenBucket(1, 1, Duration::seconds(3600)),
    'fixed window' => PipelineBuilder::fixedWindow(1, Duration::seconds(3600), FixedWindowAnchor::FirstHit),
    'sliding window' => PipelineBuilder::slidingWindow(1, Duration::seconds(3600)),
];
foreach ($algorithms as $name => $algorithm) {
    $pipeline = $base()->rateLimit($algorithm)->compileInMemory(ExceptionClassifier::create());
    $pipeline->execute($ok, $context($pipeline));
    try {
        $pipeline->execute($ok, $context($pipeline));
    } catch (AdmissionRejectedException | AdmissionDeferredException $e) {
        echo "{$name}: second call refused with {$e->reason()->value}\n";
    }
}
Text
-- 1. Rate limit: three algorithms, same builder call
token bucket: second call refused with rate.exhausted
fixed window: second call refused with rate.exhausted
sliding window: second call refused with rate.exhausted
الگوریتمرفتار
متد tokenBucket(capacity, refillTokens, interval)تا وقتی توکن باقی باشد می‌پذیرد. توکن‌ها به‌طور پیوسته و با حساب ممیز ثابت صحیح پر می‌شوند. وقتی سطل پر است، مازاد دور ریخته می‌شود.
متد fixedWindow(capacity, window, anchor)در هر پنجره می‌شمارد. پنجره‌های هم‌تراز از مضرب‌های طول پنجره روی epoch شروع می‌شوند. پنجره‌های FirstHit از اولین تماس شروع می‌شوند. پنجرهٔ جدید دقیقاً در لحظهٔ پایان پنجرهٔ قبل شروع می‌شود.
متد slidingWindow(capacity, window)یک شمارندهٔ لغزان تقریبی است. هیچ‌وقت زودتر از موعد نمی‌پذیرد، اما ممکن است دیرتر از یک لاگ لغزان دقیق بپذیرد.
سه الگوریتم محدودیت نرخ.

هر سه الگوریتم از حساب اعداد صحیح استفاده می‌کنند و زمان یکنواخت (monotonic) است. ساعتی که به عقب برود، هیچ‌وقت سهمیه را دوباره پر نمی‌کند.

انتخاب بین آن‌ها به چیزی بستگی دارد که ارائه‌دهنده اعمال می‌کند. اگر ارائه‌دهنده مستند کرده که از سطل استفاده می‌کند، سطل توکن بگذارید. اگر شمارنده را در زمان‌های ثابت صفر می‌کند، پنجرهٔ ثابت با لنگر (anchor) مناسب همان رفتار را دارد. وقتی قاعدهٔ ارائه‌دهنده را نمی‌دانید، پنجرهٔ لغزان انتخاب محتاطانه‌ای است، هرچند گاهی دیرتر از لازم می‌پذیرد.

معنای هزینه

هر ارسال به اندازهٔ cost واحد هزینه دارد و پیش‌فرض آن 1 است. تلاش اول و هر تلاش مجدد یکسان حساب می‌شوند. هزینه هیچ‌وقت برگردانده نمی‌شود، حتی برای تماسی که پس از پذیرش و پیش از ارسال لغو شود. پس سهمیهٔ مصرف‌شده با تعداد درخواست‌های ارسال‌شده برابر نیست.

این موضوع در اندازه‌گیری سهمیه مهم است. سطلی با 3 واحد و سیاست تلاش مجدد با حداکثر 3 تلاش، با یک عملیات که دو بار تلاش مجدد کند خالی می‌شود:

PHP
echo "-- 1. A retry is charged like a first attempt\n";
// A bucket of 3 units that does not refill within this script. The call may make up to 3 attempts.
$pipeline = PipelineBuilder::profile('rate-charges', 'v1')
    ->partition('shop', 'prod', 'catalog-api')
    ->attemptTimeout(Duration::seconds(2), AttemptTimeoutRequirement::Cooperative)
    ->retry(3, BackoffMode::Constant, Duration::milliseconds(5), Duration::milliseconds(5), JitterMode::None)
    ->retryBudget(10, 10, Duration::seconds(60), Duration::seconds(10))
    ->rateLimit(PipelineBuilder::tokenBucket(3, 1, Duration::seconds(3600)))
    ->compileInMemory(ExceptionClassifier::create()->retryOn(Hiccup::class));
$safety = static fn(): OperationSafety => OperationSafety::replayableRead(
    ReplaySafetyContract::forReadOnlyCallable('list-products', Duration::seconds(3600)),
);

$flaky = static function (AttemptContext $attempt): string {
    if ($attempt->dispatchedAttemptNumber < 3) {
        throw new Hiccup('try again');
    }

    return 'ok';
};
echo 'first operation: ', $pipeline->execute($flaky, $pipeline->newContext(Duration::seconds(30), $safety())), " (3 dispatches)\n";

$completion = $pipeline->executeDetailed($flaky, $pipeline->newContext(Duration::seconds(30), $safety()));
echo 'second operation: ', $completion->disposition->value, ', dispatched ', $completion->summary->dispatched, ', reason ', $completion->problem()?->code->value, "\n";
Text
-- 1. A retry is charged like a first attempt
first operation: ok (3 dispatches)
second operation: rejected, dispatched 0, reason rate.exhausted

اندازهٔ سهمیه را از حاصل‌ضرب ترافیک مورد انتظار در تعداد تلاش‌های مجاز به دست بیاورید، نه از تعداد عملیات‌ها.

سه نوع رد شدن: رد، تعویق، ناممکن

رد شدن به شکل یکی از سه استثنای نوع‌دار به شما می‌رسد. callable شما اجرا نشده است و این رد شدن شکست وابستگی حساب نمی‌شود.

چه دریافت می‌کنیدمعنا
استثنای AdmissionRejectedExceptionسهمیه تمام شده است. بسته به الگوریتم، ممکن است زمان انتظار معلوم باشد یا نامعلوم.
استثنای AdmissionDeferredExceptionهمراه رد شدن، یک انتظار مثبت هم اعلام می‌شود.
استثنای ImpossibleAdmissionExceptionدرخواست هیچ‌وقت نمی‌تواند موفق شود، مثلاً چون هزینه‌اش از ظرفیت بیشتر است. صبر کردن کمکی نمی‌کند.
سه استثنای رد شدن.

حالت آخر به پیکربندی مربوط است و به ترافیک ربطی ندارد:

PHP
echo "-- 2. A cost above the capacity can never be admitted\n";
$tooCostly = PipelineBuilder::profile('rate-charges', 'v1')
    ->partition('shop', 'prod', 'catalog-api')
    ->attemptTimeout(Duration::seconds(2), AttemptTimeoutRequirement::Cooperative)
    ->rateLimit(PipelineBuilder::tokenBucket(2, 2, Duration::seconds(1)), cost: 3)
    ->compileInMemory(ExceptionClassifier::create());
try {
    $tooCostly->execute(static fn(): string => 'never runs', $tooCostly->newContext(Duration::seconds(30)));
} catch (ImpossibleAdmissionException $e) {
    echo get_class($e), ': ', $e->reason()->value, "\n";
} catch (AdmissionRejectedException | AdmissionDeferredException $e) {
    echo 'unexpected: ', get_class($e), "\n";
    exit(1);
}
Text
-- 2. A cost above the capacity can never be admitted
Enaxon\Reqtide\Exception\ImpossibleAdmissionException: rate.cost_exceeds_capacity

کد registry را از $e->reason() و، در صورت وجود انتظار، زمان آن را از $e->decision->retryAfter بخوانید. مدیریت خطا توضیح می‌دهد با هر کدام چه باید کرد.

راهکار جایگزین برای رد شدن‌ها یک انتخاب است

به‌طور پیش‌فرض راهکار جایگزین (Fallback) فقط جای نتیجهٔ کار callable شما را می‌گیرد، پس رد شدن ناشی از محدودیت نرخ به خود شما می‌رسد. می‌توانید تعیین کنید که fallback جای رد شدن‌ها را هم بگیرد. اگر fallback رد شدن‌ها را بپوشاند، محدود شدن شما هم دیده نمی‌شود. راهکار جایگزین را ببینید.

آزمون یک محدودیت

آزمون محدودیت نرخی که در یک دقیقه دوباره پر می‌شود، نباید یک دقیقه صبر کند. به‌جای آن ساعت مجازی را جلو ببرید. آزمون با ساعت مجازی را ببینید.