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

MuxServer

MuxServer همتای سمت مقابل MuxClient است. ترافیک فریم‌شده MUX را روی یک parent transport line می‌گیرد، با رسیدن فریم Open یک child line می‌سازد و هر child را به‌عنوان line مستقلی در WaterWall به next می‌فرستد.

سمت قبلی باید transport line حامل فریم‌های MuxClient را فراهم کند و سمت بعدی child line‌های منطقی بازسازی‌شده را می‌گیرد.

جایگاه رایج​

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

از دید node بعدی، هر child ساخته‌شده توسط MuxServer مثل یک اتصال WaterWall جداگانه رفتار می‌کند.

این نود چه می‌کند؟​

  • با رسیدن Init در upstream از transport، parent line را مقداردهی اولیه می‌کند.
  • MUX frame‌ها را از parent line می‌خواند.
  • به‌ازای هر Open frame معتبر یک child line می‌سازد.
  • هر child line را به سمت next مقداردهی اولیه می‌کند.
  • Data، Close، FlowPause و FlowResume را بر اساس cid به مقصد مناسب می‌فرستد.
  • payload child‌های downstream را دوباره داخل MUX Data frame می‌پیچد.
  • رویدادهای Finishِ child را به شکل فریم Close به MuxClient برمی‌گرداند.
  • اگر سمت نوشتن child pause باشد، داده رسیده از parent را در صف نگه می‌دارد.
  • هنگام finish شدن parent transport، صف‌های پذیرفته‌شده child‌ها را detached می‌کند و با رعایت backpressure هر child تخلیه می‌کند.

MuxServer object‌های child از نوع line_t را پشت parent می‌سازد و مسئول آزاد کردن آن‌هاست. parent transport line را خودش نمی‌سازد.

نمونه تنظیم​

{
"name": "mux-server",
"type": "MuxServer",
"settings": {
"child-buffer-limit": 134217728,
"child-buffer-resume-threshold": 524288,
"parent-buffer-limit": 536870912,
"max-children": 10000,
"max-live-children": 0,
"initial-child-idle-timeout-ms": 10000,
"active-child-idle-timeout-ms": 300000,
"memory-high-watermark-percent": 85,
"memory-low-watermark-percent": 80,
"memory-reserve": 134217728,
"memory-fallback-max-live-children": 60000,
"detached-buffer-limit": 268435456,
"detached-child-limit": 12000,
"log-main-line-stats": false
},
"next": "service-side-node"
}

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

فیلدهای سطح اصلی:

فیلدنوعتوضیح
namestringنام یکتای node در config.
typestringباید دقیقاً "MuxServer" باشد.
settingsobjectاختیاری. وقتی نوشته نشود پیش‌فرض‌ها استفاده می‌شوند.
nextstringاجباری. node بعدی line‌های child بازسازی‌شده را دریافت می‌کند.

MuxServer باید یک node transport-facing قبل از خودش داشته باشد و از MuxClient متناظر برسد.

پاسخ keepalive برای parent​

گزینهٔ keepalive یک boolean با پیش‌فرض true است. MuxServer به Ping کامل و بدون بدنه، یک Pong بدون بدنه با همان token سی‌ودوبیتی در CID پاسخ می‌دهد. این کنترل‌ها برای parent هستند، child نمی‌سازند و زمان idle آن را تازه نمی‌کنند. سرور probe یا timer سلامت ایجاد نمی‌کند؛ فاصله و tolerance روی MuxClient تنظیم می‌شوند و پیش‌فرض آن‌ها به‌ترتیب 15000 و 45000 میلی‌ثانیه است.

با "keepalive": false، کنترل‌ها دریافت و مصرف می‌شوند ولی پاسخی ارسال نمی‌شود. MuxClient پیش از اعمال timeout، پشتیبانی peer را با پاسخ معتبر تأیید می‌کند؛ خاموش‌کردن پاسخ‌گویی موجب جایگزینی پی‌درپی نمی‌شود. Pong در همان صف محدود خروجی parent قرار می‌گیرد و هنگام Pause برای Resume منتظر می‌ماند. با پاسخ‌گویی فعال، Ping دارای بدنه خطای پروتکل است و parent مربوط با پاک‌سازی معمول بسته می‌شود.

