Skip to content

Commit d61df35

Browse files
committed
perf(stack-select): wrk loopback baseline for kernel-access path
- helloworld_stacksel: add single-thread epoll keep-alive bench mode - Makefile: A/B targets _ffk (ff_socket SOCK_KERNEL) vs _libc (raw socket) - docs: 10-perf-baseline-report — A/B delta within noise (no data-path regression), with freebsd_13_to_15 CVM data as background reference
1 parent 4b605b0 commit d61df35

3 files changed

Lines changed: 265 additions & 2 deletions

File tree

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# 10 性能基线报告:内核访问路径(ff_socket(SOCK_KERNEL))wrk 自压基线
2+
3+
> **文档编号**:SPEC-KE-10
4+
> **日期**:2026-06-16
5+
> **状态**:COMPLETE
6+
> **作用域**:在**本机 loopback** 用 wrk 对"本地 socket/fd/event 访问"特性(`ff_socket(SOCK_KERNEL)` 内核栈路径)做性能基线,并以同源原生 `libc socket()` 版本做 A/B 对压求开销 Δ%,以 `freebsd_13_to_15_upgrade_spec/` 既有 CVM 数据作背景对照。
7+
> **实证铁律**:所有数字来自实际 wrk 运行(原始输出 `/tmp/keperf/{A,B}_T{1,2,3}_trial{1,2,3}.txt`),禁止臆造。
8+
9+
---
10+
11+
## 1. 测试目的
12+
13+
量化"内核访问特性"在**数据面**是否引入吞吐/延迟开销。本特性核心是 socket 创建时按标记选栈(`ff_socket(...,SOCK_KERNEL)``ff_host_socket` → 宿主 `socket()`),后续 `bind/listen/accept/recv/send/epoll` 走相同的内核 fd 路径。因此理论上 A/B 的差异**仅为每连接建连时一次 `ff_socket→ff_host_socket` 函数跳转**,对 keep-alive 长连接的数据面应为零额外开销。本测试用实测验证该判断。
14+
15+
---
16+
17+
## 2. 环境
18+
19+
|||
20+
|---|---|
21+
| 主机 | 单台 CVM,16 vCPU,31 GiB |
22+
| 压测器 | wrk **4.2.0 [epoll]**(GitHub `wg/wrk` 源码本机构建,`/tmp/wrk-build/wrk`;系统源无 wrk 包) |
23+
| 被测端 | `example/helloworld_stacksel`**单线程 epoll keep-alive HTTP server**`bench <port>` 模式) |
24+
| 协议 | HTTP/1.1,`Connection: keep-alive`,固定 15B 响应体(对标 freebsd 升级 CVM helloworld 场景) |
25+
| 链路 | **loopback 127.0.0.1**(server 与 wrk 同机;server 绑 CPU0,wrk 绑 CPU2-15,`taskset` 降争用) |
26+
| 编译 | A/B 同 `-O2 -g`,同源 `main.c`,均链接 `lib/libfstack.a` |
27+
28+
### 2.1 A/B 两版本(同源,仅 `ksock()` 不同)
29+
| 版本 | 构建 | socket 创建 | 含义 |
30+
|---|---|---|---|
31+
| **A** `helloworld_stacksel_ffk` | `-DUSE_FF_KERNEL=1` | `ff_socket(AF_INET, SOCK_STREAM\|SOCK_KERNEL, 0)` | 本特性内核访问路径 |
32+
| **B** `helloworld_stacksel_libc` | `-DUSE_FF_KERNEL=0` | `socket(AF_INET, SOCK_STREAM, 0)` | 纯内核栈参照 |
33+
34+
---
35+
36+
## 3. 方法(对齐既有 CVM 方法学档位)
37+
38+
| 档位 | wrk 参数 | 时长 | 用途 |
39+
|---|---|---|---|
40+
| T1 | `-t2 -c10 --latency` | 5s | 轻载 + 预热剔除 |
41+
| T2 | `-t4 -c100 --latency` | 30s | 中负载主回归 |
42+
| T3 | `-t8 -c500 --latency` | 30s | 高并发尾延迟 |
43+
44+
- 每档 **3 trial 取 median**;每版本前置一次 3s warmup(丢弃)。
45+
- 命令模板:`taskset -c 2-15 /tmp/wrk-build/wrk -t4 -c100 -d30s --latency http://127.0.0.1:<port>/`
46+
- server 启停经 `/data/workspace/kill_process.sh`;脚本 `/tmp/keperf/runbench.sh`
47+
48+
---
49+
50+
## 4. 实测结果(median of 3 trials;原始见 `/tmp/keperf/`
51+
52+
### 4.1 吞吐 req/s
53+
54+
| 档位 | A `SOCK_KERNEL` | B `libc socket` | Δ (A vs B) | 三次 trial(A / B) |
55+
|---|---:|---:|---:|---|
56+
| T1 (-t2 -c10 5s) | 120,949 | 136,199 | **−11.2%** | A 119822/132566/120949 · B 137667/136199/118075 |
57+
| T2 (-t4 -c100 30s) | 125,169 | 119,498 | **+4.7%** | A 125169/113753/135646 · B 119498/135067/118084 |
58+
| T3 (-t8 -c500 30s) | 107,298 | 112,728 | **−4.8%** | A 102829/115724/107298 · B 114646/105920/112728 |
59+
60+
### 4.2 延迟(median of 3 trials)
61+
62+
| 档位 | A p50 | B p50 | A p99 | B p99 |
63+
|---|---:|---:|---:|---:|
64+
| T1 | 67us | 60us | 151us | 133us |
65+
| T2 | 767us | 814us | 1.04ms | 1.15ms |
66+
| T3 | 4.58ms| 4.37ms| 5.25ms | 5.11ms |
67+
68+
- **Socket errors:0**(所有 18 个 trial 均无 connect/read/write/timeout 错误)。
69+
70+
### 4.3 结论(A/B)
71+
72+
**A 与 B 的吞吐/延迟差异在 ±11% 的 trial 间噪声范围内、无系统性方向**(A 在 T2 略快、T1/T3 略慢,各 trial 区间高度重叠)。这与理论一致:`ff_socket(SOCK_KERNEL)` 仅在**建连时**`libc socket()` 多一次 `ff_host_socket` 函数跳转,对 keep-alive 数据面零额外开销。
73+
74+
**内核访问特性不引入可测量的数据面性能回归**(佐证 NFR-1 零开销 / NFR-2 业务快路径无回归)。波动主因:loopback 自压下 server 与 wrk 同机 CPU/软中断争用(实测 `sys` 时间占比极高),属测量噪声而非特性开销。
75+
76+
---
77+
78+
## 5. 背景对照:freebsd_13_to_15 既有 CVM 数据(口径不同,仅作参考)
79+
80+
来源 `docs/freebsd_13_to_15_upgrade_spec/zh_cn/13.0-baseline-cvm-bench-report.md`**双 CVM、server 跑 DPDK + F-Stack 用户态栈、相同 wrk 三档**):
81+
82+
| 档位 | 13.0 baseline req/s | 15.0 rfix req/s | 本报告 A(内核栈 loopback)|
83+
|---|---:|---:|---:|
84+
| T1 | 24,414 | 23,757 | 120,949 |
85+
| T2 | 220,691 | 203,933 | 125,169 |
86+
| T3 | 239,555 | 217,100 | 107,298 |
87+
88+
**口径差异(不可直接等价,务必注意)**
89+
1. **栈不同**:CVM 数据是 **F-Stack 用户态栈经 DPDK 网卡**的数据面吞吐;本报告是 **Linux 内核栈 + loopback** 的吞吐——二者测的是不同协议栈的不同路径。
90+
2. **拓扑不同**:CVM 为**双机网卡间**真实收发;本报告为**单机 loopback 自压**(server 与 wrk 争用同一组 CPU)。
91+
3. **并发模型不同**:CVM helloworld 为 F-Stack `lcore=4`;本报告为**单线程 epoll**(用户已确认"单线程数字仅反映串行下限")。
92+
4. T1 在两边都波动较大(5s 短窗),仅作链路探活,不用于结论。
93+
94+
**用途说明**:本特性是在 F-Stack 之外**新增**一条"内核栈可访问"通道(供本机 ping/curl 与客户端连本机/外部内核服务),**不替代** F-Stack 业务数据面。CVM 数据用于说明 F-Stack 业务面的吞吐量级背景;本特性的开销由 §4 的 A/B 同环境对压给出(≈噪声、无回归)。
95+
96+
---
97+
98+
## 6. 局限与后续
99+
100+
- 本基线为**本机 loopback 单线程**自压,反映"串行/单 loop 下限",**** server 真实极限,也非双 CVM DPDK 数据面;绝对值仅在同口径内可比。
101+
- 真实物理机/双 CVM 下的 hook 模式端到端与 F-Stack 业务面+内核管理面共存吞吐,待具备 DPDK 绑定物理 NIC 环境后按 `cvm-bench-methodology.md` 补跑(沿用 `freebsd_13_to_15` F-A3/F-A4 路径)。
102+
- 如需更高单机吞吐基线,可启用多线程 `SO_REUSEPORT`(本轮按用户要求保持单线程)。
103+
104+
---
105+
106+
## 7. 复现实步
107+
108+
```bash
109+
# 1) 构建 wrk(系统无包时源码构建)
110+
git clone --depth 1 https://github.com/wg/wrk.git /tmp/wrk-build && make -C /tmp/wrk-build -j4
111+
112+
# 2) 构建 A/B 被测二进制
113+
cd /data/workspace/f-stack/example/helloworld_stacksel && make bench
114+
# -> helloworld_stacksel_ffk (SOCK_KERNEL) / helloworld_stacksel_libc (libc)
115+
116+
# 3) 三档自压(server 绑 CPU0,wrk 绑 CPU2-15,各 3 trial)
117+
bash /tmp/keperf/runbench.sh A helloworld_stacksel_ffk 18211
118+
bash /tmp/keperf/runbench.sh B helloworld_stacksel_libc 18212
119+
# 原始输出:/tmp/keperf/{A,B}_T{1,2,3}_trial{1,2,3}.txt
120+
```
121+
122+
> 合规:wrk 进程经 `kill_process.sh` 停止、临时文件经 `rm_tmp_file.sh` 清理;无直接 rm/kill/chmod。

