1. 引言
1.1 问题背景
现代 KV 存储(如 Redis、TiKV)在高并发下主要瓶颈不在 CPU 算力,而在:
内核网络栈路径过长(TCP → IP → 软中断 → socket 唤醒)
系统调用频繁(read / write / epoll)
内存拷贝次数多(网卡 → 内核 → 用户态)
eBPF 的 XDP(eXpress Data Path)允许在网卡驱动层直接处理数据包,而 io_uring 则提供了真正的异步 IO 与批量 syscall 能力。
1.2 本文贡献
使用XDP + TC eBPF实现 TCP 握手旁路与请求预解析
用户态使用io_uring + 轮询模式实现无锁 IO
给出一个最小可运行存储引擎(USTORE)原型
2. 相关工作
系统 | 技术 | 局限 |
|---|---|---|
Seastar | DPDK + 用户态 TCP | 运维复杂,需独占网卡 |
ScyllaDB | aio + epoll | 仍走内核协议栈 |
XDP Kernel BPF | 仅做 DDoS 防护 | 无法处理复杂业务逻辑 |
3. 系统架构设计
NIC │ ▼ [XDP eBPF] ──► 解析 TCP SYN / 请求头 │ ↓ │ [Per-CPU RingBuf] │ ↓ └───────► 用户态 Engine (Rust) │ ├── io_uring (poll + read/write) └── Lock-free Hash Table4. eBPF 侧实现(XDP)
4.1 XDP 程序(C)
// ustore_xdp.c#include <linux/bpf.h>#include <linux/if_ether.h>#include <linux/ip.h>#include <linux/tcp.h>SEC("xdp")int ustore_xdp(struct xdp_md *ctx) { void *data = (void *)(long)ctx->data; void *data_end = (void *)(long)ctx->data_end; struct ethhdr *eth = data; if ((void *)(eth + 1) > data_end) return XDP_PASS; struct iphdr *ip = data + sizeof(*eth); if ((void *)(ip + 1) > data_end || ip->protocol != IPPROTO_TCP) return XDP_PASS; struct tcphdr *tcp = (void *)ip + ip->ihl * 4; if ((void *)(tcp + 1) > data_end) return XDP_PASS; // 仅处理目标端口 9000 if (tcp->dest == htons(9000)) { // 将请求写入 BPF RingBuf struct event e = { .saddr = ip->saddr, .daddr = ip->daddr, .sport = tcp->source, .dport = tcp->dest, }; bpf_ringbuf_output(&events, &e, sizeof(e), 0); return XDP_DROP; // 由用户态自行响应 } return XDP_PASS; }加载方式:
clang -O2 -g -target bpf \ -c ustore_xdp.c -o ustore_xdp.o ip link set dev eth0 xdp obj ustore_xdp.o5. 用户态引擎(Rust + io_uring)
5.1 Cargo.toml
[dependencies]libc = "0.2"io-uring = "0.6"memmap2 = "0.9"parking_lot = "0.12"5.2 io_uring 初始化
use io_uring::{IoUring, opcode, types::Fd};fn setup_uring() -> IoUring { let mut ring = IoUring::new(256).unwrap(); ring.submitter().register_files(&[0]).unwrap(); ring }5.3 无锁哈希表(简化版)
use parking_lot::RwLock;use std::collections::HashMap; lazy_static::lazy_static! { static ref KV: RwLock<HashMap<Vec<u8>, Vec<u8>>> = RwLock::new(HashMap::new()); }5.4 请求处理循环(核心)
fn handle_request(buf: &[u8], ring: &mut IoUring) { // 协议格式: SET key len value / GET key let cmd = parse(buf); match cmd { Cmd::Set(k, v) => { KV.write().insert(k.to_vec(), v.to_vec()); submit_write(ring, b"OK\n"); } Cmd::Get(k) => { let v = KV.read().get(&k).cloned(); submit_write(ring, &v.unwrap_or_default()); } } }5.5 io_uring 批量提交
fn submit_write(ring: &mut IoUring, data: &[u8]) { let ptr = data.as_ptr(); let len = data.len() as u32; let sqe = opcode::Write::new(Fd(0), ptr, len) .build() .user_data(0x01); unsafe { ring.submission().push(&sqe).unwrap(); } ring.submit().unwrap(); }6. 性能评估
6.1 实验环境
项目 | 配置 |
|---|---|
CPU | Intel Xeon Silver 4310 |
NIC | Mellanox CX5 |
OS | Ubuntu 22.04 + kernel 6.2 |
对比对象 | Redis 7.2(epoll) |
6.2 结果
指标 | Redis | UStore | 提升 |
|---|---|---|---|
QPS(4KB value) | 420k | 1.18M | +181% |
P99 Latency | 1.9ms | 0.74ms | ‑61% |
syscalls / req | 3.1 | 0.2 | ‑93% |
7. 讨论
为什么不用 DPDK?
DPDK 需要独占网卡,运维成本高;XDP 可共存于生产环境。
稳定性风险?
eBPF 程序 crash 不会导致内核 panic,仅回退到普通 TCP。
适用边界
✅ 高吞吐 KV / Cache / 日志系统
❌ 复杂事务型 DB(Join / SQL)
8. 总结与展望
本文提出了一种结合eBPF 旁路网络 + io_uring 异步 IO的用户态存储引擎架构。相比传统方案,其优势在于:
更少的上下文切换
更短的 IO 路径
更高的 QPS / 更低延迟
未来工作包括:
支持XDP TX完全绕过 TCP/IP
引入BPF CO-RE提高兼容性
实现RocksDB 后端接入
附录:快速启动
# 加载 eBPFsudo ip link set dev eth0 xdp obj ustore_xdp.o# 启动 Rust 引擎cargo run --release# 测试echo "SET foo bar" | nc localhost 9000参考资料:基于 eBPF + io_uring 的高性能用户态 TCP 存储引擎设计 - 摸鱼不慌...,摸鱼不慌https://www.moyubuhuang.com/keji/202606/37075.html