تنظیمات اختیاری​

گزینهنوعپیش‌فرضتوضیح
child-buffer-limitinteger134217728سقف charge منابع sbuf_t نگه‌داشته‌شده برای یک child در حالت pause. برای ordinary هزینهٔ تخصیص و برای splice ظرفیت منطقی sbuf به‌علاوهٔ header و alignment شمرده می‌شود؛ padding یک‌بار حساب می‌شود. باید بزرگ‌تر از 0 باشد؛ رسیدن به سقف child را با فریم Close می‌بندد.
child-buffer-resume-thresholdinteger524288Low-water mark برحسب بایت منطقی payload صف‌شده برای ارسال FlowResume پس از writableشدن child محلی. باید بزرگ‌تر از 0 باشد و مقدار عددی آن به child-buffer-limit cap می‌شود. مقدار بزرگ‌تر peer را زودتر Resume می‌کند، اما Hysteresis را کاهش می‌دهد و ممکن است چرخه‌های Pause/Resume را بیشتر کند.
parent-buffer-limitinteger536870912سقف ظرفیت ورودی در حال تشکیل فریم به‌علاوهٔ صف child‌های متصل روی هر parent. ابتدا ترکیب سودمند ورودی و سپس بستن بزرگ‌ترین صف با اولویت قدیمی‌ترین child در تساوی انجام می‌شود؛ فشار حل‌نشده parent را می‌بندد. برای غیرفعال کردن سقف تجمیعی مقدار 0 را قرار دهید.
max-childreninteger10000سقف سخت child‌های live متصل به یک parent. باید مثبت باشد و، اگر max-live-children غیرصفر است، از آن سقف بیشتر نباشد؛ CIDهای تاریخیِ ازبین‌رفته شمارش نمی‌شوند.
max-live-childreninteger0سقف سخت child‌های رزروشده یا مقداردهی‌شده در کل این instance و همه parent‌ها و worker‌ها. مقدار 0 این سقف تجمیعی تعداد را غیرفعال می‌کند؛ gate فشار حافظه، سقف هر parent و سقف fallback نمونه‌برداری حافظه فعال می‌مانند. child‌های draining و detached تا نابودی واقعی state شمارش می‌شوند.
initial-child-idle-timeout-msinteger10000مهلت idle پس از Open و پیش از نخستین payload غیرخالی. باید مثبت باشد.
active-child-idle-timeout-msinteger300000مهلت idle قابل refresh پس از آغاز payload واقعی. باید مثبت و حداقل برابر مهلت اولیه باشد.
memory-high-watermark-percentinteger85در این درصد مصرف host یا cgroup محدود، admission بسته می‌شود. بازه مجاز [1, 99] است.
memory-low-watermark-percentinteger80gate فقط وقتی باز می‌شود که همه مصرف‌های قابل اعمال حداکثر این مقدار باشند. باید در [1, 99] و کمتر از high watermark باشد.
memory-reserveintegerبر اساس ram-profileبا رسیدن available مؤثر به این reserve، Open جدید رد می‌شود. بازه [0, INT_MAX] است؛ صفر فقط reserve مطلق را غیرفعال می‌کند.
memory-fallback-max-live-childrenintegerبر اساس ram-profileسقف محافظه‌کارانه کل child‌ها وقتی snapshot حافظه unavailable، unsupported یا قدیمی‌تر از یک ثانیه است؛ حتی وقتی max-live-children برابر 0 باشد. باید مثبت باشد و، اگر max-live-children غیرصفر است، از آن سقف بیشتر نباشد.
detached-buffer-limitintegerبر اساس ram-profileسقف charge منابع نگه‌داشته‌شده به‌ازای هر worker برای صف child‌های مسدودشده پس از از دست رفتن parent. رسیدن به سقف فقط child تازه detachedشده را abort می‌کند. برای غیرفعال کردن این سقف مقدار 0 را قرار دهید.
detached-child-limitintegerبر اساس ram-profileسقف تعداد به‌ازای هر worker برای child‌های مسدودشده‌ای که پس از از دست رفتن parent نگه داشته می‌شوند. رسیدن به سقف فقط child مسدودشده‌ای را که به‌تازگی detached شده است abort می‌کند. برای غیرفعال کردن این سقف مقدار 0 را قرار دهید.
log-main-line-statsbooleanfalseاگر فعال باشد، 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
S280740352 (77 MiB)5677
M1 (client)127926272 (122 MiB)7258
M2 (client-larger)174063616 (166 MiB)8838
L1221249536 (211 MiB)10419
L2 (server، پیش‌فرض سراسری)268435456 (256 MiB)12000