example/helloworld_stacksel/Makefile

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,28 @@ LIBS+= -L${FF_PATH}/lib -Wl,--whole-archive,-lfstack,--no-whole-archive
1717
LIBS+= -Wl,--no-whole-archive -lrt -lm -ldl -lcrypto -lz -pthread -lnuma
1818

1919
TARGET=helloworld_stacksel
20+
TARGET_FFK=helloworld_stacksel_ffk
21+
TARGET_LIBC=helloworld_stacksel_libc
22+
23+
# Perf-baseline binaries are built at -O2 (same flags for A and B so the
24+
# measured A/B delta isolates only the ksock() difference).
25+
BENCH_CFLAGS=-O2 -g -I${FF_PATH}/lib $(shell $(PKGCONF) --cflags libdpdk)
2026

2127
all:
2228
cc ${CFLAGS} -o ${TARGET} main.c ${LIBS}
2329

30+
# A/B perf-baseline targets from the SAME main.c (only ksock() differs):
31+
# _ffk : USE_FF_KERNEL=1 -> ff_socket(SOCK_KERNEL) (feature path A)
32+
# _libc : USE_FF_KERNEL=0 -> libc socket() (pure-kernel ref B)
33+
.PHONY: bench
34+
bench: ${TARGET_FFK} ${TARGET_LIBC}
35+
36+
${TARGET_FFK}:
37+
cc ${BENCH_CFLAGS} -DUSE_FF_KERNEL=1 -o ${TARGET_FFK} main.c ${LIBS}
38+
39+
${TARGET_LIBC}:
40+
cc ${BENCH_CFLAGS} -DUSE_FF_KERNEL=0 -o ${TARGET_LIBC} main.c ${LIBS}
41+
2442
.PHONY: clean
2543
clean:
26-
/data/workspace/rm_tmp_file.sh $(CURDIR)/${TARGET} $(CURDIR)/main.o
44+
/data/workspace/rm_tmp_file.sh $(CURDIR)/${TARGET} $(CURDIR)/${TARGET_FFK} $(CURDIR)/${TARGET_LIBC} $(CURDIR)/main.o

