Skip to content

Commit ff09a17

Browse files
committed
Fix native-mt multi-thread multi-queue: give each worker its own prison so socket/ifioctl bind to the worker's vnet instead of vnet0, and keep pcpu cpuid at 0 to avoid out-of-bounds zpcpu_get in this non-SMP build. Also make init_mem_pool honor nb_threads in thread_mode=1.
1 parent 82b409f commit ff09a17

8 files changed

Lines changed: 1416 additions & 8 deletions

docs/native_mt_spec/zh_cn/15-worker时钟缺口修复与virtio-RSS限制.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,36 @@
33
> **文档编号**:SPEC-NMT-15
44
> **版本**:v1
55
> **日期**:2026-08-03
6-
> **状态**:H1(worker 时钟缺口)已定位并修复,实测生效;2 线程吞吐受限的最终瓶颈定位为 virtio PMD 不支持 RSS/RETA(环境限制,非代码缺陷)。
6+
> **状态**:H1(worker 时钟缺口)已定位并修复,实测生效。
7+
> **⚠️ 结论已被纠偏(2026-08-03)**:本文第 5 节「2 线程吞吐受限的最终瓶颈是 virtio PMD 不支持 RSS/RETA(环境限制,非代码缺陷)」**已被实测推翻**,请以 `16-多队列对照实验与根因纠偏.md` 为准。
78
> **实证铁律**:本文所有计数、req/s、backtrace 均来自实际运行输出,禁止臆造。
89
910
---
1011

12+
## 0. 纠偏声明(2026-08-03 追加)
13+
14+
本文第 5 节与第 9 节关于「virtio 无 RSS 导致多队列失效」的结论**不成立**,原因是本文的对照实验缺失了关键一组:**`thread_mode=0` + 2 进程 2 队列**
15+
16+
16 号文档补做该实验后实测:
17+
18+
| 配置 | 队列数 | req/s |
19+
|---|---|---|
20+
| `thread_mode=0`,1 进程 | 1 | 206,963 |
21+
| **`thread_mode=0`,2 进程** | **2** | **231,570**(正常) |
22+
| `thread_mode=1`,2 线程 | 2 | 0(不通) |
23+
24+
即 virtio 双队列在**完全相同的"无 RSS"代码路径**下工作正常且吞吐更高。本文错误地把「1 队列 vs 2 队列」的差异归因为「进程模式 vs 线程模式」。
25+
26+
真实根因是两个代码缺陷(详见 16 号文档):
27+
1. **R1**:worker cred 挂全局 `prison0`,而 socket 的 vnet 取自 `CRED_TO_VNET(cred)` 而非 `curvnet``freebsd/kern/uipc_socket.c:948`)→ worker 所有 socket / `ifioctl` 被静默重定向到 vnet0。
28+
2. **R2**:worker `pcpu_init()``rte_lcore_id()` 作 cpuid,但本build 非 SMP(`MAXCPU==1`)→ `zpcpu_get()` 越界。
29+
30+
修复后 `thread_mode=1` 双线程达~233k req/s(与 2 进程 ~234k 持平),60 秒 400 连接 soak 达 497k req/s。
31+
32+
**因此本文第 9 节「这是环境约束,代码侧已无可修之处」的表述亦属错误。**
33+
34+
---
35+
1136
## 1. 本轮起点
1237