مقادیر بین کمینه و بیشینه روی شش رده مرتب پروفایل RAM به‌صورت خطی درون‌یابی می‌شوند و سقف charge به نزدیک‌ترین MiB گرد می‌شود. نوشتن صریح هر گزینه در تنظیمات node، فقط مقدار مشتق‌شده از پروفایل برای همان گزینه را جایگزین می‌کند.

admission حافظه برای memory-reserve و memory-fallback-max-live-children پیش‌فرض‌های مستقلی دارد که در همهٔ parent‌ها و worker‌های یک نمونه MuxServer مشترک‌اند. تنظیم صریح هر گزینه فقط پیش‌فرض همان گزینه را جایگزین می‌کند:

پروفایل RAMreserve پیش‌فرض حافظهسقف پیش‌فرض fallback live-child
S1 (minimal / ultralow)16777216 (16 MiB)20480
S240370176 (38.5 MiB)28385
M1 (client)63963136 (61 MiB)36290
M2 (client-larger)87031808 (83 MiB)44190
L1110624768 (105.5 MiB)52095
L2 (server، پیش‌فرض سراسری)134217728 (128 MiB)60000

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 خروجی.

حالت‌هایی مانند timer، counter و fixed-connections-count فقط در MuxClient تنظیم می‌شوند. MuxServer بر اساس فریم‌های دریافتی عمل می‌کند و به mode کلاینت وابسته نیست.

مدل parent و child​

MuxServer دو نقش line دارد:

نقشمعنی
parent lineاتصال transport مشترک دریافت‌شده از node قبلی.
child lineیک line معمولی که MuxServer برای یک cid از parent می‌سازد.

با رسیدن فریم Open:

  1. MuxServer بررسی می‌کند که آیا cid از قبل وجود دارد.
  2. اگر cid جدید باشد، یک child line روی همان worker می‌سازد.
  3. state مربوط به MUX را برای child مقداردهی اولیه می‌کند.
  4. child را به parent link می‌کند.
  5. upstream Init را به سمت next روی child line می‌فرستد.

lookup مربوط به cid با hash map و به‌طور متوسط O(1) انجام می‌شود. Open تکراری برای هر child هنوز index‌شده یک protocol violation است و همان parent را می‌بندد.

پیش از allocation، Open جدید باید سقف سخت parent، سقف تعداد تنظیم‌شده برای instance در صورت فعال‌بودن و gate حافظه cache‌شده را بگذراند. رزروهای تجمیعی حتی وقتی max-live-children برابر 0 است نیز شمارش می‌شوند. ردشدن منابع بدون ساخت child، فریم Close(cid) می‌فرستد و parent و siblingها را زنده نگه می‌دارد. bucket هر parent burst برابر 1024 رد و refill برابر 64 token در ثانیه با ساعت monotonic worker مالک دارد؛ abuse پایدار parent را می‌بندد.

