Các cổng kết nối VPN doanh nghiệp và trạm chuyển tiếp biên phân tán hiện đại thường vận hành trên các máy chủ đa lõi mật độ cao trang bị 64, 128 hoặc 256 luồng xử lý vật lý. Khi phải xử lý lưu lượng tổng hợp lên tới hàng chục gigabit mỗi giây trên hàng trăm ngàn đường hầm mã hóa đồng thời, thông lượng mạng hiếm khi bị nghẽn do xung nhịp CPU thuần túy. Thay vào đó, hiệu năng suy giảm bắt nguồn từ các điểm nghẽn ở hệ thống phân cấp bộ nhớ: hiện tượng nảy dòng bộ nhớ đệm (cache-line bouncing), tắc nghẽn liên kết liên nút NUMA và chi phí ngắt liên bộ xử lý (inter-processor interrupt - IPI).

Theo cấu hình mặc định của nhân Linux, các gói tin mạng đến từ các card giao tiếp mạng đa hàng đợi (multi-queue NIC) được xử lý softirq trên một lõi CPU, nhưng sau đó lại được chuyển giao cho một luồng xử lý (worker thread) ở không gian người dùng đang chạy trên một lõi CPU hoàn toàn khác. Sự phân tách kiến trúc này làm mất hiệu lực bộ nhớ đệm L1 và L2 trên từng gói tin, buộc bus bộ nhớ phải liên tục đồng bộ trạng thái giữa các cụm lõi.

Để đạt được khả năng mở rộng tuyến tính theo số lõi và giữ độ trễ đuôi (tail latency) ổn định dưới một mili-giây, các proxy biên hiệu năng cao sử dụng hai tùy chọn socket chuyên biệt của Linux: SO_INCOMING_CPU và SO_INCOMING_NAPI_ID. Bằng cách gắn kết trực tiếp tiến trình điều phối socket với hàng đợi nhận phần cứng và lõi thực thi softirq của nhân, các cơ chế này tạo ra tính cục bộ bộ nhớ đệm không cần giao tiếp liên tiến trình (zero-IPC cache locality), biến các máy chủ biên đa lõi thành những cỗ máy chuyển tiếp song song hoàn toàn không khóa.


Rào cản mở rộng đa lõi: Xung đột bộ nhớ đệm và chi phí liên lõi

Để hiểu rõ nguyên nhân khiến proxy mạng bị nghẽn trên các máy chủ nhiều lõi, hãy xem xét hành trình vật lý của một gói tin đi qua kiến trúc phần cứng hiện đại:

  1. Tiếp nhận phần cứng (Hardware Ingress): Một gói tin VPN mã hóa đi vào cổng mạng vật lý. Mạch phân phối Receive-Side Scaling (RSS) của NIC tính toán mã băm Toeplitz từ 4-tuple hoặc 2-tuple và điều hướng khung dữ liệu vào một hàng đợi nhận phần cứng cụ thể (ví dụ RX Queue 3).
  2. Kích hoạt ngắt & Vòng lặp NAPI: NIC gửi một ngắt MSI-X, điều hướng luồng thực thi sang Lõi 3. Vòng lặp thăm dò NAPI của nhân chạy bên trong ksoftirqd/3, phân tích tiêu đề gói tin, kiểm tra tính hợp lệ của trạng thái mã hóa hoặc tiêu đề IP và xác định cấu trúc socket đích.
  3. Lấy gói khỏi hàng đợi & Phân phối tiến trình: Khi daemon proxy không gian người dùng sử dụng vòng lặp epoll tiêu chuẩn không có cơ chế ghim lõi, một luồng xử lý ngẫu nhiên đang chạy trên Lõi 11 sẽ thức dậy để đọc gói tin qua lệnh gọi recvmsg().
Tình trạng xung đột bộ nhớ đệm (Khi thiếu tính gắn kết lõi):

Phần cứng NIC (Hàng đợi RSS 3)
         │ (Ngắt MSI-X)
         ▼
[ Lõi CPU 3: Vòng lặp Softirq / NAPI ]  <── Dữ liệu gói ghi vào Cache L1/L2 của Lõi 3
         │
         │ (Điều phối chéo lõi / Đánh thức Epoll)
         ▼
[ Lõi CPU 11: Luồng xử lý User-Space ]  <── Hụt bộ nhớ đệm (Cold Cache Miss)! Buộc phải
                                            đọc qua bus bộ nhớ từ Lõi 3 hoặc RAM/L3 chung.

Khi các mô tả gói tin và dữ liệu tải trọng do Lõi 3 ghi được truy cập bởi Lõi 11, phần cứng CPU buộc phải kích hoạt các giao thức duy trì tính nhất quán bộ đệm (như MESI hoặc MOESI) trên bus liên kết liên socket hoặc AMD Infinity Fabric / Intel Ultra Path Interconnect (UPI). Trên một máy chủ xử lý 5 triệu gói tin mỗi giây, các đợt làm mất hiệu lực bộ đệm chéo lõi này sẽ làm cạn kiệt băng thông bus nội bộ, khiến độ trễ p99 tăng vọt hơn 400%.


