گسترش مقیاس سرورهای VPN برای تبادل ترافیک چند گیگابیتی، مستلزم توزیع بهینه میلیون‌ها دیتاگرام رمزشده UDP روی ده‌ها هسته پردازشی است. در معماری سنتی شبکه، رشته‌های پردازشی متصل به یک درگاه شنود منفرد برای دریافت بسته‌ها درون یک صف متمرکز با یکدیگر رقابت می‌کنند که این امر باعث ایجاد قفل‌های چرخشی مکرر (spinlocks) و گلوگاه همگام‌سازی می‌گردد.

با اینکه ویژگی SO_REUSEPORT در لینوکس اجازه می‌دهد چندین سوکت مستقل به یک آدرس و درگاه یکسان متصل شوند، سازوکار پیش‌فرض هسته مبتنی بر هش ثابت ۴ مقداری (آی‌پی مبدا، پورت مبدا، آی‌پی مقصد، پورت مقصد) است. در شبکه‌های رمزشده سرعت بالا، این مدل انعطاف‌ناپذیر با چالش‌های ساختاری روبرو می‌شود: پیوستگی نشست‌ها هنگام جابه‌جایی شبکه قطع می‌شود، دسترسی نامتعادل به حافظه در ساختار چندگره‌ای NUMA رخ می‌دهد و خطوط حافظه پنهان L3 میان پردازنده‌ها مدام پاک می‌شوند.

برای برطرف ساختن این موانع، مهندسان شبکه لینوکس از ترکیب SO_REUSEPORT با برنامه‌های انتخاب سوکت eBPF استفاده می‌کنند (BPF_PROG_TYPE_SK_REUSEPORT). با اجرای منطق برنامه‌پذیر هدایت بسته‌ها دقیقاً در مسیر جستجوی سوکت هسته، سرورها به پیوستگی قطعی نشست‌ها، توازن بار بدون نیاز به قفل و حفظ موقعیت داده‌ها در گره محلی NUMA دست می‌یابند.

کاستی‌های روش هش پیش‌فرض SO_REUSEPORT

در هسته لینوکس، دیتاگرام‌های ورودی منطبق بر گروه SO_REUSEPORT بر اساس الگوریتم reciprocal_scale(skb_get_hash(skb), num_sockets) بین سوکت‌ها پخش می‌شوند. هرچند این سازوکار برای وب معمولی کفایت می‌کند، اما در تونل‌های VPN با سه اختلال عمده همراه است:

دیتاگرام ورودی UDP
       │
       ▼
┌─────────────────────────────────┐
│ محاسبه هش ۴ مقداری هسته         │
│ (src_ip, src_port, dst_ip, dst) │
└──────────────┬──────────────────┘
       │
       ├─────────────────┬─────────────────┐
       ▼                 ▼                 ▼
┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│ سوکت ۰       │  │ سوکت ۱       │  │ سوکت ۲       │
│ هسته ۰ (NUMA0)│ │ هسته ۴ (NUMA0)│ │ هسته ۸ (NUMA1)│
└──────────────┘  └──────────────┘  └──────┬───────┘
                                           │  <--- تاخیر ناشی از گذرگاه بین گره‌ها
                                           ▼       (نامعتبر شدن کش L3)
                                    حافظه کش L3 دوردست

۱. جابه‌جایی کاربر و تخصیص مجدد پورت در NAT: کاربران دستگاه‌های همراه مکرراً بین Wi-Fi و اینترنت داده تغییر وضعیت می‌دهند یا پورت مبدا آن‌ها توسط گیت‌وی NAT اپراتور دگرگون می‌شود. با تغییر پورت یا آی‌پی، هش هسته فوراً دگرگون شده و بسته‌های یک نشست فعال را به هسته و سوکت دیگری می‌فرستد. سوکت جدید کلیدهای رمزنگاری را در کش محلی خود ندارد و فرایند با تاخیر همگام‌سازی یا تکرار مجدد ارتباط مواجه می‌شود. ۲. تاخیر دسترسی به حافظه در معماری NUMA: در سرورهای چندپردازنده‌ای، کارت شبکه مستقیماً به یک پردازنده خاص متصل است (گره NUMA 0). زمانی که هش هسته بسته‌ای را به رشته‌ای در گره NUMA 1 می‌فرستد، تمام تراکنش‌های حافظه باید از گذرگاه ارتباطی پردازنده‌ها (UPI) عبور کند که تاخیر پردازش هر بسته را از ۸۰۰ نانوثانیه به بالای ۳.۲ میکروثانیه می‌رساند. ۳. توزیع نامتوازن بار ترافیکی: توزیع هش ماهیت احتمالی دارد. اگر چند جریان داده سنگین به طور هم‌زمان به یک هسته اختصاص یابند، آن هسته اشباع شده و بسته‌ها را از دست می‌دهد، در حالی که هسته‌های دیگر در حالت انتظار باقی مانده‌اند.