sampler سراسری هر 500 ms snapshot می‌سازد و freshness آن یک ثانیه است. در Linux، فشار cgroup برابر بیشترین درصد مصرف و کمترین فضای باقی‌مانده در همهٔ سطح‌های finite و قابل‌مشاهدهٔ cgroup v2/v1 است؛ MemAvailable نیز فضای مؤثر را محدود می‌کند. محدودیت مبهم یا پنهان از fallback محافظه‌کارانه استفاده می‌کند. در Windows از available physical memory استفاده می‌شود و worker‌ها فقط tuple اتمی cache‌شده را می‌خوانند. snapshot قدیمی یا unsupported سقف fallback را به کار می‌گیرد و gate بسته‌شده با pressure تازه را باز نمی‌کند. با snapshot تازه، max-live-children: 0 سقف تجمیعی تعداد child‌ها را برمی‌دارد، اما admission حافظه و max-children فعال می‌مانند؛ نبود دادهٔ تازه همچنان سقف fallback را اعمال می‌کند.

فرمت frame​

MuxServer همان header هشت‌بایتی که MuxClient استفاده می‌کند را می‌پذیرد:

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معنی
0Open
1Close
2FlowPause
3FlowResume
4Data
5Ping؛ بدون بدنه، CID برابر token probe
6Pong؛ بدون بدنه، CID برابر همان token پاسخ

هر فریم Data حداکثر 1,048,576 بایت payload حمل می‌کند. encoder مشترک، payload بزرگ‌تر downstream را به‌ترتیب بایت میان چند فریم Data تقسیم می‌کند. کل خروجی همراه همه header‌ها باید در uint32_t جا شود؛ در غیر این صورت ورودی آزاد می‌شود و فقط همان child بسته می‌شود. گیرنده پس از این تقسیم، مرز callback اصلی را بازسازی نمی‌کند.

جهت و رفتار Callback​

callback ورودیرفتار
upstream Init parentstate parent را مقداردهی اولیه می‌کند، در صورت نیاز ثبت آمار را زمان‌بندی می‌کند و Est را در downstream به node قبلی گزارش می‌دهد.
upstream Payload parentفریم‌های MUX از MuxClient را تجزیه می‌کند، برای Open child می‌سازد و Data، Close، FlowPause و FlowResume را بر اساس cid می‌فرستد.
upstream Pause / Resume parentخروجی parent را بی‌درنگ متوقف می‌کند / FIFO را تخلیه می‌کند؛ نویسندهٔ مسدود به‌صورت محلی و همهٔ child‌ها در آستانهٔ تجمیعی محدود می‌شوند.
upstream Finish parentstate parent را بی‌درنگ از بین می‌برد، صف‌های پذیرفته‌شده child‌ها را به drain‌های مستقل از parent منتقل می‌کند و هر child را پس از خالی و قابل‌نوشتن شدن صف آن finish می‌کند.
downstream Payload childیک MUX Data header prepend می‌کند و frame را روی parent به سمت node قبلی می‌فرستد.
downstream Pause / Resume childFlowPause می‌فرستد / داده queue شده را flush می‌کند و در صورت نیاز FlowResume می‌فرستد.
downstream Finish childchild را جدا و state و line تحت مالکیت را نابود می‌کند؛ سپس Close را به مسیر خروجی مرتب parent تحویل می‌دهد.
upstream Estغیرفعال؛ رسیدن به این callback fatal است.
downstream Initغیرفعال؛ رسیدن به این callback fatal است.

فریم‌های مربوط به cid ناشناخته و typeهای ناشناخته دور ریخته می‌شوند.

بازیابی childهای idle​

هر child پذیرفته‌شده با initial-child-idle-timeout-ms آغاز می‌شود. نخستین بخش غیرخالی Data ورودی یا payload غیرخالی تولیدشده از سمت next آن را به active-child-idle-timeout-ms می‌برد و هر بخش یا payload غیرخالی بعدی deadline فعال را refresh می‌کند. Open، Close، flow control، Init/Est و Data با طول صفر activity محسوب نمی‌شوند. Data با طول صفر بدون callback یا ورودی صف child مصرف می‌شود.

