ساعت ۲:۱۱ بامداد است و هشدار هزینهها فعال شده، اما داشبورد شما تعداد استریمهای فعال و نرخ تکمیل درخواستها را در وضعیت عادی نشان میدهد. تعداد تکمیلهای مفید در دقیقه (Useful completions per minute) بهسختی تغییر کرده است. شما احتمالاً با یک نشت توکن خاموش روبرو هستید که توسط جریانهای یتیم (Orphan Streams) ایجاد شده است؛ فرآیندهای زامبی که پس از بستن اتصال توسط کاربر، همچنان به تولید متن ادامه میدهند.
این شکست عملیاتی زمانی رخ میدهد که یک Worker با وجود بسته شدن TCP، به خروجی توکنها ادامه دهد؛ زیرا فرآیند تولید متن به چرخه حیات سوکت (Socket) گره نخورده است. در بسیاری از معماریهای چت استریمینگ، یک Reverse Proxy بین کاربر و Worker قرار دارد؛ اگر این پروکسی سیگنال لغو (Cancellation) را صریحاً ارسال نکند، Worker بودجه شما را صرف پاسخهایی میکند که هیچکس هرگز آنها را نخواهد خواند. این یک حادثه هزینهای است، نه یک مشکل ظرفیت.
به نقل از راهنمای فنی منتشر شده در dev.to در ۶ سپتامبر ۲۰۲۶، اشتباه رایج اکثر تیمها، مقیاسدهی نسخهها (Replicas) صرفاً بر اساس میزان بهرهبرداری (Utilization) است. وقتی جریانهای یتیم جایگاهها (Slots) را اشغال میکنند، بهرهبرداری بالا به نظر میرسد، اما خروجی مفید صفر است. معیار واقعی که باید رصد کنید، شکاف بین توکنهای صورتحساب شده (Billed tokens) و توکنهای مفیدی است که به کاربران زنده تحویل داده شده است. همانطور که در تحلیلهای پیشین ما درباره بهینهسازی هزینههای استنتاج اشاره کردیم، ظرفیتهای آزاد اغلب باعث میشوند این شکستها نادیده گرفته شوند، زیرا هزینههای مربوط به ظرفیتهای بیکار شبیه به یک شب آرام به نظر میرسند، تا زمانی که در پیکهای ترافیکی بعدی، زمانهای تعیینشده (Deadlines) فرو بپاشند. این موضوع بهویژه در محیطهای آزمایشی رایگان بحرانیتر است، چرا که عدم حسابرسی دقیق توکن در لایههای رایگان میتواند منجر به شکست سریع پروژهها شود.
تمرین بازتولید محلی
برای شناسایی این نشت، میتوانید یک توپولوژی محلی روی رابطهای Loopback اجرا کنید. این تست را روی محیطهای عملیاتی مشترک اجرا نکنید. هدف، اندازهگیری اتلاف است، نه کیفیت مدل؛ بنابراین با محدود کردن حداکثر توکنها (Max tokens)، از خروج کنترلشده خروجی جلوگیری کنید تا خروجی نتواند به طور نامحدود اجرا شود. در طول اجرای این تست، تنظیمات نمونهگیری (Sampling) را تغییر ندهید.
ساختار پیشنهادی:
- Edge: یک پروکسی معکوس با مهلت خواندن (Read Timeout) ۶۰ ثانیه.
- Admit: یک فرآیند پذیرش کوچک روی پورت ۸۰۹۰.
- Worker: یک تولیدکننده محلی با یک Slot روی پورت ۸۱۰۰.
- Collector: سرویسی که هر ۱۵ ثانیه مسیر
/metricsرا میخواند.
جریان ترافیک به این صورت است: client -> edge:8080 -> admit:8090 -> worker:8100 | +-> collector:9090/metrics
اندازهگیری «شکاف یتیم»
برای اثبات وجود نشت، این راهنما یک حجم کاری (Workload) خاص را پیشنهاد میکند. Worker را به عنوان یک تولیدکننده تک-اسلتی با یک استریم در جریان (In-flight stream) و بدون دستهبندی (Batching) تعریف کنید.
شرایط تمرین:
- کاربران: ۲۰ کاربر که درخواستهای استریمینگ را روی Loopback باز میکنند.
- ورودی: هر پرامپت بهطور ثابت تقریباً ۴۰۰ توکن ورودی دارد.
- خروجی: خروجی هدف روی ۲۵۶ توکن بهطور سختگیرانه محدود (Hard-capped) شده است.
- تزریق خطا: ۸ کاربر بهطور عمدی در ثانیه ۱.۵ اتصال را قطع میکنند.
برای تکمیل این تمرین، باید این میدانهای تلهمتری (Telemetry) خاص را استخراج کنید:
stream_age_seconds: زمان سپری شده از شروع درخواست.client_connected: پرچم باینری (۱ در صورتی که سوکت کاربر زنده باشد).tokens_out_total: توکنهای خروجی که Worker برای آنها هزینه ثبت کرده است.tokens_useful_total: توکنهای تحویل داده شده به کاربر زنده.queue_age_seconds: زمان انتظار در ابتدای صف.slot_utilization: نسبت اسلاتهای مشغول به اسلاتهای موجود.deadline_slack_seconds: زمان باقیمانده تا ضربالاجل.
فرضیه: تفاضل tokens_out_total و tokens_useful_total برابر با هزینه یتیم است. اگر این شکاف در حالی که بهرهبرداری ثابت است رشد کند، سیستم باید درخواستها را رد کند.
تزریق خطا و خروجی مورد انتظار
برای اجرای این مورد در آزمایشگاه، از چهار ترمینال استفاده کنید. ترمینال A فرآیند پذیرش را اجرا میکند. ترمینال B کاربری است که تا تولید ۲۵۶ توکن منتظر میماند. ترمینال C هشت کاربر را با یک حلقه اجرا میکند که در ثانیه ۱.۵ قطع میشوند: for i in $(seq 1 8); do curl -N -m 1.5 -X POST http://127.0.0.1:8090/v1/stream & done. ترمینال D متریکها را جمعآوری میکند.
اگر کاربر طولانی را با Ctrl-C در حین تولید متن قطع کنید، client_connected باید به صفر برسد. اگر tokens_out_total پس از قطع اتصال همچنان افزایش یابد، یعنی Worker مرگ سوکت را نادیده میگیرد. این همان نشت محیط عملیاتی در مقیاس کوچک است.
خروجی نمونه (به عنوان مثال، نه اندازهگیری واقعی):
tokens_out_total: ۶۴tokens_useful_total: ۴۸cancelled_orphan: ۸rejected_age: ۱stream_age_seconds: ۰.۰client_connected: ۰slot_utilization: ۰.۰queue_age_seconds: ۰.۰deadline_slack_seconds: ۸.۰
در این سناریو، شکاف یتیم ۱۶ توکن است. ۸ قطع اتصال بهعلاوه یک ردِ سنی نباید اسلات را اشغال کنند. اگر slot_utilization پس از قطعها روی ۱.۰ بماند، باید سیستم را بازگردانی (Rollback) کنید. اگر queue_age_seconds بیش از ۶ ثانیه شد و Slack زیر ۲ ثانیه رفت، باید هشدار On-call صادر شود.
پیادهسازی برش پذیرش (Admission Cut)
راهکار اصلی، رد کردن استریمها بر اساس سن (Age) است، نه فقط در دسترس بودن اسلات. برای یک چت تعاملی معمولی که پاسخهای مفید به ۸ ثانیه Slack نیاز دارند، آستانه پیشنهادی رد کردن هر استریمی است که بیش از ۶ ثانیه سن داشته باشد.
منطق آستانه ۶ ثانیهای:
پاسخهای مفید همچنان به ۸ ثانیه Slack نیاز دارند. برش ۶ ثانیهای، ۲ ثانیه فرصت میدهد تا اسلات فعلی تخلیه شده و درخواست بعدی در صف قرار گیرد.
اگر stream_age_seconds بیش از ۶ شد در حالی که client_connected صفر است، سیستم باید این ترتیب مدیریت خطا را دنبال کند:
۱. متوقف کردن پذیرشهای جدید.
۲. ارسال سیگنال لغو به Worker و سپس بستن اتصال Upstream.
۳. علامتگذاری client_connected=0 پیش از شمارش توکنهای مفید بیشتر.
۴. تخلیه اسلات تک تا زمانی که queue_age_seconds به صفر برسد.
۵. تنها پس از آن، باز کردن پذیرش برای کاربر بعدی.
تلاش مجدد (Retry) باید فقط پس از آزاد شدن اسلات رخ دهد. تلاش مجدد در برابر یک استریم زامبی، هزینه توکن را دو برابر میکند و کمکی به Slack ضربالاجل نمیکند.
ماتریس تصمیم برای مهندسان On-Call
طبق گزارش این راهنما، هنگام دریافت هشدار هزینه، بهجای تکیه بر بهرهبرداری خام یا تجربیات شفاهی، از این جدول تصمیم استفاده کنید. در این مورد، سن صف را بر بهرهبرداری و Slack را بر نرخ توکن ترجیح دهید.
| سناریو | وضعیت متریک | اقدام |
|---|---|---|
| سالم | بهرهبرداری بالا، سن صف کم، Slack سالم | ادامه سرویسدهی |
| یتیمها | بهرهبرداری کم، سن صف بالا، کاهش Slack | رد کردن یتیمها |
| زامبیها | بهرهبرداری کم، سن صف کم، شکاف توکن بالا | لغو زامبیها |
| اضافه بار | بهرهبرداری بالا، سن صف بالا، Slack < ۲ ثانیه | حذف بار (Load Shedding) |
| نشت | ظرفیت آزاد + هرگونه شکاف زامبی | توقف Soak، عدم مقیاسدهی |
پاکسازی و بازگشت
پس از تمرین، فرآیند پذیرش و تمام کارهای curl باقیمانده را متوقف کنید. با دستور ss -ltnp تایید کنید که هیچ چیزی روی پورتهای ۸۰۹۰ یا ۸۱۰۰ گوش نمیدهد. اگر مهلت پروکسی تغییر کرده بود، آن را به حالت قبل برگردانید.
برای یک پروکسی لبه واقعی، قانون بازگشت ساده است: آخرین مهلت شناخته شده و نگاشت لغو را بازیابی کنید. پذیرش را به حالت «اسلات مشغول یعنی ۴۲۹» برگردانید. برش سنی ۶ ثانیهای را بدون بررسی در محیط عملیاتی رها نکنید. بازگشت را در همان پنجره تغییرات بنویسید و دلیل انتخاب این آستانه را یادداشت کنید تا مهندس On-call بعدی، Slack را ببیند، نه یک عدد تصادفی.
محدودیتهای حیاتی
این روش برای استریمینگ تعاملی طراحی، نه پردازش دستهای (Batch Processing). اعمال برش ۶ ثانیهای روی کارهای دستهای طولانی منجر به رد درخواستهای نادرست میشود، زیرا کارهای دستهای نیاز به سن صف بر اساس ضربالاجل دسته دارند.
چه کسانی باید از این روش صرفنظر کنند:
- تیمهایی که پروکسی آنها تولید متن را دقیقاً به طول عمر اتصال گره زده است.
- سیستمهایی که کاربران بهجای استریم، از Polling استفاده میکنند (پولینگ به مسیر لغو متفاوتی نیاز دارد).
- کسانی که از این متریکها بهعنوان حسابرس صورتحساب استفاده میکنند؛ اینها شمارندههای محلی هستند، نه فاکتورهای ارائهدهنده.
برای کسانی که از دسترسی رایگان مدلها برای تستهای Soak یا ارزیابیهای شبانه استفاده میکنند، هشدار جدی است: تکیه بر ظرفیت رایگان اشتباه است. اگر این سه شرط برقرار است، ظرفیت رایگان را رد کنید:
۱. کاربران بتوانند بدون ارسال فریم لغو، قطع شوند.
۲. Worker تولید متن را به عمر سوکت گره نزده باشد.
۳. Slack ضربالاجل کوتاهتر از میانگین سن استریم باشد.
جریانهای یتیم، لایههای رایگان را به نشتهای خاموشی تبدیل میکنند که هزینه واقعی مقیاسدهی عملیاتی را میپوشانند. این تغییر در رویکرد، اهرم اصلی کنترل هزینه را از «تعداد نسخهها» به «سن اتصال» منتقل میکند. با اعتماد به Slack ضربالاجل بهجای نرخ توکن، تیمها میتوانند پرداخت برای محاسبات زامبی که هیچ ارزشی برای کاربر ندارد را متوقف کنند. این نوع هزینههای پنهان مشابه مواردی است که در سندباکسهای متریشده برای عاملهای هوش مصنوعی مشاهده شده و هزینههای عملیاتی را بهطور غیرمنتظرهای افزایش میدهد.
گام بعدی شما
- متریک
tokens_out_totalرا باtokens_useful_totalدر داشبورد خود مقایسه کنید تا میزان نشت فعلی را بسنجید. - بررسی کنید آیا Reverse Proxy شما سیگنالهای لغو را به Workerها منتقل میکند یا خیر.
- در صورت استفاده از استریمینگ، یک آستانه زمانی برای رد درخواستهای بدون اتصال (Disconnected) تعریف کنید.
اما مدیریت حافظه در این استریمها چالش دیگری است؛ برای درک نحوه بهینهسازی KV Cache در مقیاس بالا، تحلیل ما درباره vLLM را بخوانید.




گفتگو