MuxClient
MuxClient چند اتصال منطقی WaterWall را روی تعداد کمتری اتصال مشترک transport حمل میکند. بهجای ساخت یک اتصال کامل تا سمت مقابل برای هر line ورودی، یک یا چند parent line به سمت next میسازد یا دوباره به کار میگیرد و رویدادهای هر child را در قالب فریم داخلی MUX میفرستد. در سمت مقابل، MuxServer این child lineهای مستقل را بازسازی میکند.
MuxClient خودش listener یا connector نیست؛ node قبلی child lineها را میسازد و node بعدی transport مشترک parent را فراهم میکند.
جایگاه رایج
TCP ساده:
client side: TcpListener -> MuxClient -> TcpConnector
server side: TcpListener -> MuxServer -> TcpConnector
با protocol stack بیرونی:
client side: TcpListener -> MuxClient -> HttpClient -> TlsClient -> TcpConnector
server side: TcpListener -> TlsServer -> HttpServer -> MuxServer -> TcpConnector
از MUX زمانی استفاده کنید که تعداد زیادی اتصال منطقی کوتاه یا متوسط باید روی تعداد کمتری اتصال بیرونی transport عبور کنند.
این نود چه میکند؟
- lineهای child معمولی را از node قبلی میپذیرد.
- parent transport lineها را به سمت
nextمیسازد. - parent lineها را بر اساس حالت concurrency انتخابشده دوباره به کار میگیرد.
- به هر child یک connection id یا
cidاختصاص میدهد. - payload و رویدادهای pause، resume و Finish هر child را در فریم MUX encode میکند.
- فریمهای برگشتی
MuxServerرا به child مناسب هدایت میکند. - اگر سمت نوشتن child محلی pause باشد، داده رسیده از parent را در صف نگه میدارد.
- backpressure per-child را با
FlowPauseوFlowResumeframe بازتاب میدهد. - parent lineهای exhausted و بیکار را پس از بسته شدن آخرین child و تحویل خروجی صفشده میبندد.
MuxClient objectهای parent از نوع line_t را داخل خود میسازد و مسئول آزاد کردن آنهاست. child lineهایی را که node قبلی ساخته، آزاد نمیکند.
نمونه تنظیمها
حالت timer:
{
"name": "mux-client",
"type": "MuxClient",
"settings": {
"mode": "timer",
"connection-duration-ms": 30000
},
"next": "outbound-transport"
}
حالت counter:
{
"name": "mux-client",
"type": "MuxClient",
"settings": {
"mode": "counter",
"connection-capacity": 128
},
"next": "outbound-transport"
}
حالت pool ثابت parentها:
{
"name": "mux-client",
"type": "MuxClient",
"settings": {
"mode": "fixed-connections-count",
"per-worker-connections-count": 2
},
"next": "outbound-transport"
}
فیلدهای اجباری
فیلدهای سطح اصلی:
| فیلد | نوع | توضیح |
|---|---|---|
name | string | نام یکتای node در config. |
type | string | باید دقیقاً "MuxClient" باشد. |
settings | object | اجباری. باید mode و تنظیم ویژه همان حالت را داشته باشد. |
next | string | اجباری. node بعدی parent transport lineها را حمل میکند. |
فیلدهای اجباری داخل settings:
| گزینه | اجباری در | توضیح |
|---|---|---|
mode | همیشه | یکی از timer، counter یا fixed-connections-count. |
connection-duration-ms | mode: "timer" | مدت پذیرش child جدید روی هر parent، به میلیثانیه. باید بزرگتر از 60 باشد. |
connection-capacity | mode: "counter" | حداکثر تعداد cid باز شده روی یک parent. باید بزرگتر از 0 باشد. |
per-worker-connections-count | mode: "fixed-connections-count" | تعداد parent lineهای ثابت برای هر worker. باید بزرگتر از 0 باشد. |
تنظیمات اختیاری
| گزینه | نوع | پیشفرض | توضیح |
|---|---|---|---|
max-children | integer | 10000 | بیشترین child live همزمان روی یک parent. باید مثبت باشد و از connection-capacity تجمعی در حالت counter مستقل است. |
child-buffer-limit | integer | 134217728 | سقف charge منابع sbuf_t نگهداشتهشده برای یک child در حالت pause. برای ordinary هزینهٔ تخصیص و برای splice ظرفیت منطقی sbuf بهعلاوهٔ header و alignment شمرده میشود؛ padding یکبار حساب میشود. باید بزرگتر از 0 باشد؛ رسیدن به سقف child را با فریم Close میبندد. |
child-buffer-resume-threshold | integer | 524288 | Low-water mark برحسب بایت منطقی payload صفشده برای ارسال FlowResume پس از writableشدن child محلی. باید بزرگتر از 0 باشد و مقدار عددی آن به child-buffer-limit cap میشود. مقدار بزرگتر peer را زودتر Resume میکند، اما Hysteresis را کاهش میدهد و ممکن است چرخههای Pause/Resume را بیشتر کند. |
parent-buffer-limit | integer | 536870912 | سقف ظرفیت ورودی در حال تشکیل فریم بهعلاوهٔ صف childهای متصل روی هر parent. ابتدا ترکیب سودمند ورودی و سپس بستن بزرگترین صف با اولویت قدیمیترین child در تساوی انجام میشود؛ فشار حلنشده parent را میبندد. برای غیرفعال کردن سقف تجمیعی مقدار 0 را قرار دهید. |
detached-buffer-limit | integer | بر اساس ram-profile | سقف charge منابع نگهداشتهشده بهازای هر worker برای صف childهای مسدودشده پس از از دست رفتن parent. رسیدن به سقف فقط child تازه detachedشده را abort میکند. برای غیرفعال کردن این سقف تجمیعی مقدار 0 را قرار دهید. |
detached-child-limit | integer | بر اساس ram-profile | سقف تعداد بهازای هر worker برای childهای مسدودشدهای که پس از از دست رفتن parent نگه داشته میشوند. رسیدن به سقف فقط child مسدودشدهای را که بهتازگی detached شده است abort میکند. برای غیرفعال کردن این سقف تجمیعی مقدار 0 را قرار دهید. |
log-main-line-stats | boolean | false | اگر فعال باشد، parent lineها هر 5000 ms آمار best-effort را در لاگ مینویسند. مرگ منطقی parent اجرای عادی را suppress میکند و هنگام رسیدن runner صفشده/زمانبندیشده، task با LineDead settle میشود. quiescence worker کار معلق را فعالانه لغو میکند و هیچیک از این مسیرها آن را دوباره arm نمیکند. |
اگر این دو گزینه در تنظیمات node نوشته نشوند، مقدار پیشفرض آنها از misc.ram-profile سراسری انتخاب میشود:
| پروفایل RAM | سقف پیشفرض charge detached | سقف پیشفرض childهای detached |
|---|---|---|
S1 (minimal / ultralow) | 33554432 (32 MiB) | 4096 |
| S2 | 80740352 (77 MiB) | 5677 |
M1 (client) | 127926272 (122 MiB) | 7258 |
M2 (client-larger) | 174063616 (166 MiB) | 8838 |
| L1 | 221249536 (211 MiB) | 10419 |
L2 (server، پیشفرض سراسری) | 268435456 (256 MiB) | 12000 |
مقادیر بین کمینه و بیشینه روی شش رده مرتب پروفایل RAM بهصورت خطی درونیابی میشوند و سقف charge به نزدیکترین MiB گرد میشود. نوشتن صریح هر گزینه در تنظیمات node، فقط مقدار مشتقشده از پروفایل برای همان گزینه را جایگزین میکند.
child-buffer-resume-threshold طول منطقی payload را میشمارد.
FlowPause بر اساس وضعیت Pause محلی child ارسال میشود و به طول صف وابسته نیست. charge منابع برای سقفهای سخت صف و threshold خروجی parent
استفاده میشود؛ طول محتوا همچنان معیار صفهای معمولی و flow control است.
آمار اختیاری هر پنج ثانیه شامل parent-output-queued-bytes،
parent-output-queue-charge، parent-output-queue-items،
parent-transport-paused، parent-sources-throttled،
children-parent-write-paused و children-peer-flow-paused است.
تعداد محلی میتواند غیرصفر باشد در حالی که gate تجمیعی خاموش است.
parent-output-throttle-ms و parent-output-last-throttle-ms فقط مدت gate
تجمیعی را با ساعت monotonic ۶۴ بیتی گزارش میکنند. سه threshold مؤثر نیز
ثبت میشوند. parent-child-queue-charge و parent-input-queue-charge مربوط
به ذخیرهسازی ورودیاند، نه backlog خروجی.
مدل parent و child
MuxClient دو نقش line دارد:
| نقش | معنی |
|---|---|
| child line | اتصال منطقی WaterWall دریافتی از node قبلی. |
| parent line | اتصال transport مشترک که MuxClient به سمت next میسازد. |
هر child یک cid میگیرد. مقدار cid در محدوده همان parent line محلی است و در فریمهای MUX به کار میرود تا MuxServer بتواند payload و رویدادهای کنترلی را به stream منطقی درست نسبت دهد.
در حالت timer و counter، هر worker یک parent فعال و قابل استفاده دارد. childهای جدید تا exhausted شدن parent به آن متصل میشوند. اگر با رسیدن child بعدی count به max-children رسیده باشد، parent از selection بازنشسته میشود، برای childهای فعلی زنده میماند و پس از صفر شدن تعداد child و تحویل خروجی صف بسته میشود؛ child جدید parent دیگری میگیرد.
در حالت pool ثابت، هر worker به تعداد per-worker-connections-count جایگاه قابل انتخاب دارد. ابتدا parentهای غیرمشکوک که به max-children نرسیدهاند ترجیح داده میشوند؛ سپس انتخاب با هش rendezvous میان همان گروه انجام میشود. هر child جدید یک کلید مستقل در worker و هر parent یک شناسه ثابت در طول عمرش دارد. تعداد childها فقط سقف پذیرش است و امتیاز انتخاب نیست. اتصال childهای موجود تغییر نمیکند. اگر تمام گزینهها مشکوک باشند، انتخاب در همان گروه ادامه دارد؛ اگر همه پر یا در حال پایان باشند، child جدید Finish میگیرد و صف انتظار نامحدود ساخته نمیشود.
Ping/Pong و جایگزینی نرم parent
گزینهٔ boolean به نام keepalive بهصورت پیشفرض true است و در همهٔ modeها
اعمال میشود. false ارسال probe، پذیرش پاسخ و جایگزینی مبتنی بر سلامت را
غیرفعال میکند؛ انتخاب rendezvous در حالت ثابت باقی میماند.
ping-interval پیشفرض 15000 و tolerance-ms پیشفرض 45000 میلیثانیه دارد.
هر دو باید حتی در حالت غیرفعال عدد صحیح بین 1 و 2147483647 باشند.
جدول idle محلی هر worker، probe و مهلت پاسخ parentهای قابل انتخاب و established
را زمانبندی میکند، نه childها را. دقت بررسی حدود یک ثانیه است؛ probe آماده یا
timeout ممکن است حدود یک ثانیه دیر اجرا شود، حتی اگر مدت تنظیمشده کوتاهتر باشد.
نخستین Ping یک ping-interval پس از Est سررسید میشود و در اولین expiry مجاز
در آن زمان یا پس از آن ارسال میشود؛ این انتظار parent را مشکوک نمیکند و مانع
پذیرش child نیست. ارسالهای بعدی فاصلهٔ تنظیمشده را رعایت میکنند.
روی هر parent فقط یک probe در انتظار پاسخ است. Ping و Pong
بدنه ندارند و CID آنها یک token سیودوبیتی است. فقط Pong کامل با همان token
روی همان parent پاسخ محسوب میشود؛ Data، بخشی از header و token اشتباه کافی نیست.
اولین پاسخ معتبر پشتیبانی peer را تأیید میکند. قبل از آن، probe بیپاسخ دوباره امتحان میشود ولی parent مشکوک یا جایگزین نمیشود. بنابراین خاموشبودن پاسخگویی MuxServer باعث چرخش اتصال نمیشود؛ البته parentی که هرگز اولین پاسخ را نمیدهد از این راه بازیابی نمیشود. در سکوت شبانه و آپلود یکطرفه، خود MuxServer پاسخ میدهد و نیازی به پاسخ برنامهٔ کاربر نیست.
تا وقتی خروجی parent Pause است، pump فعال دارد یا صفش خالی نیست، probe جدید ارسال نمیشود و probe آماده حدود یک ثانیه بعد دوباره امتحان میشود. بررسی probe و مهلت پاسخ در Pause ترنسپورت یا FlowPause child ادامه دارد. زمان با ساعت monotonic همان worker اندازهگیری میشود؛ فقط تحویل واقعی Ping به node بعدی مهلت پاسخ را آغاز میکند. صفهای nodeهای بعدی، Pause محلی و انتقال شبکه از tolerance مصرف میکنند. tickهای ازدسترفته به یک probe آماده تبدیل میشوند، نه چند ارسال پشت سر هم. Pong از همان FIFO خروجی parent، سقف charge و gate مربوط به Pause عبور میکند.
پس از یکچهارم tolerance، با گردکردن رو به پایین و حداقل ۱ میلیثانیه
(پیشفرض 11250)، parent تأییدشده اولویت کمتری در حالت ثابت دارد. پس از کل
مدت، expiry مهلت پاسخ یا درخواست child جدید میتواند آن را بازنشسته کند، حتی در
Pause؛ جایگزین فقط هنگام درخواست child جدید ساخته میشود. Pong معتبر دیررس
parentی را که هنوز بازنشسته نشده سالم میکند. parent بازنشسته دوباره انتخاب
نمیشود، ولی childهای موجود روی آن میمانند و تا پایان معمول خود کار میکنند.
پس از خروج آخرین child بسته میشود و خروجی نهایی مسدود را بدون انتظار برای Resume
آزاد میکند. parent بیپاسخِ بدون child با پاکسازی معمول، حتی همراه خروجی مسدود،
بسته میشود.
سقف parentهای بازنشستهٔ keepalive در هر worker برابر per-worker-connections-count
در حالت ثابت و یک در حالتهای timer/counter است. خروجی نهایی در انتظار تخلیه
نیز در این سقف حساب میشود. با پرشدن سقف، parent دارای child همچنان قابل انتخاب
میماند تا ظرفیت آزاد شود و expiry حدود هر ثانیه بازنشستگی را دوباره امتحان میکند؛
parent خالی قابل جایگزینی است. چرخش معمول timer/counter مستقل باقی میماند و
عمر parent با ساعت monotonic worker مالک سنجیده میشود. callbackهای جدول idle
probe و expiry را اجرا میکنند؛ جایگزینی هنگام درخواست child جدید انجام میشود
و هیچ child فعالی بهاجبار قطع نمیشود. timeoutهای آداپتور و peer مستقلاند.
رفتار modeها
| mode | parent چه زمانی child جدید نمیپذیرد | مناسب برای |
|---|---|---|
timer | وقتی عمر parent از connection-duration-ms بیشتر شود | چرخش parent transport lineها در طول زمان. |
counter | وقتی cid به connection-capacity برسد | cap ساده روی streamهای منطقی بهازای هر parent. |
fixed-connections-count | عمر اتصال و counter تجمعی باعث چرخش نمیشوند؛ سقف child و سیاست Ping/Pong اعمال میشوند | pool قابل انتخاب با جایگزینی نرم محدود. |
parentِ exhausted بیدرنگ بسته نمیشود؛ فقط child تازه نمیپذیرد و childهای موجود تا پایان ادامه میدهند. اگر چنین parentای child نداشته باشد، تا تحویل آخرین خروجی و پایان ارسال فعال در فهرست مالکیت میماند، ولی دیگر برای child تازه انتخاب نمیشود. در حالت fixed، فقط parent بازنشستهنشده قابل انتخاب مجدد است. worker Stop همه parentهای تحت مالکیت، حتی parent بازنشسته بدون child، را بدون انتظار برای Resume میبندد و خروجی باقیمانده را آزاد میکند.
همچنین یک hard limit مطلق برای cid وجود دارد: وقتی parent به 4294967295 برسد، exhausted میشود.
dispatch پاسخ child با hash index و بهطور متوسط O(1) است. اگر MuxServer یک
Open جدید را با Close(cid) رد کند، فقط همان child از نوع borrowed Finish میشود و parent
و siblingهای دیگر ادامه میدهند.
باز شدن Stream
کد فعلی یک child را در upstream Init attach میکند:
- یک parent line موجود یا تازه روی همان worker انتخاب میکند.
- state مربوط به MUX را برای child مقداردهی اولیه میکند.
- child را به parent link میکند.
cidبعدی را اختصاص میدهد.- downstream
Estرا به node قبلی برای child گزارش میدهد.
فریم Open هنگام Init فرستاده نمیشود. با رسیدن نخستین payloadِ upstream، فریمهای Open و Data در یک buffer ارسال میشوند. اگر child پیش از فرستادن payload finish شود، MuxClient ابتدا Open و سپس Close میفرستد تا peer یک توالی کامل open/close ببیند.
فرمت frame
MuxClient و MuxServer از header هشتبایتی مشترک استفاده میکنند:
| offset | اندازه | معنی |
|---|---|---|
| 0 | ۳ بایت | طول بدون علامت payload، بهترتیب big-endian. |
| 3 | ۱ بایت | نوع فریم / flags. |
| 4 | ۴ بایت | شناسهٔ بدون علامت اتصال، بهترتیب big-endian. |
سقف قطعی و شامل payload برای همهٔ انواع فریم، ۱ MiB یعنی 1,048,576 بایت است؛ این مقدار به پروفایل RAM، اندازهٔ buffer یا ظرفیت درخواستی و واقعی pipe وابسته نیست. فریم حداکثری با header برابر 1,048,584 بایت است و Open پیش از آن، مجموع را به 1,048,592 میرساند. پس از دریافت هر هشت بایت header، طول بزرگتر بیدرنگ رد میشود؛ حتی برای CID یا نوع ناشناخته. فقط همان parent با مسیر عادی parent-loss بسته میشود. پس از header معتبر Data، هر بخش موجود از بدنه بهترتیب تحویل میشود و decoder منتظر کاملشدن فریم نمیماند. بدنهٔ Data برای CID ناشناخته یا بستهشده نیز تدریجی دور ریخته میشود. Data خالی بدون callback به child مصرف میشود. بدنهٔ سایر فریمها نیز تدریجی مصرف میشود، اما اثر کنترل فقط پس از پایان بدنه اعمال میشود. بایتهای موجود بدنه و کنترلهای کامل پیش از بررسی سقف 1,048,584 بایت برای ورودی ناقص باقیمانده پردازش میشوند. headerِ cacheشده در این سقف حساب میشود، اما بایتهای هنوز دریافتنشده حساب نمیشوند.
flagهای frame:
| flag | معنی |
|---|---|
0 | Open |
1 | Close |
2 | FlowPause |
3 | FlowResume |
4 | Data |
5 | Ping؛ بدون بدنه، CID برابر token probe |
6 | Pong؛ بدون بدنه، CID برابر همان token پاسخ |
1,048,576 بایت سقف payload در یک فریم Data است، نه سقف یک callback از نوع
Payload برای child. encoder یک payload بزرگتر را، با حفظ ترتیب دقیق بایتها، به
چند فریم پیاپی Data با حداکثر 1,048,576 بایت تقسیم میکند.
اندازهٔ تجمیعی encodeشده ــ شامل payload، header هشتبایتی هر فریم Data و فریم
اختیاری Open ــ بهصورت checked محاسبه میشود و باید در uint32_t جا شود. اگر
جا نشود، bufferِ ordinary ورودی recycle میشود و فقط همان child بسته میشود؛ هیچ فریم ناقصی
منتشر نمیشود. تخصیص buffer تجمیعی نیز از قرارداد fail-fast مربوط به shift buffer
پیروی میکند، بنابراین شکست واقعی allocator باعث خاتمه میشود و هرگز دادهٔ ناقص
forward نخواهد شد. batch بزرگ splice بهجای این مسیر، سقف کامل batch و بستن parent
مربوط را در صورت رد پذیرش به کار میبرد؛ جزئیات در بخش نگهداری splice آمده است.
جهت و رفتار Callback
| callback ورودی | رفتار |
|---|---|
upstream Init child | یک parent انتخاب یا میسازد، child را link میکند، cid اختصاص میدهد، Est را به node قبلی برای child گزارش میدهد. |
upstream Payload child | برای اولین payload Open + Data prepend میکند، در غیر این صورت Data prepend میکند، سپس به parent به سمت next میفرستد. |
upstream Pause / Resume child | FlowPause میفرستد / داده queue شده را flush میکند و در صورت نیاز FlowResume میفرستد. |
upstream Finish child | Close میفرستد، یا Open + Close اگر هنوز payload stream را باز نکرده؛ در صورت نیاز parent exhausted بیکار را میبندد. |
downstream Payload parent | فریمهای MUX از MuxServer را تجزیه میکند و Data، Close، FlowPause و FlowResume را به child مناسب میفرستد. |
downstream Pause / Resume parent | خروجی parent را بیدرنگ متوقف میکند / FIFO را تخلیه میکند؛ نویسندهٔ مسدود بهصورت محلی و همهٔ childها در آستانهٔ تجمیعی محدود میشوند. |
downstream Finish parent | state و line تحت مالکیت parent را بیدرنگ از بین میبرد، صفهای پذیرفتهشده childها را به drainهای مستقل از parent منتقل میکند و هر child را پس از خالی و قابلنوشتن شدن صف آن finish میکند. |
upstream Est | غیرفعال؛ رسیدن به این callback fatal است. |
downstream Init | غیرفعال؛ رسیدن به این callback fatal است. |
فریمهای ناشناخته، فریمهای مربوط به cid ناموجود و فریم Open ناخواسته از سمت سرور دور ریخته میشوند.
صف خروجی parent
هر parent یک FIFO مستقل و با تخصیص در اولین استفاده برای خروجی encodeشده دارد. تنظیمهای اختیاری زیر برحسب ظرفیت sbuf بهعلاوهٔ header buffer و alignment هستند. ظرفیت ordinary فیزیکی و ظرفیت splice منطقی است؛ padding یکبار شمرده میشود. ظرفیت pipe به آن اضافه نمیشود و این معیار، اندازهگیری حافظهٔ kernel یا RSS نیست.
| تنظیم | پیشفرض | معنی |
|---|---|---|
parent-write-buffer-pause-threshold | 134217728 (128 MiB) | با رسیدن charge صف به این مقدار، تولیدکنندههای child متصل Pause میشوند. |
parent-write-buffer-resume-threshold | 4194304 (4 MiB) | آزادسازی نگهداری محلی در charge کمتر یا مساوی این مقدار، وقتی transport قابلنوشتن است. |
parent-write-buffer-limit | 536870912 (512 MiB) | سقف سخت charge صف هر parent؛ برابری با سقف مجاز است. |
pause و hard باید عدد صحیح در [1, INT_MAX] و resume در [0, INT_MAX] باشند.
شرط نهایی 0 <= resume < pause < hard است. پیشفرض resume مستقل از pause برابر
4194304 (4 MiB) است. مقدار صریح تنظیمشده اعمال میشود؛ 0 برای آزادسازی به
صف خروجی خالی نیاز دارد. در همهٔ حالتها transport باید قابلنوشتن باشد.
مقدار نامعتبر clamp نمیشود.
نمونه زیر resume برابر 1.5 MiB را صریحاً تنظیم میکند:
{
"parent-write-buffer-pause-threshold": 2097152,
"parent-write-buffer-resume-threshold": 1572864,
"parent-write-buffer-limit": 4194304
}
این بودجهٔ خروجی برای هر parent جداست. parent-buffer-limit مجموع ورودی در حال
تشکیل فریم و صف childهای متصل را محدود میکند.
با دریافت Pause از transport parent، هیچ payload تازهای، حتی فریم کنترلی یا Close، به آن سمت فرستاده نمیشود. اگر child هنگام مسدود بودن transport داده بفرستد، پس از پذیرش کامل payload یا batch فقط تولیدکنندهٔ همان child Pause میشود. این قاعده برای Data خالی و ordinary و splice یکسان است. نوشتن مستقیم نیز اگر transport را در حالت Pause باقی بگذارد، پیش از بازگشت، نویسنده را متوقف میکند. Pause و Resume موقت بر اساس وضعیت نهایی هماهنگ میشوند. ارسال کنترل بهتنهایی child را نویسنده محسوب نمیکند؛ child بیکار زیر آستانهٔ تجمیعی تا نخستین نوشتن خود متوقف نمیشود.
با رسیدن charge به parent-write-buffer-pause-threshold، محدودیت تجمیعی همچنان
همهٔ childهای باز و متصل را شامل میشود. نگهداری محلی نویسنده بهتنهایی این
gate را فعال نمیکند. هر child تنها یک دلیل محلی و یک جایگاه در FIFO محلی
parent دارد؛ علت تکراری جایگاه را عوض نمیکند. نویسندهای که پس از Resume
دوباره متوقف شود به انتهای صف میرود.
آزادسازی این نگهداریها به زنده بودن state parent، نبود Finish یا quiescence،
قابلنوشتن بودن transport و charge کمتر یا مساوی
parent-write-buffer-resume-threshold وابسته است؛ لازم نیست gate تجمیعی قبلاً
فعال شده باشد. همین شرط latch تجمیعی را هم پاک میکند. پس از هر callback و
پیش از آزادسازی بعدی یا برداشتن buffer، وضعیت دوباره بررسی میشود. Pause جدید،
عبور از آستانهٔ Resume، فعالشدن دوبارهٔ gate یا نابودی parent آزادسازی را قطع
میکند. تخلیهٔ FIFO فرصت بعدی را فراهم میکند؛ صف خالی و قابلنوشتن نباید
منتظر رویدادی بماند که دیگر رخ نمیدهد. FlowPause همتا و حالت بستهشدن مستقلاند.
پیش از اولین callback، snapshot از همهٔ lineهای منتظر reference موقت میگیرد؛ callback ممکن است sibling یا parent را نابود کند. عضویت و زندهبودن state پیش از استفاده دوباره بررسی میشود. Init/Est دلیل Pause را جدا از مجوز ارسالشده ثبت میکند تا Pause گمشده یا Resume بیجفت رخ ندهد. unlink و بستهشدن، عضویت را پیش از نابودی state حذف میکنند و صرفاً برای پاکسازی Resume نمیفرستند. خروجی بازگشتی پشت Data، کنترل و باقیماندهٔ batch پذیرفتهشده میماند.
اگر charge buffer تازه از سقف عبور کند یا رزرو صف شکست بخورد، فقط همان parent از مسیر معمول teardown بسته میشود؛ bufferها آزاد میشوند و فرایند متوقف نمیشود. مسیر مستقیمِ قابلنوشتن با صف خالی، تخصیص صف ندارد و مشمول سقف نگهداری نیست. فریمهای کنترلی کوچک از رده مناسب pool با padding chain استفاده میکنند. با از دست رفتن parent، خروجی غیرقابلارسال بیدرنگ آزاد میشود؛ drain مستقل صفهای ورودی child طبق قرارداد قبلی ادامه دارد. Open و وضعیت flow-control پیش از callback بازگشتی، پس از پذیرش در مسیر خروجی مرتب، منتشر میشوند.
Backpressure و صفها
MUX backpressure را per-child نگه میدارد:
- اگر child محلی pause شود،
MuxClientبرای همانcidیکFlowPauseمیفرستد. - اگر
FlowPauseاز peer برسد، خواندن از child محلی pause میشود. - اگر برای childای که pause است دادهای از parent برسد، داده روی همان child در صف میماند.
FlowPauseهمان لحظهای ارسال میشود که callback محلی child را Pause میکند و منتظر رسیدن طول صف به آستانهای نمیماند. اگر Pause پیش ازOpenرخ دهد، پس از پذیرشOpenو تمام اولین payload، از جمله batch بزرگ splice، وضعیت child دوباره بررسی میشود؛ فقط اگر هنوز متصل و Pause باشد،FlowPauseارسال میشود. Resume یا بستهشدن child در این فاصله مانع ارسال کنترل کهنه میشود.- وقتی بایت منطقی payload صفشده پایینتر از
child-buffer-resume-thresholdبرسد،FlowResumeمیتواند ارسال شود. مقدار تنظیمشده بهchild-buffer-limitcap میشود. - اگر charge صف یک child به
child-buffer-limitبرسد، آن child بسته میشود. - سقف
parent-buffer-limitمجموع ورودی در حال تشکیل فریم و صف childهای متصل را میسنجد. ترکیب سودمند ورودی مقدم است؛ سپس بزرگترین صف با اولویت قدیمیترین child در تساوی بسته میشود. چند قربانی ممکن است لازم باشد و فشار حلنشده همان parent را میبندد.
فریم Data با طول صفر معتبر است، اما بدون callback یا ورودی صف child مصرف
میشود و activity را زیاد نمیکند. charge صف یک budget proxy برای منابع صفهای
live است، نه حافظهٔ دقیق kernel یا RSS؛ cacheهای allocator،
حافظه حلقه pointerها و baseline ثابت pool در charge یک صف مشخص نیستند.
به این ترتیب یک child کُند میتواند stream خود را متوقف کند، بیآنکه همه childهای دیگر روی همان parent فوراً متوقف شوند.
فریم Close رسیده از peer پس از همه فریمهای Data قبلی همان cid اعمال میشود. اگر مقصد محلی child در حالت pause باشد، MuxClient بایتهای قبلی را نگه میدارد و منتظر Resume میماند؛ هیچ Payloadی را برخلاف Pause اجباری ارسال نمیکند و Finish محلی را فقط پس از خالی و قابلنوشتن شدن صف میفرستد. فریمهای بعدی برای آن cid بستهشده دور ریخته میشوند و childهای نامرتبط روی همان parent به کار عادی ادامه میدهند.
اگر parent transport از دست برود، parent line همچنان بیدرنگ بسته میشود. صفهایی که پیشتر برای childها پذیرفته شدهاند به drainهایی مستقل از parent تبدیل میشوند: childهای قابلنوشتن فوراً کامل میشوند و childهای pauseشده پس از Resume ادامه میدهند، بدون آنکه parent مرده را نگه دارند یا به آن دسترسی پیدا کنند. داده خروجی تازه از child در این حالت رد میشود. detached-buffer-limit مجموع charge منابع نگهداشتهشده و detached-child-limit تعداد child باقیمانده را بهازای هر worker محدود میکند. MuxClient مالک child lineها نیست؛ بنابراین owner واقعی source همچنان مسئول فهرستکردن و finish کردن آنها هنگام shutdown است.
Buffer و Padding
MuxClient MUX headerها را به payload خروجی parent prepend میکند. metadata node:
required_padding_left = 16
layer_group = kNodeLayer4
این 16 بایت لازم است، زیرا نخستین payloadِ child ممکن است دو header داشته باشد: یکی برای Open و دیگری برای Data. دادهها و فریمهای کنترلی بعدی تنها یک header هشتبایتی دارند.
ورودی parent فقط برای تکمیل header یا رسیدن بایتهای بیشتر بدنه منتظر میماند؛ بخش موجود Data بیدرنگ به child یا صف محدود آن میرود و بدنهٔ سایر فریمها بدون تحویل مصرف میشود. اگر ورودی ناقص باقیمانده پس از پردازش بخشهای موجود Data و کنترلهای کامل، با احتساب header، از 1,048,584 بایت بزرگتر شود، MuxClient باقیمانده ناقص parent را دور میریزد، parent line را میبندد و همان رفتار detached drain را برای صف childهایی که پیشتر parse شدهاند اعمال میکند.
نکتههای عملی
MuxClientباید باMuxServerجفت شود.modeدر implementation فعلی اجباری است.connection-duration-msفقط برای timer mode اعمال میشود.connection-capacityفقط برای counter mode اعمال میشود.per-worker-connections-countفقط برای fixed parent pool mode اعمال میشود.- با چند worker، اندازه pool ثابت parentها نیز با تعداد workerها بالا میرود. برای مثال،
per-worker-connections-count: 2با چهار worker هشت جایگاه قابل انتخاب و حداکثر هشت parent بازنشسته زنده دارد. Finishمحلی child یا shutdown منظم برنامه ممکن است صف detached باقیمانده را بهجای forward کردن آزاد کند.- detached drain هیچ بایتی از پروتکل MUX یا نیازمندی قابلیت peer را تغییر نمیدهد.
MuxClientیک تونل میانی است، نه endpoint عمومی.
خاموشسازی worker
هنگام quiescence، MuxClient جدول keepalive محلی worker را متوقف میکند و
پاکسازی پایانی صفهای باقیمانده را بدون ارسال payload، فریمهای
Open/Close یا پیامهای کنترل جریان آزاد میکند. هر worker تمام parentهایی را که ساخته است، مستقل از
انتخاب اتصال، نگه میدارد؛ این فهرست اتصالهای در حال برقراری، بیکار، فعال و بازنشسته از انتخاب را هم شامل
میشود. در worker stop همهٔ این parentها بسته، itemهای keepalive حذف و جدول خالی
روی همان worker نابود میشود؛ Finish برای childهای متصل به سمت مالک مبدأ
ارسال میشود؛ فقط مالک واقعی child line از نوع borrowed را نابود میکند. childهای از قبل جداشده میتوانند تا
Finish مالک مبدأ پس از اجرای hook زنده بمانند و وضعیت و حساب دقیق حافظهٔ صف آنها حفظ میشود.
هنگام نابودی نهایی نمونه، فهرست مالکیت و حساب اتصالهای جداشده باید خالی باشند. تخلیهٔ عادی در قطع اتصال
و Close دریافتی همچنان از backpressure پیروی میکند.
جریان بایت و UDP
هر child یک جریان مرتب بایت است؛ مرز callback از نوع Payload و مرز فریم Data جزو قرارداد child نیستند. یک فریم میتواند با رسیدن بخشهای بدنه چند callback غیرخالی ایجاد کند. برای مثال، فریم با طول اعلامشدهٔ ۶۴ KiB میتواند نخستین ۴ KiB موجود را بدون انتظار برای ۶۰ KiB دیگر تحویل دهد. فریمهای بعدی روی همان parent همچنان پشت باقیماندهٔ بدنهٔ فعلی قرار دارند.
دریافت parent فقط header هشتبایتی را cache میکند و طول باقیماندهٔ بدنهٔ Data را نگه میدارد. هر بخش موجود با مالکیت مستقل استخراج میشود و header بعدی فقط پس از مصرف کامل بدنهٔ فعلی خوانده میشود. child در حالت Pause این بخشها را بهترتیب FIFO و زیر سقفهای فعلی صف نگه میدارد. اگر child هنگام تحویل بسته شود، باقیماندهٔ بدنه بدون تحویل دور ریخته میشود تا header بعدی قابل پردازش باشد. قطع parent بایتهای قبلاً تحویلشده را پس نمیگیرد. بخشهای پذیرفتهشده در صف نیز همان detached drain را دارند، حتی اگر انتهای فریم هرگز نرسیده باشد.
payload با طول صفر همچنان به یک header از نوع Data و بدون بدنه encode میشود. گیرنده آن را بدون callback به child و بدون ورودی صف مصرف میکند. این فریم نه EOF است و نه Close و activity مهلت idle را تازه نمیکند.
برای UDP، در سمت childهای MUX از framing صریح UdpOverTcp استفاده کنید:
client: UdpListener -> UdpOverTcpClient -> MuxClient -> TcpConnector
server: TcpListener -> MuxServer -> UdpOverTcpServer -> UdpConnector
آداپتور UDP که مستقیم کنار MUX قرار دارد نمیتواند به حفظ مرز datagram تکیه کند. جفت UdpOverTcp فعلاً datagram غیرخالی میخواهد؛ جریان MUX نمایش مستقلی برای datagram خالی فراهم نمیکند.
نگهداری splice و بودجهٔ ظرفیت صف
جدول یکپارچهٔ محدودیتها، روند تخصیص و حرکت داده، و حالتهای مرزی را در راهنمای توسعه (انگلیسی) ببینید.
هزینهٔ هر buffer از فرمول مشترک زیر محاسبه میشود:
sizeof(sbuf_t) + sbufGetTotalCapacity(buf) + kSbufAllocationAlignment
ظرفیت شامل padding است و دوباره اضافه نمیشود. برای ordinary این همان هزینهٔ تخصیص قبلی است؛ برای splice ظرفیت منطقی sbuf شمرده میشود، بدون افزودن حافظهٔ کنترل wrapper یا ظرفیت kernel pipe. نامعلوم بودن ظرفیت cacheشدهٔ pipe، یک منبع معتبر را به ordinary تبدیل نمیکند.
parent-buffer-limit با پیشفرض ۵۱۲ MiB، مجموع ورودی در حال تشکیل فریم و
صفهای child متصل را محدود میکند. شمارندهٔ stream از pending_child_queue_charge
جدا میماند و مجموع با بررسی سرریز محاسبه میشود. بخشهای موجود Data و کنترلهای
کامل ابتدا پردازش میشوند؛ عبور موقت همان delivery از سقف باعث رد آن نمیشود.
یک فریم Data میتواند برای child در حالت Pause چند ورودی صف با charge مستقل
بسازد. headerِ cacheشده تا پایان بدنه در طول منطقی و سقف مستقل 1,048,584 بایت
حساب میشود، اما sbuf charge ندارد. بایتهای هنوز دریافتنشده charge نگهداری ندارند.
مصرف بدنهٔ headِ splice ظرفیت منطقی و هزینهاش را کم میکند؛ جابهجایی cursorِ ordinary
یا مصرف prefix چنین کاهشی ندارد.
با رسیدن به سقف دریافت، ابتدا فقط در صورت کاهش قطعی هزینه، fragmentهای ورودی
در یک buffer ordinary با اندازهٔ best-fit ترکیب میشوند. header و ترتیب بایتها حفظ
میشوند. این کار روی هر Push اجرا نمیشود و صف child یا خروجی را بازنویسی نمیکند.
اگر فشار باقی بماند، child با بزرگترین صف بسته میشود؛ در تساوی، قدیمیترین child
اولویت دارد. پس از هر callback وضعیت و هزینه دوباره بررسی میشوند و ممکن است چند
child بسته شوند. نبود قربانی یا نبود پیشرفت، فقط همان parent را میبندد. مقدار 0
سقف تجمیعی دریافت را غیرفعال میکند و سقف پنهان دیگری جای آن نمیگیرد.
سقف child همچنان ۱۲۸ MiB است و برابری رد میشود. خروجی parent بودجهای جدا با آستانهٔ پیشفرض Pause برابر ۱۲۸ MiB، آستانهٔ پیشفرض Resume برابر ۴ MiB و سقف ۵۱۲ MiB دارد؛ برابری سقف خروجی مجاز است. بودجههای detached روی هر worker و معنی صفر/نامحدود آنها حفظ میشوند. آستانهٔ FlowResume طول منطقی payload را میشمارد؛ FlowPause تابع وضعیت Pause محلی child است. برای ordinaryِ child انتخاب ردهٔ pooled ارزانتر باقی است؛ spliceها برای فشار صف تکتک materialize نمیشوند.
هیچ سهمیهٔ مشترک تعداد یا ظرفیت pipe روی worker وجود ندارد. نگهداری یک parent باعث fallback parent دیگر نمیشود. این هزینه سقف FD، ظرفیت kernel، ارجاع صفحه، صف socket یا RSS نیست و سقف لحظهای حافظه نیز محسوب نمیشود. splice بیشتر میتواند با همان بودجهٔ منطقی از descriptor و ظرفیت kernel بیشتری استفاده کند. recycle ممکن است pipe خالی را در pool نگه دارد؛ کاهش charge به معنی بستهشدن واقعی pipe نیست.
ورودی تا ۱ MiB در مسیر تکbufferای میماند؛ ورودی خالی یک Data دارد. ordinary بزرگتر یک aggregate میسازد و splice بزرگتر یک batch کامل و مرتب، با Open فقط یکبار پیش از نخستین Dataِ client. بدنهٔ ۱ MiB بهعلاوهٔ prefix ممکن است نیازمند تقسیم باشد. تخمین هزینهٔ pipe مقصد از ظرفیت منطقی پس از تکمیل بدنه است. هزینهٔ fallback ordinary برای فریمهای باقیمانده رزرو میشود. تمام پذیرشهای قابل شکست پیش از انتشار batch و callback انجام میشوند؛ رد پذیرش همهٔ batch را آزاد و parent مربوط را میبندد. مسیر مستقیم تکbufferایِ parent قابلنوشتن به پذیرش FIFO نیاز ندارد.
ساخت ناموفق pipe، ظرفیت ناکافی، کمبود slot، انتقال کوتاه و EINTR همچنان با fallback کامل ordinary مدیریت میشوند؛ ظرفیت اسمی ۱ MiB تضمین slot کافی نیست. headroom و همهٔ بایتها حتی پس از پیشرفت جزئی حفظ میشوند. اندازهٔ pool/pipe و پیشفرضهای JSON و سقف پروتکل تغییر نمیکنند. Pause پمپ را متوقف میکند؛ Resume، ورودی بازگشتی و Close ترتیب FIFO را حفظ میکنند. مرگ child خروجی متعلق به parent را آزاد نمیکند. parent-loss ورودی و خروجی را آزاد و صفهای مجاز child را به حساب detached منتقل میکند. هر pop و discard پیش از callback هزینهٔ مالک را تسویه میکند. BufferStream و HTTP فقط ordinary میپذیرند.
متادیتای نود
metadata برگرفته از کد منبع:
| ویژگی | مقدار |
|---|---|
| node flags | kNodeFlagSupportsSplice |
can_have_prev | true |
can_have_next | true |
layer_group | kNodeLayer4 |
layer_group_prev_node | kNodeLayer4 |
layer_group_next_node | kNodeLayer4 |
required_padding_left | 16 bytes |