گلوگاه تک‌صفی در رابط‌های شبکه مجازی

تمرکزدهنده‌های بزرگ VPN در مقیاس سازمانی، گیت‌وی‌های مبتنی بر نرم‌افزار و مسیریاب‌های لبه شبکه، حجم عظیمی از داده‌ها را روی بسترهای پرظرفیت 40 GbE و 100 GbE جابه‌جا می‌کنند. با وجود اینکه کارت‌های شبکه فیزیکی (NIC) سال‌هاست از صف‌های سخت‌افزاری چندگانه (Receive Side Scaling / RSS) بهره می‌برند، رابط‌های مجازی در فضای کاربر اغلب به دلیل پیکربندی‌های قدیمی در صف‌های تک‌مسیره متوقف می‌مانند.

در سیستم‌عامل لینوکس، راهکار استاندارد برای تبادل بسته‌های IP در لایه ۳ میان پردازه‌های فضای کاربر و پشته شبکه هسته، درایور Universal TUN/TAP با مسیر /dev/net/tun است. در معماری‌های متداول گذشته، پردازه شبکه تنها یک بار /dev/net/tun را باز کرده، یک توصیف‌گر فایل (file descriptor) به دست آورده و یک رابط شبکه مانند tun0 ایجاد می‌کرد.

هنگامی که پهنای باند از مرز چندین گیگابیت بر ثانیه عبور می‌کند، این رویکرد تک‌صفی با محدودیت‌های بنیادین مواجه می‌شود:

  1. انحصار پردازش روی هسته شماره ۰: به دلیل اینکه تنها یک توصیف‌گر فایل وظیفه خواندن و نوشتن بسته‌ها را بر عهده دارد، تمام پردازش ترافیک روی همان هسته پردازنده‌ای متمرکز می‌شود که رشته ورودی/خروجی را اجرا می‌کند. حتی در سروری با ۶۴ هسته فیزیکی، هسته ۰ به اشباع ۱۰۰ درصدی می‌رسد در حالی که ۶۳ هسته دیگر کاملاً بیکار می‌مانند.
  2. رقابت شدید بر سر قفل‌های اسپین‌لاک در tun.c: در رابط TUN تک‌صفی، تمام عملیات ارسال و دریافت از طریق یک قفل اسپین‌لاک واحد هسته (tun->lock) همگام‌سازی می‌شوند. تلاش همزمان چند نخ برای خواندن یا نوشتن روی این توصیف‌گر، موجب تعویض زمینه مکرر و اتلاف سیکل‌های پردازنده می‌گردد.
  3. از بین رفتن مزیت صف‌های سخت‌افزاری کارت شبکه: کارت‌های فیزیکی پیشرفته بسته‌های ورودی را با استفاده از الگوریتم هش Toeplitz بین چندین حلقه دریافت (RX rings) تقسیم می‌کنند. هدایت اجباری این جریان‌های موازی به یک رابط مجازی تک‌نخی، کارایی حاصل از شتاب‌دهنده‌های سخت‌افزاری را خنثی می‌سازد.

برای رفع این مانع ساختاری، از نسخه ۳.۸ هسته لینوکس قابلیت IFF_MULTI_QUEUE به زیرسیستم TUN/TAP افزوده شد.

ساختار درونی IFF_MULTI_QUEUE در هسته لینوکس

معماری چندصفی، ارتباط سنتی میان دستگاه شبکه و توصیف‌گرهای فایل را دگرگون ساخته است. به جای نگاشت انحصاری یک‌به‌یک میان struct net_device و دستگاه کاراکتری، فلگ IFF_MULTI_QUEUE اجازه می‌دهد چندین توصیف‌گر فایل مستقل (struct tun_file) به یک رابط مجازی مشترک (struct tun_struct) متصل گردند.

+-------------------------------------------------------------------------+
|                  موتور VPN چندنخی در فضای کاربر (User-Space)            |
+-------------------------------------------------------------------------+
|  نخ کاری ۰          |  نخ کاری ۱          |  ...   |  نخ کاری N-1       |
|  (متصل به هسته ۰)   |  (متصل به هسته ۱)   |        |  (متصل به هسته N-1)|
+---------------------+---------------------+--------+--------------------+
       |                     |                                |
       | fd_0                | fd_1                           | fd_n-1
       v                     v                                v
+-------------------------------------------------------------------------+
|               توصیف‌گرهای فایل مستقل و مجزا (/dev/net/tun)              |
+-------------------------------------------------------------------------+
       |                     |                                |
       v خواندن/نوشتن        v خواندن/نوشتن                   v خواندن/نوشتن