1338
14 号文档结论:per-vnet 隔离(每 worker 独立 `vnet_alloc()` + 独立 ifp)已消除 `in_pcblookup_mbuf` crash,但 2 线程吞吐塌陷至 91.39 req/s,而 1 线程为 209,388 req/s。本轮定位该性能塌陷的根因。
@@ -172,7 +197,7 @@ worker(lcore=2)hardclock 从 **0 变为与主线程同步推进**(1999 vs
172197

173198
---
174199

175-
## 5. 最终瓶颈:virtio PMD 不支持 RSS/RETA(环境限制)
200+
## 5. ~~最终瓶颈:virtio PMD 不支持 RSS/RETA(环境限制)~~【本节结论已被推翻,见第 0 节与 16 号文档】
176201

177202
### 5.1 现象
178203

docs/native_mt_spec/zh_cn/16-多队列对照实验与根因纠偏.md

Lines changed: 336 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# M1-A 代码路径核验:thread_mode=0 vs 1 双队列差异 + 根因定位
2+
3+
> 探测人:leader(纯只读探测/汇总角色)+ code-explorer 子 agent A
4+
> 日期:2026-08-03
5+
> 实证铁律:所有结论附`file:line`;无法静态判定者标注「需运行时验证」,并在下文给出实际运行输出。
6+
7+
---
8+
9+
## 0. 先决事实(后续结论的基础,全部代码坐实)
10+
11+
| 事实 | 证据 |
12+
|---|---|
13+
| `parse_lcore_mask` 把 bit-count 写入 `nb_procs`,填 `proc_lcore[]`(mask=6 → `{1,2}`| `lib/ff_config.c:110-141` |
14+
| `port_cfgs[].nb_lcores` 在 ini 解析期固化为 `nb_procs`(=2) 并复制 `proc_lcore` | `lib/ff_config.c:566-572` |
15+
| `thread_mode=1` 的塌缩在所有 per-port 校验之后:`nb_threads=nb_procs; nb_procs=1; proc_id=0; proc_mask=lcore_mask` | `lib/ff_config.c:1465-1484` |
16+
|`nb_lcores==2`(队列数)与 `nb_threads==2` 一致,`nb_procs==1` | 上两条组合 |
17+
| DPDK main lcore = 第一个enabled lcore(未传 `--main-lcore`)→ mask=6 时为 lcore 1 | `dpdk-stable-24.11.6/lib/eal/common/eal_common_options.c` |
18+
| 所有 lcore(含 main)都跑 `main_loop` | `lib/ff_dpdk_if.c:2855` `rte_eal_mp_remote_launch(main_loop, lr, CALL_MAIN)` |
19+
| `ff_lcore_conf_idx()`:mode0 恒 0;mode1 用 `rte_lcore_id()` | `lib/ff_memory.h:104-111` |
20+
21+
---
22+
23+
## 1. H6:dispatch_ring / msg_ring 按 nb_procs(=1) 分配 → worker 访问 NULL ring
24+
25+
**结论:否证(不成立)。**
26+
27+
- `init_dispatch_ring()` 队列上界取 `pconf->nb_lcores`(=2),非 `nb_procs``lib/ff_dpdk_if.c:643-660`
28+
- `init_msg_ring()` 显式按 thread_mode 选 `nb_threads``lib/ff_dpdk_if.c:692-694`
29+
- 消费侧索引合法:`process_dispatch_ring``dispatch_ring[port_id][queue_id]``:2106`),`queue_id` 来自 `init_lcore_conf` thread_mode 分支的 0/1(`:441-448`);`process_msg_ring(qconf->proc_id)``:2784`),`lc->proc_id = ti``:433`
30+
31+
**运行时印证**:ff_log 实测 `dispatch_ring_p0_q0` / `dispatch_ring_p0_q1` 均创建成功。
32+
33+
### 1.1 附带发现的条件性缺陷(本轮未触发,建议后续修)
34+
35+
`init_mem_pool()` 的 mempool 创建循环上界仍是 `nb_procs``lib/ff_dpdk_if.c:548`
36+
37+
```c
38+
for (i = 0; i < ff_global_cfg.dpdk.nb_procs; i++) { /* thread_mode=1 → 只循环 1 次 */
39+
```
40+
41+
而 `nb_mbuf` 规模计算已正确使用 `nb_threads`(`:526-527`)。后果:worker lcore 若与 `proc_lcore[0]` 不同 NUMA socket,`pktmbuf_pool[worker_socket]` 保持 NULL,`init_port_start` 按每队列 lcore 的 socket 取池(`:1062-1080`)会拿到 NULL。
42+
**本机 lcore 1/2 同 socket(实测 ff_log 仅 `create mbuf pool on socket 0`),故未触发。**
43+
44+
---
45+
46+
## 2. H7:veth_ctx 主线程条目与 worker 条目错位/NULL/重复创建
47+
48+
**结论:否证。**
49+
50+
- `veth_ctx[RTE_MAX_LCORE][RTE_MAX_ETHPORTS]` 二维按 lcore 维度隔离,容量足够
51+
- worker 在 `main_loop` 按 `rte_lcore_id()` 索引创建(`lib/ff_dpdk_if.c:2649-2664`),`veth_ctx[lcore][port] == NULL` 才创建,无重复
52+
- **运行时印证**:`DBGVNET ... unit_eq=1` 表明 `ifunit_ref(if_name)` 返回的 ifp 与 `sc->ifp` 相同,无错位
53+
54+
---
55+
56+
## 3. H8:KNI 归属吞掉 80 端口 SYN
57+
58+
**结论:对本场景否证。**
59+
60+
- `ff_kni_is_owner_thread()`:thread_mode=1 时 `rte_lcore_id() == proc_lcore[0]`(`lib/ff_dpdk_kni.c:92-98`),即只有 lcore 1 是 owner
61+
- `config.ini` 为 `kni.enable=1, method=reject, tcp_port=80,443` → `kni_accept=0`
62+
- `process_packets` 的 KNI 分支(`lib/ff_dpdk_if.c:2081-2089`):`FILTER_KNI && kni_accept` 或 `(FILTER_UNKNOWN || >=FILTER_OSPF) && !kni_accept` 才入 KNI。80 端口 TCP 属`FILTER_KNI`,而 `kni_accept=0` → **走 `ff_veth_input`**,不进 KNI
63+
64+
---
65+
66+
## 4. H9:worker vnet 无 listen socket
67+
68+
**结论:否证(app 侧每线程都 listen)。**
69+
70+
`example/main.c`:
71+
- `kq`/`sockfd`/`sockfd6` 均为 `__thread`(`:22-28`)
72+
- `init_thread()` 每线程各自 `ff_socket` + `SO_REUSEPORT` + `ff_bind` + `ff_listen`(`:62-140`)
73+
- `loop()` 首行调`init_thread()`(`:144`),故每个 worker 线程都建了 listen socket
74+
- **运行时印证**:`helloworld.log` 有 `thread init success on lcore 1.` 与 `thread init success on lcore 2.`,两线程 listen 均成功
75+
76+
>但见第 6 节:这些 socket **实际建在 vnet0**,这是真正的问题所在。
77+
78+
---
79+
80+
## 5. thread_mode=0 vs 1 双队列路径差异对照
81+
82+
| 维度 | thread_mode=0(2 进程) | thread_mode=1(2 线程) | 是否差异源 |
83+
|---|---|---|---|
84+
| `nb_procs` / `nb_threads` | 2 / 0 | 1 / 2 | 否(各处已正确适配) |
85+
| `lcore_conf` 索引 | 恒 0(`ff_lcore_conf_idx`) | `rte_lcore_id()` | 否 |
86+
| queue 分配 | 每进程 1 队列(实测 p0→q0, p1→q1) | 每线程 1 队列(q0/q1) | 否 |
87+
| RSS 配置路径 | `if (dev_info.flow_type_rss_offloads)` 整段跳过(virtio) | **完全相同** | **否(关键:两模式共享同一"无 RSS"路径)** |
88+
| `dispatch_ring` | 按 `nb_lcores`=2 | 按 `nb_lcores`=2 | 否 |
89+
| `msg_ring` | 按 `nb_procs`=2 | 按 `nb_threads`=2 | 否 |
90+
| mempool 循环 | `nb_procs`=2(覆盖两lcore socket) | `nb_procs`=1(只覆盖 lcore[0] socket) | 条件性(跨 NUMA 才触发) |
91+
| KNI owner | primary 进程 | `lcore==proc_lcore[0]` | 否 |
92+
| **协议栈实例** | 每进程独立地址空间 → 独立 vnet0,cred/prison0各自一份 | 同进程内 vnet0 + vnet_i,**共享唯一 prison0** | **是(根因)** |
93+
94+
---
95+
96+
## 6. 根因(代码 + 运行时双坐实)
97+
98+
### 6.1 运行时证据
99+
100+
在 `lib/ff_veth.c` `ff_veth_setup_interface` / `ff_veth_setaddr` 加临时探针实测:
101+
102+
```
103+
主线程 (f-stack-0.log):
104+
DBGSOCK cret=0 so=0x7f9526f94400 so_vnet=0x37754ef0 curvnet=0x37754ef0 ioctl_ret=0
105+
DBGVNET curvnet=0x37754ef0 ifp_vnet=0x37754ef0 ifp_fib=0 gw_ifa=0x37d1f240 self_ifa=0x37d1f240 unit_eq=1 flags=0x8803
106+
107+
worker (helloworld.log):
108+
DBGSOCK cret=0 so=0x7f9526f95c00 so_vnet=0x37754ef0 curvnet=0x7f952000eb10 ioctl_ret=0
109+
DBGVNET curvnet=0x7f952000eb10 ifp_vnet=0x7f952000eb10 ifp_fib=0 gw_ifa=0 self_ifa=0 unit_eq=1 flags=0x8802
110+
f-stack-0: ff_veth_set_gateway failed DBGERR=51
111+
```
112+
113+
判读:
114+
- worker 的 `so_vnet=0x37754ef0` **等于主线程的 vnet0**,而 `curvnet=0x7f952000eb10` 是 worker 自己的 vnet_2 → **不一致**
115+
- `ioctl_ret=0`("成功"),但 worker vnet 中 `self_ifa=0`(`ifa_ifwithaddr(本机IP)` 找不到)、`gw_ifa=0`(`ifa_ifwithnet(网关)` 找不到)
116+
- `flags`:主线程 `0x8803` 含 `IFF_UP`(0x1),worker `0x8802` **缺 IFF_UP**(`if_up` 未被调用的连带现象)
117+
- `DBGERR=51` = **ENETUNREACH**(`freebsd-src-releng-15.0/sys/sys/errno.h:114`),来自 `net/route.c:507` `rt_getifa_fib()` 中 `info->rti_ifa == NULL`
118+
119+
### 6.2 代码链(全部坐实)
120+
121+
| 步 | 位置 | 事实 |
122+
|---|---|---|
123+
| 1 | `freebsd/net/vnet.c:336` | `curvnet = prison0.pr_vnet = vnet0 = vnet_alloc();` — prison0 永久绑定 vnet0 |
124+
| 2 | `lib/ff_init_main.c:586-590` | worker cred:`p->p_ucred = crget(); ... cr_prison = &prison0;` |
125+
| 3 | `freebsd/net/vnet.h:247` | `#define CRED_TO_VNET(cr) (cr)->cr_prison->pr_vnet` → **vnet0** |
126+
| 4 | `freebsd/kern/uipc_socket.c:948` | `so = soalloc(CRED_TO_VNET(cred));` → worker 所有 socket 的 `so_vnet` = vnet0 |
127+
| 5 | `freebsd/kern/uipc_socket.c:829-833` | `so->so_vnet = vnet;`(来自 soalloc 参数) |
128+
| 6 | `freebsd/net/if.c:2908` | `ifioctl` 开头 `CURVNET_SET(so->so_vnet);` → **切到 vnet0** |
129+
| 7 | `lib/ff_veth.c:599-601` | `ff_veth_setaddr` 用 `socreate()` + `ifioctl(so, SIOCAIFADDR, ...)` |
130+
| 8 | 后果 | worker 的 IP 被加到 **vnet0** 的 `f-stack-0` 上(故`ioctl_ret=0`),worker 自己的 vnet_2 里的 ifp 从未获得地址 |
131+
| 9 | `lib/ff_veth.c:1028` → `lib/ff_veth.c:638` → `freebsd/net/route/route_ctl.c:756-757` → `freebsd/net/route.c:497,507` | `ff_veth_set_gateway` 的 `rib_action(RTM_ADD)` 在 vnet_2 执行(不经 socket,直接用 `curvnet`)→ `ifa_ifwithroute` 找不到 on-link ifa → `ENETUNREACH` |
132+
133+
### 6.3 为何 thread_mode=0 没有此问题
134+
135+
多进程模式每个进程是独立地址空间,各自有一份 `prison0` 且各自 `prison0.pr_vnet = 自己的 vnet0`,故 `CRED_TO_VNET(cred) == curvnet` 恒成立。**单进程多线程共享唯一 `prison0`(全局变量 `ff_init_main.c:97`)才暴露该错配。**
136+
137+
### 6.4 影响范围(不止路由)
138+
139+
凡「经 socket 进入协议栈」的 worker 操作都被静默重定向到 vnet0:
140+
- `ff_veth_setaddr` / `ff_veth_setaddr6` / `ff_veth_setvaddr`(`ifioctl` 路径)
141+
- `lo_set_defaultaddr()`(定义 `lib/ff_freebsd_init.c:219-263`,`socreate` 在 `:255`、`ifioctl` 在 `:259`)
142+
- **app 侧 `ff_socket`/`ff_bind`/`ff_listen`**:worker 的 listen PCB 全部建在 vnet0 的 PCB 哈希表中,而 worker 数据面 `ff_veth_input` 用 `ifp->if_vnet`(=vnet_2) 作 curvnet 查 PCB → 永远查不到 listen socket → 不回 SYN-ACK
143+
144+
这与实测「listen 成功、client 反复重传 SYN 无响应」完全吻合。
145+
146+
---
147+
148+
## 7. 最可能根因排序
149+
150+
| 排名 | 根因 | 性质 |
151+
|---|---|---|
152+
| **1** | worker cred 挂 `prison0` → `CRED_TO_VNET` 恒为 vnet0 → 所有 socket/ifioctl 操作被 `CURVNET_SET(so->so_vnet)` 重定向到 vnet0 | **代码 + 运行时双坐实** |
153+
| 2 | `init_mem_pool` 循环上界 `nb_procs`(跨 NUMA 才触发) | 代码坐实,本机未触发 |
154+
| 3 | virtio 无 RSS | **已由 E2 实测否证**(thread_mode=0 双队列 231,570 req/s 正常) |

0 commit comments

Comments
 (0)