Phân tích chuyên sâu: SO_INCOMING_CPU và tính gắn kết Softirq nhân

Tùy chọn socket SO_INCOMING_CPU (được đưa vào Linux 3.19 và hoàn thiện trong các bản phát hành tiếp theo) cung cấp cho ứng dụng không gian người dùng quyền kiểm soát tinh tế đối với việc điều phối CPU. Cơ chế này vận hành theo hai phương thức: kiểm tra socket và điều hướng kết nối đi vào.

1. Điều phối kết nối trực tiếp đến các lõi xử lý Softirq

Khi được cấu hình song song với SO_REUSEPORT, SO_INCOMING_CPU cho phép lựa chọn socket mang tính tất định. Thay vì để nhân hệ thống phân bổ ngẫu nhiên các phiên bắt tay TCP hoặc luồng UDP đồng đều trên tất cả các socket đang lắng nghe, ứng dụng khởi tạo các socket lắng nghe riêng biệt—mỗi socket dành riêng cho một lõi CPU vật lý:

int cpu_id = 4;
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);

int enable_reuseport = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEPORT, &enable_reuseport, sizeof(enable_reuseport));

/* Yêu cầu nhân chuyển các kết nối do softirq của Lõi 4 xử lý vào socket này */
setsockopt(listen_fd, SOL_SOCKET, SO_INCOMING_CPU, &cpu_id, sizeof(cpu_id));

bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr));
listen(listen_fd, 1024);

Trong quá trình gói tin đi vào, hàm tra cứu socket của nhân sẽ kiểm tra xem có socket nào trong mảng SO_REUSEPORT khớp với ID CPU hiện tại (smp_processor_id()) hay không. Nếu tìm thấy sự trùng khớp, kết nối sẽ được đưa trực tiếp vào hàng đợi chấp nhận (accept queue) của chính socket đó.

2. Loại bỏ ngắt liên bộ xử lý và hiện tượng nảy bộ nhớ đệm L3

Khi các luồng xử lý ở không gian người dùng ghim thực thi vào các lõi CPU tương ứng thông qua pthread_setaffinity_np(), chúng chỉ đọc dữ liệu từ các hàng đợi cục bộ:

  • Luồng xử lý 4 chạy cố định trên Lõi CPU 4.
  • Lõi 4 xử lý các khung mạng đi vào bên trong softirq.
  • Luồng xử lý 4 lấy các khung dữ liệu ra khỏi socket được gán SO_INCOMING_CPU = 4.

Vì bộ đệm dữ liệu vẫn nằm sẵn trong bộ nhớ đệm L1 và L2 của Lõi CPU 4, độ trễ truy xuất bộ nhớ giảm từ khoảng 60 nano-giây (khi phải nạp từ L3 từ xa hoặc RAM qua các node NUMA) xuống dưới 2 nano-giây. Các tín hiệu ngắt liên bộ xử lý (IPI) biến mất hoàn toàn khỏi đường dẫn dữ liệu.


Đồng bộ hàng đợi phần cứng: Truy vấn SO_INCOMING_NAPI_ID

Trong khi SO_INCOMING_CPU cung cấp khả năng ghim ở cấp độ lõi, các card mạng hiện đại phân chia công việc trên nhiều hàng đợi phần cứng riêng biệt. Mỗi hàng đợi phần cứng tương ứng với một thực thể NAPI duy nhất trong nhân.

1. Ánh xạ vòng đệm nhận của NIC tới luồng công việc không gian người dùng

Trong Linux 4.12, hệ thống mạng giới thiệu tùy chọn SO_INCOMING_NAPI_ID. Tùy chọn chỉ đọc này cho phép các tiến trình nền ở không gian người dùng xác định chính xác hàng đợi phần cứng đã tiếp nhận các gói tin của kết nối:

unsigned int napi_id = 0;
socklen_t optlen = sizeof(napi_id);

if (getsockopt(client_fd, SOL_SOCKET, SO_INCOMING_NAPI_ID, &napi_id, &optlen) == 0) {
    /* Điều phối client_fd tới luồng chuyên trách xử lý vòng NAPI này */
    assign_socket_to_napi_worker(client_fd, napi_id);
}

Bằng cách truy vấn ID NAPI, các gateway biên mật độ cao thiết lập mối liên kết 1:1 giữa các vòng phần cứng vật lý và các vòng lặp sự kiện không gian người dùng. Nếu card mạng 100 Gbps cung cấp 32 hàng đợi RX, proxy sẽ khởi chạy 32 luồng công việc, mỗi luồng gắn liền với một ID hàng đợi nhất định.

2. Đường dẫn gói tin đối xứng hai chiều với tích hợp XPS

Chuyển tiếp thông lượng cao đòi hỏi sự đối xứng trên cả chiều nhận (RX) và chiều gửi (TX). Khi proxy biên nhận một gói tin đóng gói, giải mã và chuyển tiếp tới giao diện mạng nội bộ, việc truyền từ một lõi CPU không đồng bộ sẽ gây ra xung đột khóa trên các hàng đợi phát TX.

Đường dẫn phần cứng đối xứng toàn diện:

