در معماریهای نوین رله شبکه، کنترل تاخیر رفت و برگشت به اندازه ظرفیت پهنای باند اهمیت دارد. درگاههای تجاری، تونلهای پشتیبان پروتکل WireGuard مبتنی بر TCP و مراکز پایانی ارتباطات رمزنگاریشده پیوسته جریانهای داده چندگیگابیتی را هدایت کرده و همزمان باید پاسخی آنی برای بستههای تعاملی فراهم آورند. با این وجود، با جهش سرعت پیوندهای فیزیکی از ۱ گیگابیت به ۱۰۰ گیگابیت بر ثانیه، نقصی پنهان گریبانگیر کارایی سامانهها شده است: تورم بافر (bufferbloat) در صف نوشتن دادهها در پشته شبکه هسته لینوکس.
هنگامی که برنامهای از طریق اتصال TCP داده ارسال میکند، هسته لینوکس گنجایش بافر ارسال سوکت (sk->sk_sndbuf) را به شکل پویا و خودکار تنظیم مینماید. در مسیرهای بینالمللی با حاصلضرب پهنای باند در تاخیر (BDP) بسیار بالا، هسته بافر ارسال را تا ۴، ۸ یا حتی ۱۶ مگابایت به ازای هر سوکت گسترش میدهد. با آنکه این گنجایش بالا موجب اشباع ظرفیت خط میگردد، گلوگاهی ساختاری میآفریند: برنامههای فضای کاربری مگابایتها داده فشردهنشده را بسیار سریعتر از توان خروج فیزیکی شبکه، به صف هسته سرازیر میکنند.
این بافرسازی لجامگسیخته صفهای راکد و طویلی در حافظه هسته پدید میآورد. چنانچه بستهای با اولویت بسیار بالا—نظیر فشرده شدن کلیدی در ترمینال، بسته صوتی مکالمه یا پیام دستدادن برای تعویض کلیدهای رمزگذاری—قصد عبور داشته باشد، پشت مگابایتها داده مربوط به بارگیری فایل معطل میماند. جهت حذف این تاخیر مصنوعی و بازگرداندن هدایت جریان داده به برنامه، هسته لینوکس گزینه TCP_NOTSENT_LOWAT را تعبیه کرده است.
چالش صف نوشتن داده: رشد بیرویه BDP و پدیده تورم بافر در هسته
علت ریشهای تورم بافر در سمت ارسال، عدم پیوند مستقیم میان حلقههای پردازش رویداد در فضای کاربر و ماشینهای حالت TCP در هسته است. یک فرآیند ناهمگام (مبتنی بر epoll یا io_uring) به محض آنکه سوکت اعلام نماید ظرفیت پذیرش داده دارد (EPOLLOUT)، اقدام به نوشتن بایتها میکند:
- محاسبه بافر در هسته: هسته طبق قاعده
sk_sndbuf = 2 * BDPتا ۱۶ مگابایت فضا اختصاص میدهد تا پنجره ارسال هرگز با مانع روبرو نگردد. - ارسال سیلآسای برنامه: از آنجا که هسته سوکت را تا پیش از پر شدن سقف
sk_sndbufآماده نوشتن اعلام میکند، برنامه تمامی دادههای موجود در حافظه خود را به صف هسته تحویل میدهد. - انباشت صفهای راکد: دادههایی که نمیتوانند بیدرنگ روی کابل فرستاده شوند (به دلیل زمانبندی آهنگ ارسال pacing یا ظرفیت پنجره دریافت طرف مقابل)، در صف نوشتن هسته (
sk_write_queue) متراکم میشوند.
صف نوشتن در سوکتهای سنتی بدون اعمال TCP_NOTSENT_LOWAT:
[ حلقه رویداد برنامه کاربردی ] ──► write(16 مگابایت)
│
▼
[ بافر سوکت در هسته: sk_sndbuf (اختصاص ۱۶ مگابایت) ]
┌─────────────────────────────────────────────────────────────┬─────────────┐
│ دادههای ارسالنشده معطل در صف هسته (۱۵٫۵ مگابایت انتظار) │ دادههای │
│ (ایجاد تاخیر شدید و مسدودسازی سر صف برای بستههای اضطراری) │ در حال عبور │
└─────────────────────────────────────────────────────────────┴─────────────┘
│
▼ (تخلیه تدریجی بر اساس کشش خط)
[ کارت شبکه فیزیکی ]
انباشته شدن این حجم از دادههای راکد دو آسیب مهم به دنبال دارد:
- مسدودسازی سر صف (Head-of-Line Blocking): برنامه کاربری امکان بازبینی، اولویتبندی دوباره یا ارسال فوری بستههای حساس را از دست میدهد زیرا بایتها در انحصار صف هسته قرار گرفتهاند.
- اتلاف تصاعدی حافظه رم: چنانچه یک سرور میزبانی ۲۰٫۰۰۰ ارتباط فعال داشته باشد و هر سوکت ۸ مگابایت داده راکد نگهداری کند، تا ۱۶۰ گیگابایت حافظه موقت صرفاً برای نگهداری صفهای منفعل پایمال میگردد.
سازوکار درونی TCP_NOTSENT_LOWAT: مکانیک هسته و سیگنالهای سوکت
گزینه TCP_NOTSENT_LOWAT که توسط اریک دومازه توسعه یافته، شرط آماده به نوشتن بودن سوکت را دگرگون میسازد. هسته به جای سنجش کل فضای تهی در sk_sndbuf، حجم بایتهای ارسالنشدهای (unsent bytes) را که اکنون در صف جا ماندهاند بررسی میکند.
هسته بایتهای ارسالنشده را تفاوت میان کل بایتهای نوشتهشده و بایتهای مخابرهشده در شبکه تعریف مینماید:
$$\text{بایتهای ارسالنشده} = \text{write_seq} - \text{snd_nxt}$$
هنگامی که این متغیر روی مقداری معین (مثلاً ۱۶ یا ۳۲ کیلوبایت) تنظیم شود، هسته تا زمانی که حجم دادههای مانده در صف بیش از این آستانه باشد، انتشار رویداد EPOLLOUT را متوقف میسازد.
روند تنظیم جریان خروجی با TCP_NOTSENT_LOWAT = ۱۶ کیلوبایت:
[ بافرهای برنامه کاربردی ] ── (نگهداری در فضای کاربری برای اولویتبندی بهنگام)
│
│ 1. حلقه رویداد در انتظار سیگنال EPOLLOUT میماند
▼
[ صف سوکت در هسته: sk_write_queue ]
┌──────────────────────────────────────────────┬────────────────────────────┐
│ باقیمانده ارسالنشده: مهارشده زیر ۱۶ کیلوبایت│ دادههای در حال عبور از خط│
│ (تزریق به موقع و متناسب دادهها به هسته) │ (تکمیل کامل ظرفیت لوله) │
└──────────────────────────────────────────────┴────────────────────────────┘
│
▼ (انتقال پیوسته به سمت درگاه فیزیکی)
[ رابط فیزیکی شبکه ]
1. تفکیک گنجایش بافر سوکت از انباشت دادههای ارسالنشده
نکته بنیادین این است که TCP_NOTSENT_LOWAT به هیچ روی اندازه sk_sndbuf را کوچک نکرده و مانعی پیش پای پنجره ازدحام (snd_cwnd) نمیگذارد. پشته شبکه همچنان مجاز است حجم انبوهی از دادهها را در لولههای ارتباطی میانقارهای با BDP بالا در جریان نگه دارد.
این راهکار ظرفیت پنجره انتقال را از رفتار شتابزده برنامه در پیشانباشت دادهها جدا میسازد. هسته تایید دریافت بستهها را میسنجد و اندازه لوله را متناسب میسازد، اما فضای کاربر تنها هنگامی بستههای تازه تحویل هسته میدهد که قطعات پیشین ارسال شده باشند.
2. سیگنالدهی مبتنی بر رخداد EPOLLOUT و تنظیم پویای جریان
در بطن پیادهسازی شبکه لینوکس (net/ipv4/tcp_output.c) تابع tcp_write_xmit() ژرفای صف دادهها را رصد میکند:
/* ارزیابی وضعیت در تابع داخلی tcp_stream_memory_free */
static inline bool tcp_stream_memory_free(const struct sock *sk)
{
const struct tcp_sock *tp = tcp_sk(sk);
u32 notsent = tp->write_seq - tp->snd_nxt;
return notsent < tp->notsent_lowat;
}
هر بار که کنترلکننده شبکه فیزیکی بستهها را پخش کرده و اشارهگر snd_nxt را پیش میبرد، حجم دادههای راکد کاهش مییابد. به محض افت مقدار notsent به زیر خط tp->notsent_lowat، هسته صف انتظار سوکت را فراخوانده و به epoll_wait() پیام میدهد که امکان نوشتن دادههای تازه فراهم است.
موازنه میان تاخیر و حافظه: تنظیم عمومی در برابر پیکربندی مجزای هر سوکت
مدیران شبکه میتوانند این سقف را به شکل فراگیر در کل سرور مستقر کنند یا آن را برای تکتک سوکتها در کدهای برنامه تعریف نمایند.
سلسلهمراتب تنظیم صف ارسال دادههای TCP:
[ متغیر سراسری در هسته Sysctl ]
net.ipv4.tcp_notsent_lowat = 16384 (مقدار مبنا برای سراسر سامانه)
│
├─► اعمال مستقیم روی سوکتهای عادی شبکه
│
└─► تغییر انحصاری در سطح برنامه با setsockopt():
setsockopt(fd, SOL_TCP, TCP_NOTSENT_LOWAT, &val, sizeof(val))
1. اعمال فراگیر در سیستم با متغیر net.ipv4.tcp_notsent_lowat
راحتترین راهکار برای دور نگاه داشتن سرور از تورم بافر، تغییر پارامتر عمومی sysctl است:
# بررسی تنظیمات فعلی هسته (غالباً برابر 0xFFFFFFFF یعنی غیرفعال)
cat /proc/sys/net/ipv4/tcp_notsent_lowat
# تثبیت سقف ۱۶ کیلوبایت برای تمام سوکتهای TCP در سیستم
sudo sysctl -w "net.ipv4.tcp_notsent_lowat = 16384"
مقداردهی tcp_notsent_lowat = 16384 سقفی ۱۶ کیلوبایتی برای انباشت داده در هر اتصال قرار میدهد و مانع از هدررفت منابع حافظه توسط فرآیندهای پسزمینه میگردد.
2. کنترل دقیق هر سوکت در گرههای رله چندکاربره
در گرههای شلوغ شبکه، انتساب دستی این پارامتر درون ساختار نرمافزار انعطاف بینظیری به همراه دارد. سامانه میتواند سقف محدودی (مثل ۴ یا ۸ کیلوبایت) برای نشستهای حساس در نظر بگیرد و در عین حال ۳۲ یا ۶۴ کیلوبایت برای دریافت فایلهای کلان تعیین کند:
int notsent_limit = 16384; /* تعیین سقف ۱۶ کیلوبایت برای دادههای منتظر */
if (setsockopt(client_fd, SOL_TCP, TCP_NOTSENT_LOWAT, ¬sent_limit, sizeof(notsent_limit)) < 0) {
perror("خطا در تنظیم TCP_NOTSENT_LOWAT");
return -1;
}
الگوی عملیاتی: پیادهسازی TCP_NOTSENT_LOWAT در سرویسهای متراکم
تطبیق یک پروکسی ناهمگام با معماری TCP_NOTSENT_LOWAT مستلزم هماهنگی ظریف مدیریت حافظه در فضای کاربر با چرخه رویدادها است:
- حفظ داده در حافظه برنامه: پرهیز از سپردن یکباره دادههای حجیم به سوکت. دادههای آماده باید در بافرهای محلی فرآیند باقی بمانند.
- پیروی بی چونوچرا از سیگنال EPOLLOUT: با دریافت
EPOLLOUT، تنها یک بخش معین تا مرز منطقی بعدی ارسال شده و فرآیند سریعاً به حلقه رویداد بازمیگردد. - سازگاری با حالت حساس به لبه (Edge-Triggered): هنگام بهرهگیری از
EPOLLET، حلقه نوشتن تا زمان دریافت خطایEAGAINیاEWOULDBLOCKتکرار میشود که نشان از رسیدن به آستانه مجاز دارد.
نمودار جریان حلقه رویداد برای ارسال داده با کمترین تاخیر:
[ دادههای جدید آماده ارسال هستند ] ──► بررسی صف اولویت داخلی برنامه
│
▼
آیا سوکت آماده نوشتن است (EPOLLOUT)؟
/ \
(بله) (خیر)
/ \
▼ ▼
ارسال بخشی از دادهها به سوکت انتظار برای رویداد epoll
│
▼
آیا تابع مقدار EAGAIN داد؟
/ \
(بله) (خیر)
/ \
▼ ▼
حذف نشانگر آمادگی ادامه ارسال یا بازگشت به چرخه
مزایای تسهیم همزمان: حذف مسدودسازی سر صف در جریانهای داده
برای پروتکلهایی که چند جریان مستقل را درون یک اتصال واحد TCP تجمیع میکنند (همچون HTTP/2، پروتکل gRPC و تونلهای VPN)، ویژگی TCP_NOTSENT_LOWAT مزیتی سرنوشتساز ارائه میدهد: مهار کامل انسداد میان لایهای.
فرض کنید تونلی شرکتی همزمان دادههای پشتیبان حجیم و دستورات محیط متنی ترمینال را عبور میدهد:
- در غیاب TCP_NOTSENT_LOWAT: دادههای پشتیبان بیدرنگ ۸ مگابایت بافر هسته را پر میکنند. با فشردن یک کلید در ترمینال، بسته متناظر در انتهای صف ۸ مگابایتی محبوس شده و با ۲۰۰ میلیثانیه تاخیر مصنوعی به مقصد میرسد.
- با اتکا به TCP_NOTSENT_LOWAT: در هسته همواره حداکثر ۱۶ کیلوبایت داده راکد وجود دارد. با صدور فرمان متنی، زمانبند برنامه بلافاصله آن را به صدر صف خود میبرد. در کمتر از چند میکروثانیه و به محض مخابره بخش ۱۶ کیلوبایتی جاری، داده اضطراری روانه خط میگردد.
معماری RiceGuard: مدیریت تطبیقی بافر در گرههای شبکه با ظرفیت بالا
پلتفرم VPNRice قابلیت TCP_NOTSENT_LOWAT را در هسته توزیع دادههای پروتکل RiceGuard یکپارچه نموده است. برای کاربران در حال رفتوآمد با تلفن همراه، رلههای شرکتی و سامانههای رسانهای مبتنی بر بسترهای TCP، پیشگیری از تورم بافر ضامن تجربهای پیوسته است.
ماژول پایشگر RiceGuard به شکل پیوسته وضعیت خط—از جمله برآورد لحظهای RTT، ضربآهنگ توزیع و کشش شبکه کلاینت—را بررسی میکند. هسته سامانه حد TCP_NOTSENT_LOWAT را به صورت خودکار بین ۸ تا ۳۲ کیلوبایت تنظیم مینماید تا پیوندها همواره در سقف ظرفیت کار کرده و تاخیر صف در محدوده زیر یک میلیثانیه مهار شود. همراهی تنظیمات پیشرفته هسته لینوکس با اولویتبندی هوشمند، سرعت واکنشی استثنایی و حفظ کامل حریم خصوصی را برای کاربران سرتاسر جهان تضمین مینماید.



