وقتی «بیکاری» بیکاری نیست: باگ پنهان CUBIC در QUIC
خلاصهٔ کاملتر
CUBIC پرکاربردترین الگوریتم کنترل ازدحام (Congestion Control Algorithm) در اینترنته و پیشفرض کرنل لینوکسه. وظیفهاش اینه که بفهمه چقدر میشه داده فرستاد — نه آنقدر کم که پهنای باند هدر بره، نه آنقدر زیاد که شبکه ببنده. ابزار اصلیش پنجره ازدحام (cwnd) هست: سقفی روی تعداد بایتهایی که میتونن همزمان «در راه» باشن. کلودفلر هم از CUBIC بهعنوان کنترلر پیشفرض در پیادهسازی متنباز QUIC خودش به اسم quiche استفاده میکنه.
ماجرا با گزارش خرابیهای عجیب در تستهای یکپارچهسازی شروع شد. تست شبیهسازی یه دانلود ۱۰ مگابایتی از طریق HTTP/3 بود با RTT برابر ۱۰ms و ۳۰٪ اتلاف تصادفی بسته فقط در دو ثانیه اول. بعد از دو ثانیه، اتلاف کاملاً متوقف میشد. انتظار این بود که CUBIC کمی عقبنشینی کنه، بعد بهتدریج سرعت بگیره و دانلود رو در ۴–۵ ثانیه تموم کنه. ولی در حدود ۶۱٪ موارد، دانلود در مهلت ۱۰ ثانیهای هم تموم نمیشد.
با آنالیز لاگها مشخص شد که بعد از توقف اتلاف، cwnd روی پایینترین مقدار ممکن — یعنی ۲۷۰۰ بایت (دو پکت) — قفل میمونه و اصلاً رشد نمیکنه. بدتر از اون، CUBIC در ۶.۷ ثانیه دقیقاً ۹۹۹ بار بین حالت «جلوگیری از ازدحام» و «ریکاوری» جابجا میشه — یعنی هر ~۱۴ms یهبار؛ که مشکوک به RTT اتصاله. این نشون میداد یه چیزی در منطق CUBIC داره هر دور رفتوبرگشت، یه رویداد «اتلاف» کاذب تشخیص میده.
برای تأیید اینکه مشکل مختص CUBICه، همون تست با Reno (یه الگوریتم کنترل ازدحام دیگه) اجرا شد و نرخ موفقیت ۱۰۰٪ بود — Reno بعد از توقف اتلاف بهراحتی ریکاوری میکرد.
ریشه مشکل به یه بهینهسازی کرنل لینوکس در سال ۲۰۱۷ برمیگرده. در CUBIC یه مفهوم به اسم epoch وجود داره که نقطه مرجع زمانی برای محاسبه نرخ رشد cwnd هست. وقتی اتصال مدتی بیکار بوده («app-limited»)، این epoch میتونه خیلی قدیمی باشه و باعث تورم ناگهانی cwnd بشه. کرنل لینوکس این مشکل رو با ریست کردن epoch در زمان idle حل کرد. ولی وقتی این منطق به QUIC و quiche پورت شد، یه مشکل جدید بوجود اومد: در QUIC، وقتی bytes_in_flight صفر میشه (مثلاً بین دو burst پکت)، سرور بهاشتباه این حالت رو «idle» تفسیر میکنه و epoch رو ریست میکنه. این ریست شدن مکرر باعث میشه cwnd هیچوقت نتونه رشد کنه.
// شرط قبلی: epoch هر بار که bytes_in_flight == 0 ریست میشد
if self.bytes_in_flight == 0 {
self.epoch = now;
}راهحل نهایی یه اصلاح تقریباً تکخطی بود: بهجای اینکه هر بار bytes_in_flight صفر شد epoch ریست بشه، این ریست فقط در شرایطی انجام میشه که واقعاً نشانهای از idle بودن عمدی اتصال وجود داشته باشه — نه صرفاً خالی بودن لحظهای بافر در یه دانلود پرترافیک.
نکات کلیدی:
- CUBIC الگوریتم کنترل ازدحام پیشفرض لینوکس و quiche کلودفلیر هست
- باگ باعث میشد cwnd بعد از دوره اتلاف سنگین، برای همیشه روی حداقل قفل بمونه
- ۶۱٪ تستهای مرتبط شکست میخوردن، حتی بعد از توقف کامل اتلاف بسته
- ریشه مشکل: پورت نادرست یه بهینهسازی کرنل لینوکس ۲۰۱۷ به QUIC
- منطق «ریست epoch هنگام idle» در TCP درست بود، ولی در QUIC bytes_in_flight==0 لزوماً idle نیست
- راهحل یه اصلاح تقریباً تکخطی بود که چرخه بیپایان ریکاوری رو شکست