expiry فقط همان child را می‌بندد، در صورت وجود parent فریم Close(cid) می‌فرستد و parent و siblingها را حفظ می‌کند. child detached از مسیر detached-abort استفاده می‌کند. close صریح timer item را بی‌درنگ حذف می‌کند. این تغییر wire format را عوض نمی‌کند؛ parent در تمام عمر transport قابل استفاده مجدد است و نابودی child ظرفیت شمارش آن را آزاد می‌کند.

صف خروجی parent​

هر parent یک FIFO مستقل و با تخصیص در اولین استفاده برای خروجی encode‌شده دارد. تنظیم‌های اختیاری زیر برحسب ظرفیت sbuf به‌علاوهٔ header buffer و alignment هستند. ظرفیت ordinary فیزیکی و ظرفیت splice منطقی است؛ padding یک‌بار شمرده می‌شود. ظرفیت pipe به آن اضافه نمی‌شود و این معیار، اندازه‌گیری حافظهٔ kernel یا RSS نیست.

تنظیمپیش‌فرضمعنی
parent-write-buffer-pause-threshold134217728 (128 MiB)با رسیدن charge صف به این مقدار، تولیدکننده‌های child متصل Pause می‌شوند.
parent-write-buffer-resume-threshold4194304 (4 MiB)آزادسازی نگه‌داری محلی در charge کمتر یا مساوی این مقدار، وقتی transport قابل‌نوشتن است.
parent-write-buffer-limit536870912 (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 شود، MuxServer برای همان cid یک FlowPause می‌فرستد.
  • اگر FlowPause از peer و از طریق parent transport برسد، خواندن از child سمت سرویس pause می‌شود.
  • اگر برای child‌ای که pause است داده‌ای از parent برسد، داده روی همان child در صف می‌ماند.
  • FlowPause همان لحظه‌ای ارسال می‌شود که callback محلی child را Pause می‌کند و منتظر رسیدن طول صف به آستانه‌ای نمی‌ماند.
  • وقتی بایت منطقی payload صف‌شده پایین‌تر از child-buffer-resume-threshold برسد، FlowResume می‌تواند ارسال شود. مقدار تنظیم‌شده به child-buffer-limit cap می‌شود.
  • اگر charge صف یک child به child-buffer-limit برسد، آن child بسته می‌شود.
  • سقف parent-buffer-limit مجموع ورودی در حال تشکیل فریم و صف child‌های متصل را می‌سنجد. ترکیب سودمند ورودی مقدم است؛ سپس بزرگ‌ترین صف با اولویت قدیمی‌ترین child در تساوی بسته می‌شود. چند قربانی ممکن است لازم باشد و فشار حل‌نشده همان parent را می‌بندد.
  • قرار دادن parent-buffer-limit روی 0 تنها بودجه تجمیعی را غیرفعال می‌کند و سقف per-child همچنان اعمال می‌شود.

این charge یک budget proxy برای منابع صف‌های live است، نه حافظهٔ دقیق kernel یا RSS؛ cacheهای allocator، حافظه حلقه pointer‌ها و baseline ثابت pool در charge یک صف مشخص نیستند. به این ترتیب یک child کُند می‌تواند stream خود را متوقف کند، بی‌آنکه همه child‌های دیگر روی همان parent فوراً متوقف شوند.

Finish و Close​

اگر MuxServer برای یک child فریم Close بگیرد، این Close پس از همه بایت‌های Data قبلیِ صف‌شده اعمال می‌شود. child در حالت pause همچنان با cid قابل‌شناسایی می‌ماند؛ فریم‌های بعدی برای آن cid بسته‌شده دور ریخته می‌شوند، Resume تخلیه FIFO را ادامه می‌دهد و Finish به‌همراه آزاد کردن line تحت مالکیت فقط در مرز خالی و قابل‌نوشتن شدن صف انجام می‌شود.

اگر سمت service-facing یک child را ببندد، MuxServer فریم Close را می‌سازد، child را از parent جدا و state و line تحت مالکیت خود را نابود می‌کند، سپس Close را به مسیر خروجی مرتب parent تحویل می‌دهد. پاسخ Close به Open ردشده هم از همین FIFO می‌گذرد. در overflow محلی، state parent قرض‌گرفته‌شده آزاد و owner آن مطلع می‌شود؛ MuxServer خود parent را با lineDestroy نابود نمی‌کند.

اگر parent transport بسته شود، MuxServer state مربوط به MUX در parent قرض‌گرفته‌شده را بی‌درنگ از بین می‌برد و صف هر child پذیرفته‌شده را به registry تحت مالکیت و worker-local منتقل می‌کند. صف‌های قابل‌نوشتن فوراً finish می‌شوند؛ صف‌های pause‌شده پس از Resume و بدون نگه‌داشتن یا دسترسی به parent مرده تخلیه می‌شوند. داده خروجی تازه از child رد می‌شود. detached-buffer-limit مجموع charge منابع نگه‌داشته‌شده و detached-child-limit تعداد child باقی‌مانده را به‌ازای هر worker محدود می‌کند.

Buffer و Padding​

MuxServer MUX header‌ها را به payload downstream child prepend می‌کند. metadata node:

required_padding_left = 8
layer_group = kNodeLayer4
layer_group_next_node = kNodeLayer4
layer_group_prev_node = kNodeLayer4

ورودی parent فقط برای تکمیل header یا رسیدن بایت‌های بیشتر بدنه منتظر می‌ماند؛ بخش موجود Data بی‌درنگ به child یا صف محدود آن می‌رود و بدنهٔ سایر فریم‌ها بدون تحویل مصرف می‌شود. اگر ورودی ناقص باقی‌مانده پس از پردازش بخش‌های موجود Data و کنترل‌های کامل، با احتساب header، از 1,048,584 بایت بزرگ‌تر شود، MuxServer باقی‌مانده ناقص را دور می‌ریزد، parent را به سمت node قبلی finish می‌کند و برای صف child‌هایی که پیش‌تر parse شده‌اند همان detached drain را به کار می‌گیرد.

نکته‌های عملی​

  • این node را با MuxClient جفت کنید؛ بدون آن کاربردی ندارد.
  • MuxServer تنظیمات حالت concurrency سمت کلاینت را ندارد.
  • یک TcpConnector بعد از MuxServer برای هر child یک اتصال سرویس جداگانه می‌سازد.
  • MuxServer یک تونل میانی است، نه endpoint عمومی.
  • Finish سمت سرویس یا shutdown منظم worker ممکن است داده detached باقی‌مانده را دور بریزد. هنگام shutdown، پیش از بستن همه child‌های تحت مالکیت، هیچ Payload صف‌شده‌ای حتی برای child قابل‌نوشتن forward نمی‌شود.
  • هیچ تغییری در بایت‌های پروتکل MUX یا قابلیت موردنیاز peer ایجاد نشده است.

خاموش‌سازی worker​

هنگام quiescence، MuxServer تایمر idle را جدا می‌کند و صف‌های باقی‌مانده را بدون ارسال payload، فریم Close یا پیام‌های کنترل جریان آزاد می‌کند. جدول idle محلی هر worker فهرست کامل child‌های متعلق به آن، چه متصل و چه جداشده، است. worker stop کل جدول را تخلیه می‌کند، حساب حافظهٔ صف و سهم اتصال زندهٔ هر child را دقیقاً یک بار آزاد می‌کند، Finish را به سمت مقصد می‌فرستد و child را نابود می‌کند. parent قرض‌گرفته‌شده می‌تواند با وضعیت خالی و معتبر MUX تا Finish مالک واقعی خود زنده بماند. جدول idle پس از تخلیه روی همان worker نابود می‌شود؛ بررسی مجموع child‌های زنده پس از توقف همهٔ worker‌ها انجام می‌شود. رفتار عادی قطع اتصال، انقضای idle و Close ترتیبی تغییر نمی‌کند.

جریان بایت و 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 flagskNodeFlagSupportsSplice
can_have_prevtrue
can_have_nexttrue
layer_groupkNodeLayer4
layer_group_prev_nodekNodeLayer4
layer_group_next_nodekNodeLayer4
required_padding_left8 bytes