Timeout در کدام مرحله است؟ اتصال، خواندن و بودجهٔ زمانی
در این درس ۷ بخش
فرض کنید صفحه باز میشود اما دریافت پاسخ کامل در مرورگر طول میکشد. عبارت «Timeout شد» فقط میگوید یک انتظار از مهلت تعیینشده عبور کرده است؛ هنوز معلوم نیست منتظر چه بودهایم. در این درس یک اتصال محلی عمداً پاسخ را آهسته میدهد. هدف، تشخیص مرحله و ساخت بودجهٔ زمانی روشن است، نه افزایش دلخواه همهٔ مهلتها. پیشنیاز، پوشهٔ استخراجشدهٔ آزمایشگاه و حساب معمولی لینوکس است؛ sudo لازم نیست.
سه انتظار متفاوت را یک نام نگذاریم
پیش از اتصال، حل نام ممکن است زمان بگیرد؛ سپس برقراری اتصال، و در پروتکلهای امن مذاکرهٔ TLS مطرح است. بعد از آن هم انتظار نخستین داده و خواندن ادامهٔ پاسخ وجود دارد. این مرحلهها یک رفتار واحد نیستند. در آزمایش ما از IPv4 لوپبک و بدون DNS و TLS استفاده میشود تا فقط انتظار خواندن را ببینیم. سادهکردن محیط به معنی نبود آن مرحلهها در برنامهٔ واقعی نیست.
مهلت اتصال تعیین میکند مشتری تا چه حد برای وصلشدن صبر کند. مهلت خواندن، انتظارِ عملیات دریافت را محدود میکند. بودجهٔ کل، سقف زمانیِ تعریفشده برای مجموعهٔ مراحل انتخابشده است. اگر هر عملیات از نو همان مهلت کامل را بگیرد، سقف کل الزاماً برقرار نمیماند. در گزارش بنویسید بودجه از کدام نقطه آغاز میشود؛ «کل» بدون بیان دامنه، برای مقایسه کافی نیست.
آزمایش واقعیِ انتظار هنگام خواندن
ترمینال را در پوشهٔ project باز کنید و فرمان زیر را اجرا کنید. آزمایشگاه سوکتی روی 127.0.0.1 با پورت انتخابشدهٔ سیستم میسازد، چند بایت کوتاه میفرستد و در ادامه مکث کنترلشده دارد. مشتری پیش از هر دریافت، باقیماندهٔ بودجه را حساب میکند. هیچ درخواست اینترنتی، حل نام یا تغییر قانون فایروال در این آزمایش انجام نمیشود.
python3 lab.py timeoutموفقیت آموزشی این فرمان با رخدادن انتظارِ عمدی سازگار است: ok=true، connected=true و stage برابر read را میخواهیم. server_joined=true نشان میدهد رشتهٔ سرور هم پس از آزمایش جمع شده است. مقدار received_bytes و elapsed_ms مشاهدهاند و ممکن است میان اجراها تفاوت کنند؛ زمانبند سیستم، بار دستگاه و زمان آغاز هر مرحله روی عدد واقعی اثر میگذارند.
خطای برنامهٔ آزمایشگاه با انتظارِ عمدی فرق دارد. اگر CLI با کد یک یا ok=false تمام شد، نتیجهٔ پیشبینیشده اثبات نشده است. اگر با کد ۷۷ و skipped=true روبهرو شدید، پیشنیاز اجرا برقرار نیست؛ مثلاً فرمان را با root اجرا کردهاید. این خروجی را سبز گزارش نکنید. پورت چاپشده نیز عددی برای کپیکردن در تنظیمات سرور اصلی شما نیست.
connected=true
stage=read
server_joined=true
elapsed_ms و received_bytes: از اجرای واقعی خوانده شوند؛ عدد ثابت معیار نیست.چرا برای فاصلهٔ زمانی از monotonic استفاده میکنیم؟
ساعت دیواری برای ثبت تاریخ رخداد مفید است، اما ممکن است در اثر تنظیم ساعت تغییر کند. برای اندازهگیری فاصله، time.monotonic مبنایی با حرکت یکنواخت میدهد؛ مبدأ آن را تاریخ تقویمی ندانید. یک deadline از همان ساعت میسازیم و خواندنهای بعدی را هم با همان ساعت مقایسه میکنیم. ترکیب عدد epoch با خواندن monotonic، حتی اگر هر دو بر حسب ثانیه باشند، محاسبهٔ معناداری نیست.
فایل مستقل 05-deadline-arithmetic.py زیر فقط حسابِ بودجه را آموزش میدهد. خواندنهای آن ساختگیاند و شاهد رفتار شبکه معرفی نمیشوند. پس از اجرای آن باید 0.20, 0.13, 0.06, 0.00 ببینید. این تمرین کوچک کمک میکند پیش از بررسی تأخیر سیستم، خطای حساب و تمدید بیخبرِ مهلت را از طراحی برنامه حذف کنیم.
import math
def remaining_seconds(deadline, now):
if not math.isfinite(deadline) or not math.isfinite(now):
raise ValueError('Use finite readings from the same clock')
return max(0.0, deadline - now)
# Arithmetic exercise: these readings are invented, not network measurements.
deadline = 0.20
readings = [0.00, 0.07, 0.14, 0.21]
remaining = [remaining_seconds(deadline, now) for now in readings]
assert all(math.isclose(a, b, abs_tol=1e-12)
for a, b in zip(remaining, [0.20, 0.13, 0.06, 0.0]))
print(', '.join(f'{value:.2f}' for value in remaining))
در حلقهٔ واقعی، پیش از هر دریافت، remaining را از deadline منهای زمان جاری حساب میکنیم. اگر مثبت نبود، عملیات تازه آغاز نمیشود. اگر مثبت بود، همان مقدار، مهلت انتظار بعدی است. حتی این روش اجرای دقیقاً سرِ میلیثانیه را تضمین نمیکند؛ برگشت کنترل، پاکسازی و زمانبندی سیستم زمان میبرند. قرارداد ما جلوگیری از اعطای بودجهٔ تازه در هر دور است.
Timeout با پایان پاسخ و خطای HTTP فرق دارد
ممکن است سرور اتصال را ببندد و دریافت، بایت خالی برگرداند؛ این با تمامشدن مهلت یکسان نیست. ممکن است پاسخ کامل با وضعیت HTTP خطا دریافت شود؛ آن هم Timeout نیست. حتی دریافت چند بایت موفق ثابت نمیکند پیام کاربردی کامل است. برنامه باید قالب و شرط پایان پاسخ را بداند؛ این آزمایشگاه فقط رفتار انتظار سوکت را جدا میکند و پیادهسازی کامل HTTP نیست.
افزایش مهلت گاهی برای یک عملیات واقعاً طولانی مناسب است، ولی علتِ انتظار بیپایان یا قفل برنامه را رفع نمیکند. پیش از تغییر، نمونهٔ ورودی، مرحلهٔ انتظار، زمان آغاز، دادهٔ دریافتشده و محدودیت قبلی را ثبت کنید. سپس فقط یک فرضیه را تغییر دهید. اگر چند مهلت، تعداد worker و شبکه را همزمان عوض کنید، معلوم نمیشود کدام تغییر واقعاً اثر گذاشته است.
پیوند با سرورهایی که قبلاً ساختهایم
در مثالهای Nginx و Node، مشتری، پراکسی و برنامه ممکن است هرکدام مهلت مستقل داشته باشند. پایان انتظار پراکسی لزوماً کارِ درحالاجرای برنامه را لغو نمیکند. هنگام بررسی درخواست واقعی باید ببینید کدام جزء پیام را تولید کرده و آیا زمانها قابل مقایسهاند. برای مرور مسیر لایهها به درس قبلی مراجعه کنید؛ اینجا نصب دوبارهٔ سرویس یا گواهی را تکرار نمیکنیم.
درس عیبیابی لایهبهلایهٔ بکاند:
https://www.darsavand.com/courses/nodejs-backend-ubuntu-deployment?lesson=4529b889-fc8c-493e-bf2b-fd47eb5c9231
درس HTTPS و مسیر مرورگر تا درگاه:
https://www.darsavand.com/courses/nodejs-http-server-page-api-tls?lesson=ebfdc0f5-e56c-45d7-9b23-f8623f0b57af
برای مرور واژهها از #linux-timeout، #linux-socket و #linux-loopback استفاده کنید. معیار تحویل این درس یک گزارش کوتاه است که نوع مهلت، دامنهٔ بودجه، مرحلهٔ مشاهدهشده و محدودیت نتیجه را بگوید. فقط نوشتن «سرور کند بود» کافی نیست؛ باید بتوانید توضیح دهید کدام بخش از همین آزمایش این برداشت را پشتیبانی میکند.
تمرین و پاسخ تشریحی
پرسش 1: مشتری هر بار recv را با timeout برابر دو ثانیه صدا میزند. سرور هر یک ثانیه یک بایت میفرستد. آیا کل پاسخ حتماً تا دو ثانیه تمام میشود؟
پاسخ تشریحی: خیر. مهلت هر recv مستقل است و با دریافت هر بایت ممکن است دور بعدی دو ثانیهٔ تازه بگیرد. برای سقف کل باید پیش از شروع، deadline بسازیم و پیش از هر خواندن زمان باقیمانده را دوباره حساب کنیم. اگر قالب پاسخ پایان مشخصی نداشته باشد، علاوه بر زمان، شرط کاملشدن پیام هم باید تعریف شود.
پرسش 2: گزارش آزمایش connected=true و stage=read دارد. آیا بازکردن دوبارهٔ پورت در فایروال، نخستین اصلاح منطقی است؟
پاسخ تشریحی: این مشاهده نشان میدهد اتصال محلی در همان آزمایش برقرار شده و انتظار در مرحلهٔ خواندن رخ داده است. فایروالِ اتصال، از این شاهد علت نخست نیست. نخست رفتار پاسخگوییِ سرور، پایان پیام و بودجهٔ خواندن را بررسی میکنیم. دربارهٔ مسیر اینترنت یا سرور دیگری هنوز شاهدی نداریم؛ نتیجه را به همهٔ شبکه تعمیم نمیدهیم.
پرسش 3: بودجه ۲۰۰ میلیثانیه است. پس از دریافت اول ۷۰ و پس از دریافت دوم ۱۴۰ میلیثانیه گذشته است. مهلت بعدی هر بار چقدر است و در ۲۱۰ چه میکنیم؟
پاسخ تشریحی: در دو نقطهٔ نخست بهترتیب ۱۳۰ و ۶۰ میلیثانیه باقی مانده است. در ۲۱۰، بودجه تمام شده و باید پیش از درخواست خواندن تازه، شکستِ مهلت کل را اعلام کنیم. timeout صفر را بهعنوان «بینهایت صبر کن» تفسیر نمیکنیم؛ در سوکت پایتون صفر حالت غیرمسدود است. مرز زمان را خود الگوریتم باید بررسی کند.
منابع رسمی برای بررسی و مطالعهٔ بیشتر
Python 3.12 — socket timeout modes: https://docs.python.org/3.12/library/socket.html
Python 3.12 — time.monotonic: https://docs.python.org/3.12/library/time.html
Python 3.12 — subprocess timeout: https://docs.python.org/3.12/library/subprocess.html
متن، سناریوها و پرسشهای این درس تألیف مستقل تحریریه درسآوند است. منابع برای بررسی رفتار ابزار و ادامهٔ مطالعه معرفی شدهاند؛ این آموزش ترجمهٔ یک منبع واحد نیست.
فایل آزمایشگاه، راهنمای اجرا، آزمونها و فرم گزارش رخداد:
https://www.darsavand.com/resources/linux-application-errors-practice-kit
برای دیدن ترتیب دورههای مقدماتی، پیشرفته و این مسیر، نقشهٔ راه لینوکس را بخوانید:
https://www.darsavand.com/resources/linux-learning-roadmap
تألیف و مثالهای اختصاصی: تحریریه درسآوند.
متن درسهای عمومی را بدون ورود بخوانید.