example/helloworld_stacksel/main.c

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,46 @@
1616
* (no args) self-test: in-process kernel-stack server+client
1717
* server <port> one-shot HTTP/1.1 server on the kernel stack
1818
* client <ip> <port> connect to a kernel-stack service and print reply
19+
* bench <port> single-thread epoll keep-alive HTTP server (wrk target)
20+
*
21+
* A/B build switch (perf baseline):
22+
* USE_FF_KERNEL=1 (default) ksock() -> ff_socket(SOCK_KERNEL) [feature path A]
23+
* USE_FF_KERNEL=0 ksock() -> libc socket() [pure-kernel ref B]
1924
*/
2025

2126
#include <stdio.h>
2227
#include <stdlib.h>
2328
#include <string.h>
2429
#include <unistd.h>
2530
#include <errno.h>
31+
#include <fcntl.h>
2632
#include <sys/types.h>
2733
#include <sys/socket.h>
34+
#include <sys/epoll.h>
2835
#include <sys/wait.h>
2936
#include <netinet/in.h>
3037
#include <arpa/inet.h>
3138

3239
#include "ff_api.h" /* ff_socket + SOCK_KERNEL / SOCK_FSTACK markers */
3340

41+
#ifndef USE_FF_KERNEL
42+
#define USE_FF_KERNEL 1
43+
#endif
44+
3445
static int
3546
ksock(void)
3647
{
37-
/* Single API + marker: this socket belongs to the host kernel stack. */
48+
#if USE_FF_KERNEL
49+
/* A (feature path): connection-level stack selection via marker. */
3850
int fd = ff_socket(AF_INET, SOCK_STREAM | SOCK_KERNEL, 0);
3951
if (fd < 0)
4052
perror("ff_socket(SOCK_KERNEL)");
53+
#else
54+
/* B (pure-kernel reference): raw libc kernel socket. */
55+
int fd = socket(AF_INET, SOCK_STREAM, 0);
56+
if (fd < 0)
57+
perror("socket()");
58+
#endif
4159
return fd;
4260
}
4361

