پرش به مطلب اصلی

SpeedLimit

SpeedLimit یک tunnel عبوری است که نرخ عبور بایت‌های payload را با token bucket محدود می‌کند. این node داده protocol را تغییر نمی‌دهد، header اضافه یا حذف نمی‌کند و destination context را تغییر نمی‌دهد؛ فقط تعیین می‌کند payload اکنون عبور کند، منتظر بماند یا کنار گذاشته شود.

هر دو جهت از همان bucket استفاده می‌کنند. برای نمونه، اگر سرعت یک bucket برابر 1 MB/s باشد، مجموع upload و download آن bucket همین ظرفیت را با هم تقسیم می‌کنند.

جایگاه رایج​

برای stream:

TcpListener -> SpeedLimit -> TcpConnector
TcpListener -> TlsServer -> SpeedLimit -> TcpConnector

برای packet یا datagram:

UdpListener -> SpeedLimit -> UdpConnector
TunDevice -> SpeedLimit -> UdpStatelessSocket

برای ترافیک stream معمولاً حالت pause مناسب‌تر است تا داده اضافه منتظر بماند. برای packet و datagram معمولاً drop بهتر است، چون packet قدیمی اغلب از packet ازدست‌رفته هم کم‌ارزش‌تر است.

نمونه stream​

در این نمونه هر connection یک bucket جدا با سرعت 256 KiB/s دارد و داده اضافی تا پر شدن دوباره token‌ها در صف می‌ماند.

{
"name": "speed-limit",
"type": "SpeedLimit",
"settings": {
"kilo-bytes-per-sec": 256,
"limit-mode": "per-line",
"work-mode": "pause"
},
"next": "next-node"
}

نمونه limit مشترک​

در این نمونه همه line‌های عبوری از این instance یک bucket مشترک 8 MiB/s دارند.

{
"name": "global-speed-limit",
"type": "SpeedLimit",
"settings": {
"mega-bytes-per-sec": 8,
"limit-mode": "all-lines",
"token-recharge-rate": 20,
"work-mode": "pause"
},
"next": "next-node"
}

نمونه packet drop​

در این نمونه هر worker یک bucket مشترک 500000 بایت بر ثانیه دارد و chunk‌هایی که token کافی برایشان موجود نیست کنار گذاشته می‌شوند.

{
"name": "packet-speed-limit",
"type": "SpeedLimit",
"settings": {
"bytes-per-sec": 500000,
"limit-mode": "per-worker",
"work-mode": "drop"
},
"next": "next-node"
}

فیلدهای اجباری​

settings الزامی است و باید objectی غیرخالی باشد.

دقیقاً یکی از فیلدهای سرعت باید تنظیم شود:

فیلدمعنی
bytes-per-secبایت بر ثانیه.
kilo-bytes-per-secکیبی‌بایت بر ثانیه، ضرب در 1024.
mega-bytes-per-secمبی‌بایت بر ثانیه، ضرب در 1024 * 1024.

مقدار انتخاب‌شده باید عددی بزرگ‌تر از 0 باشد. تنظیم هم‌زمان بیش از یک فیلد سرعت باعث شکست ساخت node می‌شود.

این فیلدها هم اجباری هستند:

فیلدمقدارهای مجاز
limit-modeper-line, per-connection, all-lines, all-connections, per-worker
work-modepause, drop

در parser فعلی این string‌ها به حروف کوچک و بزرگ حساس‌اند.

فیلد اختیاری​

فیلدپیش‌فرضتوضیح
token-recharge-rate10فاصله refill شدن token‌ها به میلی‌ثانیه. باید بزرگ‌تر از 0 باشد.

token-recharge-rate مقدار پهنای‌باند را تعیین نمی‌کند؛ فقط فاصله زمانی افزودن token به bucket را مشخص می‌سازد.

برای مثال، با این تنظیم:

  • bytes-per-sec: 1000
  • token-recharge-rate: 10

bucket تقریباً هر 10 ms به اندازه 10 byte token دریافت می‌کند. interval کوچک‌تر معمولاً خروجی روان‌تری می‌دهد و interval بزرگ‌تر می‌تواند ترافیک را burstی‌تر کند.