ساختار هدایت برنامه‌پذیر با BPF_PROG_TYPE_SK_REUSEPORT

نوع برنامه BPF_PROG_TYPE_SK_REUSEPORT در هماهنگی با آرایه نگاشت BPF_MAP_TYPE_REUSEPORT_SOCKARRAY، جایگزین تابع هش ایستا با معیارهای دلخواه مهندسان می‌گردد.

دیتاگرام رمزشده ورودی
       │
       ▼
┌────────────────────────────────────────────────────────┐
│ طبقه‌بند eBPF: نوع BPF_PROG_TYPE_SK_REUSEPORT           │
│ - بررسی محتوای بسته UDP (شناسه نشست دریافتی)           │
│ - خواندن smp_processor_id() برای همگرایی با NUMA       │
│ - فراخوانی تابع کمکی bpf_sk_select_reuseport()         │
└──────────────────────────┬─────────────────────────────┘
                           │
                           ▼
          ┌──────────────────────────────────┐
          │ BPF_MAP_TYPE_REUSEPORT_SOCKARRAY │
          └────────────────┬─────────────────┘
                           │
       ┌───────────────────┴───────────────────┐
       ▼                                       ▼
┌──────────────┐                        ┌──────────────┐
│ هسته کاری ۰  │ (گره محلی NUMA 0)      │ هسته کاری ۱  │ (گره محلی NUMA 0)
│ شناسه سوکت ۰ │                        │ شناسه سوکت ۱ │
└──────────────┘                        └──────────────┘

هنگامی که بسته‌ای به مرحله شناسایی سوکت UDP می‌رسد، هسته پیش از ارسال آن به صف، برنامه eBPF پیوست‌شده را صدا می‌زند. اطلاعات ورودی شامل زمینه struct sk_reuseport_md است:

  • skb->data و skb->data_end: اشاره‌گرهای مستقیم به سرایند و بدنه داده.
  • sk: سوکت منتخب اولیه.
  • ip_protocol: پروتکل لایه انتقال (UDP یا TCP).
  • bind_inany: وضعیت اتصال پورت.

برنامه eBPF با اجرای تابع کمکی bpf_sk_select_reuseport(md, &reuseport_map, &key, flags)، سوکت مورد نظر در آرایه نگاشت را به صورت آنی مشخص می‌کند.

پیاده‌سازی هسته: هدایت بر پایه شناسه نشست WireGuard

در پیاده‌سازی‌های مبتنی بر پروتکل WireGuard، هر بسته داده رمزشده شامل یک شناسه ۳۲ بیتی receiver_index در بایت چهارم محتوای UDP است که نشان‌دهنده نشست متصل طرف مقابل است.

برنامه eBPF زیر نحوه خواندن این شناسه و ارسال مستقیم بسته به هسته پردازشی واقع بر گره محلی NUMA را نشان می‌دهد:

#include <linux/bpf.h>
#include <linux/filter.h>
#include <bpf/bpf_helpers.h>

#define WG_DATA_PACKET_TYPE 4
#define NUMA_NODE_CORES 16

struct {
    __uint(type, BPF_MAP_TYPE_REUSEPORT_SOCKARRAY);
    __uint(max_entries, 64);
    __type(key, __u32);
    __type(value, __u32);
} reuseport_sock_map SEC(".maps");