[ Hàng đợi RX NIC 2 ] ──► [ Softirq Lõi 2 ] ──► [ Luồng xử lý 2 (SO_INCOMING_CPU) ]
                                                              │
                                                              ▼ (Xử lý & Giải mã)
[ Hàng đợi TX NIC 2 ] ◄── [ Hàng đợi XPS Lõi 2 ] ◄── [ Bộ đệm truyền (Không khóa tranh chấp) ]

Bằng cách kết hợp SO_INCOMING_NAPI_ID ở chiều nhận với Transmit Packet Steering (XPS) được ánh xạ vào cùng lõi CPU ở chiều gửi, gói tin di chuyển qua một đường ống khép kín từ dây cáp lên không gian người dùng rồi trở lại cáp mà không bao giờ phải vượt qua ranh giới giữa các lõi CPU.


Kiến trúc triển khai: Vòng lặp công việc ghim lõi và phân mảnh socket

Việc triển khai SO_INCOMING_CPU trong một gateway biên hướng sự kiện đòi hỏi quá trình khởi tạo có cấu trúc:

  1. Phát hiện cấu trúc tô-pô: Đọc /sys/devices/system/cpu/ và /sys/class/net/<iface>/device/numa_node để xác định cấu trúc phân cấp các miền NUMA và lõi vật lý.
  2. Khởi tạo mảng Listener: Tạo một mảng các socket lắng nghe được cấu hình với SO_REUSEPORT và SO_INCOMING_CPU ánh xạ tương ứng tới từng lõi CPU.
  3. Ghim lõi thực thi: Đối với mỗi tiến trình hoặc luồng xử lý, gọi pthread_setaffinity_np() để giới hạn phạm vi thực thi trên lõi được chỉ định.
  4. Đăng ký Epoll: Mỗi luồng chỉ đăng ký socket lắng nghe của riêng mình và các kết nối phát sinh vào thực thể epoll_create1() cục bộ.
/* Điểm nhập của luồng xử lý công việc */
void *worker_loop(void *arg) {
    int core_id = (int)(intptr_t)arg;
    
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(core_id, &cpuset);
    pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset);
    
    /* Vòng lặp epoll riêng biệt cho lõi này */
    int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listeners[core_id], &ev);
    
    while (running) {
        int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        for (int i = 0; i < nfds; i++) {
            /* Xử lý kết nối hoàn toàn trên cache L1/L2 cục bộ */
            handle_active_socket(events[i].data.fd);
        }
    }
    return NULL;
}

Phân tích thử nghiệm vi mô: Độ ổn định độ trễ và thông lượng gia tăng

Các bài kiểm tra vi mô so sánh trên hệ thống máy chủ 128 lõi AMD EPYC dưới tải lưu lượng liên tục 20 Gbps cho thấy rõ rệt tác động của tính gắn kết socket vào CPU:

So sánh hiệu năng dưới tải đa lõi quy mô lớn:

Chỉ số đánh giá               Epoll mặc định       SO_INCOMING_CPU + Ghim NAPI
────────────────────────────────────────────────────────────────────────────────
Thông lượng xử lý (Mpps)      3.4 Mpps             5.8 Mpps (+70%)
Hụt Cache L3 (Misses)         28.4%                6.2% (-78%)
Ngắt liên bộ xử lý (IPI)      142,000 / giây       1,800 / giây (-98%)
Độ trễ đuôi (p99)             14.2 ms              1.1 ms (-92%)
Tải hệ thống CPU (IPI/Lock)   34%                  7%

Bằng cách triệt tiêu hiện tượng mất hiệu lực bộ nhớ đệm chéo lõi, mức bão hòa bus bộ nhớ giảm xuống rõ rệt. Máy chủ hấp thụ các đợt tăng vọt lưu lượng bất ngờ mà không làm tràn bộ đệm vòng trong trình điều khiển NIC.


Đường dẫn dữ liệu RiceGuard: Phân lập lõi đo từ xa và chuyển tiếp tốc độ cao

VPNRice tích hợp trực tiếp SO_INCOMING_CPU và SO_INCOMING_NAPI_ID vào đường dẫn dữ liệu của các trạm chuyển tiếp RiceGuard. Khi phục vụ các đường hầm doanh nghiệp thông lượng cao và các nút biên phân tán, duy trì tốc độ xử lý gói tin có tính tất định là yêu cầu then chốt cho an ninh mạng.

Tại các trạm RiceGuard, các gói tin đi vào được lọc qua XDP trước khi đi tới các tiến trình chuyển tiếp không gian người dùng đã được ghim NUMA. Bằng cách đồng bộ hóa hàng đợi nhận phần cứng với các luồng công việc chuyên biệt, RiceGuard loại bỏ hoàn toàn các khóa đồng bộ luồng và chi phí chuyển đổi ngữ cảnh. Sự cô lập kiến trúc này bảo đảm các đợt yêu cầu kết nối quy mô lớn không thể làm gián đoạn các đường hầm người dùng đang hoạt động, mang lại tính ổn định cao, độ trễ thấp nhất quán và sự riêng tư dữ liệu nghiêm ngặt.