Limit mode‌ها​

modeرفتار
per-line / per-connectionهر line در WaterWall bucket خودش را دارد. دو line هم‌زمان با 1 MB/s می‌توانند در مجموع حدود 2 MB/s مصرف کنند.
all-lines / all-connectionsهمه line‌های این tunnel instance یک bucket سراسری و atomic میان worker‌ها دارند. با 1 MB/s، کل node همین ظرفیت را میان همه line‌ها تقسیم می‌کند.
per-workerهر worker bucket مشترک خودش را دارد. با 4 worker و 1 MB/s، سقف عملی کل می‌تواند حدود 4 MB/s شود.

هر bucket در ابتدا پر است و ظرفیتی برابر حدود یک ثانیه از نرخ تنظیم‌شده دارد. در نتیجه ممکن است در شروع کار، نزدیک به یک ثانیه ترافیک را به شکل burst عبور دهد و سپس روی میانگین تعیین‌شده پایدار شود.

Work mode‌ها​

pause​

pause برای ترافیک ترتیبی مانند TCP طراحی شده است.

وقتی payload در bucket جا نشود:

  1. buffer درون SpeedLimit در صف قرار می‌گیرد.
  2. سمت source با callback‌های Pause WaterWall متوقف می‌شود.
  3. یک timer روی worker منتظر refill شدن token‌ها می‌ماند.
  4. داده صف‌شده به همان ترتیب ارسال می‌شود.
  5. با خالی شدن صف، ارسال در سمتی که به‌صورت local متوقف شده بود از سر گرفته می‌شود.

اگر buffer صف‌شده از token موجود بزرگ‌تر باشد، SpeedLimit می‌تواند بخش ابتدایی آن را بفرستد و باقی را تا tick بعدی در صف نگه دارد. با این کار ترتیب بایت‌ها حفظ و نرخ میانگین کنترل می‌شود.

حالت pause buffer‌ها را در حافظه نگه می‌دارد؛ بنابراین limit بسیار پایین، peer کند یا نرخ ورودی بالا می‌تواند مصرف حافظه و latency را افزایش دهد.

drop​

drop برای ترافیک packet و datagram طراحی شده است.

برای هر payload chunk:

  • اگر bucket برای کل chunk به اندازه کافی token داشته باشد، chunk فرستاده می‌شود
  • اگر token کافی نباشد، کل chunk کنار گذاشته می‌شود

در حالت drop، payload نه تکه‌تکه می‌شود و نه برای بعد در صف می‌ماند. این رفتار معمولاً برای UDP و packet chain مناسب‌تر است، زیرا datagram‌های قدیمی اغلب دیگر کاربردی ندارند.

جهت‌ها و backpressure​

محدودسازی payload و backpressure در هر دو جهت متقارن است:

  • upstream و downstream از همان منطق bucket انتخاب‌شده استفاده می‌کنند
  • داده صف‌شده در upstream، سمت قبلی را متوقف می‌کند
  • داده صف‌شده در downstream، سمت بعدی را متوقف می‌کند
  • حالت Pause و Resume خارجی به‌خاطر سپرده می‌شود تا SpeedLimit سمتی را که هنوز از بیرون pause است به اشتباه resume نکند

در نتیجه حالت pause با backpressure معمول WaterWall هماهنگ می‌شود و صرفاً درون callback منتظر نمی‌ماند.

پشتیبانی از splice و سقف صف​

هر جهت یک FIFO جدا دارد که حداکثر 16 MiB payload، برابر 32 MiB charge ظرفیت buffer و 1,024 buffer را نگه می‌دارد. buffer خالی نیز charge و یک entry مصرف می‌کند. رد شدن admission، اتصال TCP مربوط را می‌بندد و هر دو صف را آزاد می‌کند؛ بایت‌های TCP پذیرفته‌شده بدون اطلاع حذف نمی‌شوند.