SEC("sk_reuseport")
int select_vpn_socket(struct sk_reuseport_md *ctx)
{
    void *data = ctx->data;
    void *data_end = ctx->data_end;
    
    /* بررسی مرزهای حافظه برای طول استاندارد داده UDP */
    if (data + 8 > data_end)
        return SK_PASS;
        
    __u8 packet_type = *(__u8 *)data;
    __u32 target_socket_index = 0;
    
    if (packet_type == WG_DATA_PACKET_TYPE) {
        /* استخراج شناسه ۳۲ بیتی receiver_index در بایت چهارم */
        __u32 receiver_index = *(__u32 *)(data + 4);
        
        /* حفظ پیوستگی نشست و هدایت به هسته متناظر */
        target_socket_index = receiver_index % NUMA_NODE_CORES;
    } else {
        /* بسته‌های آغازین به پردازنده جاری واگذار می‌شوند */
        __u32 current_cpu = bpf_get_smp_processor_id();
        target_socket_index = current_cpu % NUMA_NODE_CORES;
    }
    
    /* انتخاب سوکت بدون هیچ‌گونه قفل نرم‌افزاری */
    int ret = bpf_sk_select_reuseport(ctx, &reuseport_sock_map, &target_socket_index, 0);
    if (ret == 0)
        return SK_PASS;
        
    return SK_DROP;
}

char _license[] SEC("license") = "GPL";

با این روش، داده‌های یک نشست همیشه روی یک هسته اختصاصی پردازش می‌شوند حتی اگر نشانی کاربر در شبکه عوض شود. همچنین بسته‌های برقراری ارتباط در همان هسته‌ای که وقفه سخت‌افزاری کارت شبکه را دریافت کرده پردازش شده و کارایی کش L1/L2 را حفظ می‌کنند.

اتصال از فضای کاربر با استفاده از SO_ATTACH_REUSEPORT_EBPF

برای راه‌اندازی، برنامه کاربر سوکت‌ها را تشکیل داده و کدهای کامپایل‌شده eBPF را از طریق فراخوانی‌های استاندارد فعال می‌کند:

int setup_reuseport_cluster(int epoll_fd, int bpf_prog_fd, int sock_map_fd)
{
    int num_cores = 16;
    
    for (int i = 0; i < num_cores; i++) {
        int sfd = socket(AF_INET, SOCK_DGRAM, 0);
        
        int opt = 1;
        setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
        
        struct sockaddr_in addr = {
            .sin_family = AF_INET,
            .sin_port = htons(51820),
            .sin_addr.s_addr = htonl(INADDR_ANY)
        };
        bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
        
        /* ذخیره شناسه سوکت در آرایه نگاشت eBPF */
        __u32 key = i;
        bpf_map_update_elem(sock_map_fd, &key, &sfd, BPF_ANY);
        
        /* اتصال فیلتر eBPF به نخستین سوکت گروه */
        if (i == 0) {
            setsockopt(sfd, SOL_SOCKET, SO_ATTACH_REUSEPORT_EBPF, 
                       &bpf_prog_fd, sizeof(bpf_prog_fd));
        }
    }
    return 0;
}

با اتصال به نخستین سوکت، هسته لینوکس تصمیم‌گیری‌های eBPF را روی کلیه سوکت‌های هم‌گروه SO_REUSEPORT اعمال می‌کند.

به‌کارگیری در زیرساخت Anycast سامانه RiceGuard

سامانه RiceGuard بر بستر شبکه توزیع‌شده Anycast از سازوکار BPF_PROG_TYPE_SK_REUSEPORT برای بهره‌وری حداکثری هسته‌ها استفاده می‌کند:

  • حذف کامل تبادل داده بین پردازنده‌ها: حلقه‌های پردازش رمزنگاری در RiceGuard به هسته‌هایی پیوند خورده‌اند که به گذرگاه PCIe همان کارت شبکه ۱۰۰ گیگابیت دسترسی مستقیم دارند. رمزگشایی و هدایت بسته‌ها تماماً در حافظه کش محلی L3 تکمیل شده و زمان تاخیر را تا ۶۰ درصد کاهش می‌دهد.
  • تثبیت نشست در شرایط جابه‌جایی شبکه: با تغییر دکل مخابراتی یا اتصال شبکه، فیلتر eBPF بسته را بر اساس شناسه به رشته در حال اجرا می‌رساند و مانع از قطع نشست می‌گردد.
  • به‌روزرسانی نرم‌افزار بدون قطعی ارتباط: در جریان ارتقای نگارش نرم‌افزار سرور، سوکت‌های نسخه تازه در جدول eBPF جایگزین می‌شوند در حالی که سوکت‌های پیشین ترافیک جاری را به پایان می‌رسانند و بدین ترتیب حتی یک بسته نیز دور ریخته نمی‌شود.

همگرایی SO_REUSEPORT و فناوری برنامه‌پذیر eBPF، مقیاس‌پذیری پایدار و ایزولاسیون کامل ترافیک را در زیرساخت‌های نوین WireGuard و RiceGuard تضمین می‌کند.