Mở rộng quy mô cổng VPN đa người dùng để đạt tốc độ truyền tải hàng chục gigabit mỗi giây đòi hỏi khả năng phân phối hàng triệu gói tin UDP mã hóa một cách đồng đều trên hàng chục lõi vi xử lý. Trong kiến trúc mạng truyền thống, các luồng xử lý cùng gắn với một cổng lắng nghe thường tranh chấp gói tin qua hàng đợi socket tập trung, gây tắc nghẽn khóa xoay (spinlock) và suy giảm nghiêm trọng khả năng xử lý song song.
Mặc dù tùy chọn socket SO_REUSEPORT của nhân Linux cho phép nhiều socket độc lập cùng lắng nghe trên cùng một địa chỉ IP và số hiệu cổng, cơ chế phân phối mặc định của nhân lại dựa hoàn toàn vào hàm băm 4 thành phần cố định (IP nguồn, cổng nguồn, IP đích, cổng đích). Trong môi trường mạng mã hóa hiệu năng cao, mô hình băm tĩnh này bộc lộ những hạn chế cốt tử: phá vỡ tính bám dính phiên khi thiết bị di động chuyển vùng, gây xung đột truy cập bộ nhớ liên nút NUMA (Non-Uniform Memory Access) và làm tráo đổi bộ nhớ đệm L3 giữa các socket vi xử lý.
Để giải quyết triệt để nút thắt cổ chai này, các kỹ sư mạng Linux kết hợp SO_REUSEPORT với bộ phân loại điều phối eBPF (BPF_PROG_TYPE_SK_REUSEPORT). Bằng cách thực thi logic định tuyến gói tin có thể lập trình trực tiếp ngay trong quá trình tra cứu socket của nhân, hệ thống đạt được sự bám dính phiên tuyệt đối, cân bằng tải không khóa và tối ưu hóa tính cục bộ NUMA.
Những hạn chế về quy mô của cơ chế băm mặc định SO_REUSEPORT
Trong nhân Linux tiêu chuẩn, gói tin UDP đến khớp với nhóm SO_REUSEPORT được phân bổ ngẫu nhiên theo thuật toán reciprocal_scale(skb_get_hash(skb), num_sockets). Dù vận hành tốt với các dịch vụ web phi trạng thái, mô hình này gây ra ba vấn đề kỹ thuật lớn đối với đường hầm VPN:
Gói tin UDP gửi đến
│
▼
┌─────────────────────────────────┐
│ Hàm băm 4 thành phần của nhân │
│ (src_ip, src_port, dst_ip, dst) │
└──────────────┬──────────────────┘
│
├─────────────────┬─────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Socket 0 │ │ Socket 1 │ │ Socket 2 │
│ Lõi 0 (NUMA0)│ │ Lõi 4 (NUMA0)│ │ Lõi 8 (NUMA1)│
└──────────────┘ └──────────────┘ └──────┬───────┘
│ <--- Nghẽn liên kết UPI liên nút
▼ (Mất hiệu lực dòng Cache L3)
Bộ nhớ đệm L3 từ xa
- Chuyển vùng máy khách và đổi cổng NAT: Thiết bị VPN di động thường xuyên chuyển đổi giao diện mạng (từ Wi-Fi sang mạng di động 4G/5G) hoặc bị thiết bị NAT của nhà mạng cấp phát lại cổng nguồn. Khi địa chỉ IP hoặc cổng nguồn thay đổi, hàm băm 4 thành phần lập tức đổi hướng gói tin của một phiên đang hoạt động sang một luồng xử lý hoàn toàn mới. Luồng mới không có sẵn khóa phiên trong bộ nhớ đệm cục bộ, buộc phải kích hoạt khóa đồng bộ liên luồng hoặc thực hiện đàm phán lại từ đầu.
- Độ trễ bộ nhớ liên nút NUMA: Trên các máy chủ đa vi xử lý hiện đại, card mạng PCIe kết nối vật lý trực tiếp với một socket CPU cụ thể (ví dụ Nút NUMA 0). Khi hàm băm mặc định giao gói tin cho một luồng đang chạy trên Nút NUMA 1, dữ liệu buộc phải di chuyển qua bus liên kết Ultra Path Interconnect (UPI). Quá trình này đẩy độ trễ xử lý gói từ khoảng 800 nano giây lên hơn 3,2 micro giây.
- Phân bổ tải bất đối xứng (Hiện tượng luồng dữ liệu khổng lồ): Hàm băm phân phối thuần túy theo xác suất toán học. Khi một vài phiên truyền dữ liệu dung lượng lớn vô tình bị băm vào cùng một lõi, lõi đó sẽ bị quá tải 100% dẫn tới rớt gói, trong khi các lõi xung quanh vẫn đang ở trạng thái rảnh rỗi.
Điều phối lập trình được: Kiến trúc BPF_PROG_TYPE_SK_REUSEPORT
Cơ chế BPF_PROG_TYPE_SK_REUSEPORT kết hợp cùng bản đồ bộ nhớ BPF_MAP_TYPE_REUSEPORT_SOCKARRAY cho phép thay thế hàm băm tĩnh của nhân bằng logic điều phối do lập trình viên định nghĩa.
Gói tin mã hóa gửi đến
│
▼
┌────────────────────────────────────────────────────────┐
│ Bộ phân loại eBPF: BPF_PROG_TYPE_SK_REUSEPORT │
│ - Kiểm tra tải trọng UDP (WireGuard Receiver Index/ID) │
│ - Đọc smp_processor_id() định vị lõi thuộc nút NUMA │
│ - Gọi hàm trợ giúp bpf_sk_select_reuseport() │
└──────────────────────────┬─────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ BPF_MAP_TYPE_REUSEPORT_SOCKARRAY │
└────────────────┬─────────────────┘
│
┌───────────────────┴───────────────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Luồng lõi 0 │ (Cục bộ Nút NUMA 0) │ Luồng lõi 1 │ (Cục bộ Nút NUMA 0)
│ Socket FD 0 │ │ Socket FD 1 │
└──────────────┘ └──────────────┘
Khi gói tin đi vào nhánh tra cứu socket UDP, nhân sẽ gọi chương trình eBPF đính kèm trước khi đưa gói vào bất kỳ hàng đợi nào. Chương trình nhận ngữ cảnh struct sk_reuseport_md:
skb->datavàskb->data_end: Con trỏ trực tiếp đến dữ liệu phần đầu và tải trọng gói tin.sk: Socket lắng nghe ứng viên.ip_protocol: Giao thức tầng giao vận (UDP hoặc TCP).bind_inany: Cờ trạng thái liên kết địa chỉ.
Bằng cách gọi hàm trợ giúp bpf_sk_select_reuseport(md, &reuseport_map, &key, flags), mã eBPF chỉ định chính xác chỉ số socket trong bản đồ sẽ tiếp nhận gói tin.
Cài đặt trong nhân: Định tuyến theo chỉ số phiên WireGuard
Trong các triển khai VPN chuẩn WireGuard, mỗi gói tin dữ liệu mã hóa đều chứa một trường receiver_index 32-bit tại độ lệch byte thứ 4 của tải trọng UDP. Giá trị này định danh duy nhất phiên của thiết bị đối tác.
Chương trình eBPF sau đây minh họa cách trích xuất chỉ số phiên để điều phối về đúng lõi CPU đã được gắn cố định với Nút NUMA cục bộ:
#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;
/* Kiem tra gioi han bo nho: kich thuoc toi thieu cua tai trong 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) {
/* Trich xuat chi so receiver_index 32-bit o byte thu 4 */
__u32 receiver_index = *(__u32 *)(data + 4);
/* Bao dam tinh bam dinh phien voi nhom loi worker */
target_socket_index = receiver_index % NUMA_NODE_CORES;
} else {
/* Goi tin bat tay: phan phoi toi loi CPU hien tai */
__u32 current_cpu = bpf_get_smp_processor_id();
target_socket_index = current_cpu % NUMA_NODE_CORES;
}
/* Lua chon socket khong can khoa dong bo */
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";
Nhờ giải thuật này, các gói tin dữ liệu thuộc cùng một phiên liên lạc luôn đi vào đúng một lõi xử lý duy nhất dù IP hay cổng nguồn máy khách có thay đổi. Đồng thời, các gói tin bắt tay khởi tạo phiên được xử lý ngay tại lõi đang tiếp nhận ngắt phần cứng của card mạng, duy trì độ nóng tối đa của bộ nhớ đệm L1/L2.
Khởi tạo và đính kèm từ không gian người dùng
Để kích hoạt bộ điều phối eBPF, tiến trình dịch vụ khởi tạo nhóm socket và gắn chương trình eBPF thông qua giao diện lập trình socket POSIX chuẩn:
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));
/* Luu file descriptor cua socket vao ban do sockarray */
__u32 key = i;
bpf_map_update_elem(sock_map_fd, &key, &sfd, BPF_ANY);
/* Dinh kem chuong trinh eBPF vao socket dau tien cua nhom */
if (i == 0) {
setsockopt(sfd, SOL_SOCKET, SO_ATTACH_REUSEPORT_EBPF,
&bpf_prog_fd, sizeof(bpf_prog_fd));
}
}
return 0;
}
Ngay khi gắn vào socket đầu tiên, nhân Linux sẽ tự động áp dụng bộ điều hướng eBPF cho toàn bộ các socket thuộc cụm SO_REUSEPORT.
Ứng dụng thực tiễn trong hạ tầng RiceGuard
Hệ thống Anycast phân tán của RiceGuard triển khai BPF_PROG_TYPE_SK_REUSEPORT nhằm loại bỏ triệt để xung đột tài nguyên giữa các lõi:
- Xóa bỏ nghẽn liên nút NUMA: RiceGuard ghim chặt các luồng giải mã mã hóa vào tập lõi CPU chia sẻ chung bus PCIe với card mạng 100G. Gói tin được phân tích, giải mã và điều hướng hoàn toàn bên trong cây nhớ đệm L3 cục bộ, giảm hơn 60% độ trễ xử lý.
- Duy trì phiên chuyển vùng mượt mà: Khi người dùng di động di chuyển giữa các trạm phát sóng di động, bộ phân loại eBPF của RiceGuard đối chiếu mã định danh phiên và chuyển tiếp gói tin tới đúng luồng đang chạy mà không cần đàm phán lại phiên mã hóa.
- Nâng cấp phần mềm không gián đoạn: Trong quá trình cập nhật phần mềm cổng định tuyến, các socket mới được bổ sung vào bản đồ
REUSEPORT_SOCKARRAYtrong khi các tiến trình cũ dần xả hết kết nối hiện hữu, đảm bảo tỷ lệ rớt gói bằng 0.
Bằng việc kết hợp SO_REUSEPORT với khả năng điều hướng gói tin lập trình được của eBPF, kiến trúc mạng WireGuard và RiceGuard đạt được hiệu suất đa lõi vượt trội cùng khả năng cô lập lưu lượng đa người dùng chặt chẽ.