+-------------------------------------------------------------------------+
|                           زیرسیستم TUN در هسته                          |
|                                                                         |
|  +--------------+   +--------------+              +--------------+      |
|  | tun_file [0] |   | tun_file [1] |     ...      | tun_file[n-1]|      |
|  | صف سوکت      |   | صف سوکت      |              | صف سوکت      |      |
|  +--------------+   +--------------+              +--------------+      |
|         \                  |                             /              |
|          \                 |                            /               |
|           v                v                           v                |
|   +-----------------------------------------------------------------+   |
|   |              struct tun_struct (net_device: tun0)               |   |
|   |         - هدایت جریان‌ها از طریق تابع skb_get_hash()             |   |
|   |         - تخصیص بدون قفل صف‌ها با استفاده از آرایه RCU          |   |
|   +-----------------------------------------------------------------+   |
+-------------------------------------------------------------------------+
       |                                                    ^
       v ارسال بسته‌ها به شبکه                              | دریافت بسته‌ها
+-------------------------------------------------------------------------+
|                   پشته مسیریابی IP و فیلترینگ هسته لینوکس                |
+-------------------------------------------------------------------------+

۱. انتخاب صف بدون قفل از طریق ساختار RCU

در سورس هسته drivers/net/tun.c، صف‌های فعال از طریق آرایه‌ای از اشاره‌گرها مدیریت می‌شوند که توسط مکانیزم Read-Copy-Update (RCU) محافظت می‌گردد: struct tun_file __rcu *tfiles[MAX_TAP_QUEUES]. هنگام ارسال بسته خروجی به سمت tun0، هسته بدون نیاز به قفل‌های مسدودکننده عمومی صف مقصد را تعیین می‌کند:

// منطق انتخاب صف در فایل tun.c درون هسته لینوکس
static struct tun_file *tun_get_queue(struct tun_struct *tun, struct sk_buff *skb)
{
    u32 hash = skb_get_hash(skb);
    u16 numqueues = READ_ONCE(tun->numqueues);
    u16 index = reciprocal_scale(hash, numqueues);
    return rcu_dereference(tun->tfiles[index]);
}

به دلیل استفاده از ایندکس RCU و عدم وجود قفل، مقیاس‌پذیری هدایت بسته‌ها به شکل خطی همراه با افزایش تعداد هسته‌ها رشد می‌کند.

۲. هدایت متقارن جریان‌های شبکه

برای جلوگیری از بی‌نظمی در تحویل بسته‌ها در اتصالات TCP یا UDP، هسته با استفاده از skb_get_hash() یک هش متقارن بر اساس ۴ فیلد کلیدی سرآیند محاسبه می‌نماید. تمام بسته‌های مربوط به یک اتصال مشخص همواره به یک صف ثابت هدایت می‌شوند؛ بدین ترتیب نظم بسته‌ها حفظ شده و تونل‌های مجزا بین صف‌های مختلف توزیع می‌گردند.

پیاده‌سازی تخصیص چندصفی در زبان C

برای راه‌اندازی رابط TUN چندصفی، نرم‌افزار در فضای کاربر باید /dev/net/tun را به ازای هر نخ پردازشی باز کند. سپس هر نخ دستور ioctl(TUNSETIFF) را با نام دستگاه مشترک و فلگ IFF_MULTI_QUEUE فراخوانی می‌نماید.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/if.h>
#include <linux/if_tun.h>
#include <errno.h>

/**
 * اتصال یک توصیف‌گر فایل مستقل به دستگاه TUN چندصفی
 * @param dev نام رابط شبکه (برای مثال "tun0")
 * @return توصیف‌گر ایجادشده در صورت موفقیت، یا -1 در صورت بروز خطا
 */
int tun_alloc_mq(const char *dev)
{
    struct ifreq ifr;
    int fd;

    // باز کردن یک نمونه مستقل از دستگاه کاراکتری /dev/net/tun
    if ((fd = open("/dev/net/tun", O_RDWR | O_NONBLOCK)) < 0) {
        perror("خطا در باز کردن /dev/net/tun");
        return -1;
    }

    memset(&ifr, 0, sizeof(ifr));

    // تنظیم حالت TUN لایه ۳، پشتیبانی چندصفی و حذف سرآیند اطلاعات اضافی
    ifr.ifr_flags = IFF_TUN | IFF_MULTI_QUEUE | IFF_NO_PI;

    if (dev && *dev) {
        strncpy(ifr.ifr_name, dev, IFNAMSIZ - 1);
    }

    // ثبت این توصیف‌گر فایل در رابط شبکه هسته
    if (ioctl(fd, TUNSETIFF, (void *)&ifr) < 0) {
        perror("خطا در اجرای فراخوان ioctl(TUNSETIFF)");
        close(fd);
        return -1;
    }

    return fd;
}

فلگ‌های مکمل بنیادین: IFF_NO_PI و IFF_VNET_HDR

برای رسیدن به توان پردازش حداکثری، تخصیص چندصفی معمولاً با فلگ‌های زیر ترکیب می‌شود:

  • IFF_NO_PI (حذف سرآیند اطلاعات بسته): به صورت پیش‌فرض، درایور TUN یک سرآیند ۴ بایتی به هر بسته اضافه می‌کند. فعال‌سازی IFF_NO_PI این داده اضافی را حذف کرده و اجازه می‌دهد بسته‌های خام مستقیماً روی حافظه برای پردازش برداری SIMD قرار گیرند.
  • IFF_VNET_HDR (سرآیند شبکه مجازی Virtio): امکان بهره‌گیری از Generic Segmentation Offload (GSO) و Generic Receive Offload (GRO) را میان هسته و فضای کاربر فراهم می‌سازد. به جای جابه‌جایی بسته‌های جداگانه ۱۵۰۰ بایتی، برنامه می‌تواند اَبَربسته‌هایی با اندازه تا ۶۴ کیلوبایت را جابه‌جا کرده و بار فراخوان‌های سیستمی را تا ۷۵ درصد کاهش دهد.