@@ -211,6 +229,109 @@ do_selftest(void)
211229
return 1;
212230
}
213231

232+
static int
233+
set_nonblock(int fd)
234+
{
235+
int fl = fcntl(fd, F_GETFL, 0);
236+
return (fl < 0) ? -1 : fcntl(fd, F_SETFL, fl | O_NONBLOCK);
237+
}
238+
239+
/*
240+
* Single-threaded epoll keep-alive HTTP server (mode "bench <port>") — the wrk
241+
* performance-baseline target. One thread, one epoll loop multiplexing the
242+
* listen fd and all accepted keep-alive connections (same single-loop
243+
* event-driven model as the F-Stack helloworld_epoll baseline). All fds come
244+
* from ksock(): SOCK_KERNEL host fds (A) or raw libc fds (B); the rest of the
245+
* path (bind/listen/accept/epoll/recv/send) is identical, so the A/B delta
246+
* isolates only the per-connection ff_socket->ff_host_socket indirection.
247+
*/
248+
static int
249+
do_bench(int port)
250+
{
251+
static const char resp[] =
252+
"HTTP/1.1 200 OK\r\nContent-Length: 15\r\n"
253+
"Connection: keep-alive\r\n\r\nhello-stacksel\n";
254+
const size_t resp_len = sizeof(resp) - 1;
255+
256+
int sfd = ksock();
257+
if (sfd < 0)
258+
return 1;
259+
int on = 1;
260+
setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
261+
262+
struct sockaddr_in sa;
263+
memset(&sa, 0, sizeof(sa));
264+
sa.sin_family = AF_INET;
265+
sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
266+
sa.sin_port = htons((unsigned short)port);
267+
if (bind(sfd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
268+
perror("bind");
269+
return 1;
270+
}
271+
if (listen(sfd, 1024) < 0) {
272+
perror("listen");
273+
return 1;
274+
}
275+
set_nonblock(sfd);
276+
277+
int ep = epoll_create1(0);
278+
if (ep < 0) {
279+
perror("epoll_create1");
280+
return 1;
281+
}
282+
struct epoll_event ev, evs[1024];
283+
ev.events = EPOLLIN;
284+
ev.data.fd = sfd;
285+
epoll_ctl(ep, EPOLL_CTL_ADD, sfd, &ev);
286+
287+
printf("[bench] %s keep-alive server on 127.0.0.1:%d (single-thread epoll)\n",
288+
(USE_FF_KERNEL ? "ff_socket(SOCK_KERNEL)" : "libc socket()"), port);
289+
fflush(stdout);
290+
291+
for (;;) {
292+
int n = epoll_wait(ep, evs, 1024, -1);
293+
if (n < 0) {
294+
if (errno == EINTR)
295+
continue;
296+
perror("epoll_wait");
297+
break;
298+
}
299+
for (int i = 0; i < n; i++) {
300+
int fd = evs[i].data.fd;
301+
if (fd == sfd) {
302+
for (;;) {
303+
int cfd = accept(sfd, NULL, NULL);
304+
if (cfd < 0)
305+
break; /* EAGAIN: backlog drained */
306+
set_nonblock(cfd);
307+
ev.events = EPOLLIN;
308+
ev.data.fd = cfd;
309+
epoll_ctl(ep, EPOLL_CTL_ADD, cfd, &ev);
310+
}
311+
} else {
312+
char buf[2048];
313+
ssize_t r = recv(fd, buf, sizeof(buf), 0);
314+
if (r > 0) {
315+
ssize_t off = 0;
316+
while (off < (ssize_t)resp_len) {
317+
ssize_t w = send(fd, resp + off, resp_len - off, 0);
318+
if (w <= 0)
319+
break;
320+
off += w;
321+
}
322+
} else if (r == 0 ||
323+
(r < 0 && errno != EAGAIN && errno != EWOULDBLOCK)) {
324+
epoll_ctl(ep, EPOLL_CTL_DEL, fd, NULL);
325+
close(fd);
326+
}
327+
}
328+
}
329+
}
330+
close(ep);
331+
close(sfd);
332+
return 0;
333+
}
334+
214335
int
215336
main(int argc, char **argv)
216337
{
@@ -220,5 +341,7 @@ main(int argc, char **argv)
220341
return do_server(atoi(argv[2]), /*oneshot*/atoi(argv[3])) == 0 ? 0 : 1;
221342
if (argc >= 4 && strcmp(argv[1], "client") == 0)
222343
return do_client(argv[2], atoi(argv[3])) == 0 ? 0 : 1;
344+
if (argc >= 3 && strcmp(argv[1], "bench") == 0)
345+
return do_bench(atoi(argv[2]));
223346
return do_selftest();
224347
}

0 commit comments

Comments
 (0)