Pause از consumer تخلیه timer همان جهت را متوقف می‌کند. داده در صف باقی می‌ماند و timer هنگام Pause مرتباً polling نمی‌کند. Resume تخلیه را دوباره زمان‌بندی می‌کند، اما source فقط پس از رفع هم‌زمان فشار بیرونی و hold صف محلی Resume دریافت می‌کند. اگر ساختن یا فعال‌سازی دوبارهٔ timer رد شود، هر دو صف آزاد می‌شوند و اتصال مربوط از طریق owner بسته می‌شود. بسته شدن line از داخل callback نیز به‌صورت ایمن مدیریت می‌شود.

این node فلگ kNodeFlagSupportsSplice دارد. عبور کامل buffer و نگه‌داری در صف، pipe خصوصی را حفظ می‌کنند. grant جزئی در حالت pause با sbufMoveRangeTo() دقیقاً همان تعداد بایت را به pipe مستقل منتقل می‌کند؛ اگر ایجاد pipe یا انتقال کامل ممکن نباشد، خروجی به buffer معمولی تبدیل می‌شود. باقی‌مانده قدیمی پیش از ورودی تودرتو قرار می‌گیرد. حساب token شامل prefix و body داخل pipe است. حالت drop بدون خواندن بایت‌های payload درباره کل buffer تصمیم می‌گیرد.

splice به پشتیبانی تمام node‌های chain نهایی وابسته است. packet chain‌ها و platform‌های فاقد splice همچنان از buffer معمولی استفاده می‌کنند.

نکته packet-line​

packet line‌های WaterWall، line‌های کمکی و محلی هر worker هستند که برای packet‌های زیادی دوباره به کار می‌روند. در packet chain، per-line یعنی bucket متصل به packet line پایدار همان worker، نه یک bucket واقعی برای هر flow یا client. اگر limit بر اساس worker می‌خواهید، per-worker را صریح انتخاب کنید. اگر یک cap مشترک بین همه worker‌ها می‌خواهید، all-lines را انتخاب کنید.

برای chain‌های packet یا UDP-style معمولاً work-mode: "drop" انتخاب بهتری است، مگر اینکه عمداً delayed packet delivery و backpressure بخواهید.

رفتار Finish​

line state فقط هنگام Init در جهت upstream مقداردهی اولیه می‌شود. دریافت Init در جهت downstream ممنوع است و با abortProgramNow(1) فرایند را خاتمه می‌دهد. پس از مقداردهی اولیه در upstream، دریافت payload در downstream همچنان پشتیبانی می‌شود.

این node را بین Bridge و ReverseClient قرار ندهید؛ وقتی این دو node در چنین توپولوژی‌ای به هم متصل هستند، باید مستقیماً کنار یکدیگر باشند.

هنگام Finish، node state محلی را از بین می‌برد، timer‌های تخلیه صف را لغو می‌کند، buffer‌های صف‌شده را آزاد می‌کند و سپس Finish WaterWall را در همان جهت می‌فرستد.

این tunnel چیزی به ابتدای payload اضافه نمی‌کند؛ بنابراین required_padding_left برابر 0 است.

اشتباه‌های رایج​

  • بیشتر از یک فیلد سرعت تنظیم نکنید.
  • برای packet chain‌ها از pause استفاده نکنید مگر اینکه packet‌های تاخیردار قابل قبول باشند.
  • از per-worker انتظار یک سقف مشترک سراسری نداشته باشید؛ سقف عملی با تعداد worker‌ها افزایش می‌یابد.
  • در packet chain‌ها per-line را per-client یا per-flow فرض نکنید.
  • token-recharge-rate را بسیار بزرگ نگذارید، مگر اینکه خروجی burstی برایتان قابل قبول باشد.

متادیتای node​

متادیتای برگرفته از کد منبع:

ویژگیمقدار
node flagskNodeFlagSupportsSplice
can_have_prevtrue
can_have_nexttrue
layer_groupkNodeLayerAnything
layer_group_prev_nodekNodeLayerSameAsNext
layer_group_next_nodekNodeLayerSameAsPrev
required_padding_left0 بایت