راهکارهای کلیدی معماری برای گیت‌وی‌های چندصفی VPN

استفاده عملیاتی از IFF_MULTI_QUEUE در شبکه‌های حساس سازمانی نیازمند هماهنگی میان پارامترهای هسته، زمان‌بندی پردازنده و پشته شبکه است.

ترافیک ورودی رمزنگاری‌شده WAN (کارت فیزیکی 100GbE)
                      |
                      v توزیع سخت‌افزاری RSS
+-------------------------------------------------------------------+
|               رمزگشایی و هدایت هش بسته‌ها در هسته لینوکس          |
+-------------------------------------------------------------------+
       |                      |                      |
       v هسته ۰               v هسته ۱               v هسته ۲
+---------------+      +---------------+      +---------------+
| tun0 صف ۰     |      | tun0 صف ۱     |      | tun0 صف ۲     |
| fd_0          |      | fd_1          |      | fd_2          |
+---------------+      +---------------+      +---------------+
       |                      |                      |
       v                      v                      v
+---------------+      +---------------+      +---------------+
| اتصال به هسته ۰|     | اتصال به هسته ۱|     | اتصال به هسته ۲|
| نخ پردازشی    |      | نخ پردازشی    |      | نخ پردازشی    |
+---------------+      +---------------+      +---------------+

۱. اتصال انحصاری نخ‌ها به هسته‌های پردازنده

برای تثبیت تأخیر در سطح زیر یک میلی‌ثانیه، هر توصیف‌گر صف باید توسط یک نخ مشخص مدیریت شود که با تابع pthread_setaffinity_np() روی یک هسته اختصاصی تثبیت شده است. این کار از جابه‌جایی پرهزینه حافظه‌های نهان L1/L2 میان هسته‌ها جلوگیری می‌کند.

۲. تنظیم طول صف ارسال رابط شبکه

هر توصیف‌گر tun_file بافر دریافت مختص به خود را در هسته دارد. در شرایط هجوم ترافیک ناگهانی، طول پیش‌فرض ممکن است به افت نامحسوس بسته‌ها منجر شود. پیشنهاد می‌شود ظرفیت صف ارسال (txqueuelen) روی رابط مجازی افزایش یابد:

# افزایش طول صف ارسال بسته‌ها روی رابط چندصفی
ip link set dev tun0 txqueuelen 10000

۳. تلفیق با رویدادهای ناهمگام I/O

هر نخ کاری توصیف‌گر خود را از طریق سامانه‌های مبتنی بر لبه رویداد (epoll همراه با EPOLLET) یا حلقه‌های io_uring زیر نظر می‌گیرد. نخ‌ها بسته‌ها را به شکل دسته‌ای و با توابع برداری (readv یا recvmmsg) دریافت می‌کنند تا بافرها به موقع تخلیه گردند.

مقیاس‌پذیری مسیرهای چند‌هسته‌ای در RiceGuard

در معماری توزیع‌شده پلتفرم VPNRice، موتور پیشرفته RiceGuard از سازوکار IFF_MULTI_QUEUE برای ایجاد مسیرهای داده‌ای بدون قفل و با کارایی بالا در خوشه‌های پرتردد استفاده می‌کند.

۱. آرایش صف‌ها با درک توپولوژی NUMA: پلتفرم RiceGuard مرزهای گره‌های NUMA و کانال‌های حافظه رم را شناسایی می‌نماید. صف‌ها به طور متقارن به هسته‌هایی متصل می‌شوند که مسیر مستقیم به کنترلر محلی PCIe دارند و بدین ترتیب پهنای باند گذرگاه‌های بین‌سوکت اشغال نمی‌شود. ۲. پردازش برداری بسته‌ها بدون کپی مضاعف: با ادغام توصیف‌گرهای چندصفی با IFF_VNET_HDR و ماژول‌های بهینه‌شده رمزنگاری ChaCha20-Poly1305، موتور RiceGuard روی سخت‌افزارهای استاندارد به نرخ پردازش بیش از ۲۵ میلیون بسته در ثانیه دست می‌یابد. ۳. جداسازی کامل در فضاهای نام شبکه: هر رابط چندصفی TUN مستقیماً درون یک فضای نام شبکه مستقل (Network Namespace) ایجاد می‌گردد. این جداسازی امنیتی از خروج بسته‌های رمزنگاری‌نشده به جدول‌های عمومی یا رابط‌های خارجی جلوگیری کرده و امنیت اطلاعات را در کنار کارایی فوق‌العاده تضمین می‌کند.