首页
关于
Search
1
PacketForge 博客部署文档
16 阅读
2
深入 SONiC 与 SAI:从命令行到芯片的完整数据之旅
14 阅读
3
DPU 与 CPO:AI 时代的网络基础设施革命
11 阅读
4
Broadcom芯片
10 阅读
5
DPDK高性能网络
10 阅读
默认分类
交换芯片
高性能网络
AI算力网络
WebSocket高性能项目
DPDK 成长路线
①性能工程基础
②DPDK 源码深读
③用户态协议栈
④软件交换与 SONiC
⑤PCIe、虚拟化与 DPU
⑥面试与复盘
登录
Search
标签搜索
DPDK→DPU 学习路线
P0
P1
P2
P3
P5
PCIe
P4
P6
libmtcp
NUMA
mTCP
OVS
SONiC
SAI
DPU
perf
gdb
EAL
mempool
PacketForge
累计撰写
53
篇文章
累计收到
3
条评论
首页
栏目
页面
关于
WebSocket高性能项目
共
4
篇
最新文章
热门文章
评论最多
点赞最多
2026-08-16
从零实现一个用户态 TCP/IP 协议栈:用 DPDK 把内核 TCP 甩在身后
从零实现一个用户态 TCP/IP 协议栈:用 DPDK 把内核 TCP 甩在身后6000 行 C 代码,11 状态 TCP 状态机,RFC 5681 拥塞控制,零拷贝零系统调用。实测:TCP 连接 1.5ms(内核 2.5ms,快 40%),WS echo RTT 1.5ms(内核 3.1ms,快 52%)。一、为什么要绕过内核 TCP对于绝大多数应用,Linux 内核 TCP 是完美的:30 年打磨、各种边界场景都处理过。但在低延迟数据面(金融行情、交易执行、高频网关)场景下,内核协议栈有三笔"固定开销":开销内核 TCP用户态协议栈(本项目)内存拷贝(收+发)2 次(skb → 用户缓冲区)0 次(mbuf 全程零拷贝)系统调用每次 I/O 都要进入内核0 次(纯用户态函数调用)协议栈可定制性改内核模块,复杂且高风险任意修改,改完 make 即可可观测性/proc/net/tcp 有限每个 TCB 可挂任意指标本项目用 DPDK 24.11 接管网卡,在用户态从二层(以太网)一路实现到七层(WebSocket),数据包全程不经过内核。测试拓扑:Intel X540 10G 网卡 → 千兆交换机 → C WebSocket 服务端,rdtsc 硬件时间戳测量。二、整体架构:双核流水线 + 无锁队列核心思路:用两个 lcore 组成流水线,核间只通过 rte_ring SP/SC 无锁队列传递指针,数据本体零拷贝。flowchart TD subgraph NIC["物理网卡 (DPDK PMD)"] RX["RX 队列 (128 desc)"] TX["TX 队列 (512 desc)"] end subgraph LC0["lcore 0 — 主线程 (main.c)"] B1["rte_eth_rx_burst 收包"] B2["ring→out 出队 → rte_eth_tx_burst 发包"] B3["WS 帧接收轮询 + 应用逻辑"] end subgraph LC1["lcore 1 — pkt_process (tcp_core.c)"] C1["分发: ARP / ICMP / TCP"] C2["TCP 11 状态机 + 拥塞控制"] C3["定时器: RTO / delACK / keepalive / ZWP"] C4["TX 输出引擎 ng_tcp_out"] end RX -->|"ring→in 只入队指针"| C1 C4 -->|"ring→out 只出队指针"| B2 subgraph TCB["每连接 (TCB)"] D1["rcvbuf rte_ring (1024)"] D2["sndbuf rte_ring (1024)"] D3["pthread_cond_t + mutex"] end C2 -->|"数据入队 + cond signal"| D1 D2 --> C4 D1 --> B3 B3 -->|"nsend 分段入队"| D2为什么这样分工?lcore0 只做两件事:从网卡收包、向网卡发包,以及跑应用逻辑(WS 轮询)。它不碰任何协议状态,忙轮询即可。lcore1 是协议处理核心:TCP 状态机、拥塞控制、定时器都在这里。单线程无锁,天然避免竞态。跨核传递只传 rte_mbuf * 指针(8 字节入 ring),实测 0.2 μs,接近 CPU 三级缓存延迟(~15ns)的量级。三、TCB 查找:O(1) 的两级设计每个 TCP 连接一个 ng_tcp_stream(TCB)。数据包到达后,第一步就是找到它属于哪条连接。这里做了两级 O(1) 设计:┌─────────────────────────────────────────────────┐ │ 256 桶哈希表 (4-tuple: sip, dip, sport, dport) │ ← 包路径: 收到段 → 查哈希 → 定位 TCB │ ht_next 链表解决冲突 │ └─────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────┐ │ fd_to_tcb[] 直接数组 (fd → TCB 指针) │ ← 应用路径: nsend(fd) 直接取 TCB └─────────────────────────────────────────────────┘包路径用哈希表:pkt_process 收到一个段,用四元组哈希直接命中 TCB,平均 2.1 ns/次(微基准实测 4.81 亿次/秒)。应用路径用直查数组:nsend(fd) 无需遍历,fd_to_tcb[fd] 一条指令完成。另有一条 active_head 双向链表维护活跃连接,定时器遍历时不用扫全部 256 个桶(这是 P4 优化的成果,详见第三篇)。四、TCP 状态机:完整的 11 状态src/tcp_core.h 中定义了与 RFC 793 一致的 11 个状态。作为客户端,我们主要走右侧这条路径:stateDiagram-v2 [*] --> CLOSED CLOSED --> SYN_SENT: nconnect() 发 SYN SYN_SENT --> ESTABLISHED: 收 SYN+ACK,回 ACK ESTABLISHED --> FIN_WAIT_1: nclose() 发 FIN FIN_WAIT_1 --> FIN_WAIT_2: 收 ACK FIN_WAIT_2 --> TIME_WAIT: 收 FIN,回 ACK TIME_WAIT --> CLOSED: 2MSL 超时 FIN_WAIT_1 --> CLOSING: 同时收到 FIN CLOSING --> TIME_WAIT: 收 ACK ESTABLISHED --> CLOSE_WAIT: 收到对方 FIN CLOSE_WAIT --> LAST_ACK: 本方发 FIN LAST_ACK --> CLOSED: 收 ACK/* src/tcp_core.h — 状态枚举 */ typedef enum _NG_TCP_STATUS { NG_TCP_STATUS_CLOSED = 0, NG_TCP_STATUS_LISTEN, NG_TCP_STATUS_SYN_RCVD, NG_TCP_STATUS_SYN_SENT, NG_TCP_STATUS_ESTABLISHED, NG_TCP_STATUS_FIN_WAIT_1, NG_TCP_STATUS_FIN_WAIT_2, NG_TCP_STATUS_CLOSING, NG_TCP_STATUS_TIME_WAIT, NG_TCP_STATUS_CLOSE_WAIT, NG_TCP_STATUS_LAST_ACK } NG_TCP_STATUS;状态机入口 ng_tcp_process()(src/tcp_input.c,~690 行)按状态 + 段标志(SYN/ACK/FIN/RST)驱动迁移,每个迁移附带动作(发 ACK、唤醒等待线程、回收资源)。五、拥塞控制:RFC 5681 全套src/tcp_output.c 实现了完整的拥塞控制闭环,参数直接对应 RFC:机制实现初始窗口cwnd = 2 × MSS(~2920 字节,RFC 6928)慢启动每收一个 ACK,cwnd += MSS,指数增长拥塞避免超过 ssthresh 后按 MSS²/cwnd 线性增长快速重传收到 3 个重复 ACK 立即重传,不等 RTO快速恢复快重传后 cwnd 减半,进入恢复阶段RTORFC 6298 RTT 估计(srtt + 4×rttvar)+ 指数退避配套的定时器系统(src/tcp_timer.c)在 pkt_process 每次迭代中检查:定时器间隔用途RTO 重传动态(指数退避)丢包恢复延迟 ACK200 ms合并 ACK,减少小包Keepalive2 小时空闲探测对端存活零窗口探测 (ZWP)1 s,最多 10 次对端窗口恢复后继续发六、乱序处理:排序链表 + 有界队列乱序段(序号超前于接收窗口左缘)进入 ofo_head 排序链表(src/tcp_input.c),最大 64 个条目:rcv_nxt = 1000 (期望下一个字节) 收到 seq=1460 的段 → 乱序,挂入 ofo_head(按 seq 排序) 收到 seq=2460 的段 → 乱序,插入 ofo_head 第二个位置 收到 seq=1000 的段 → 命中期望!立刻交付,然后沿 ofo_head 逐条取出: seq=1460 ✓ 交付 seq=2460 ✓ 交付 → ofo_head 清空每个 OFO 段同样从 mempool 分配(见第三篇 P0 优化),上限 256 个,超过即丢弃,防止内存被恶意乱序攻击耗尽。七、阻塞式 Socket API:用条件变量骗过应用层这是最有意思的设计。用户态协议栈的难点之一:应用层习惯阻塞式调用(connect() 卡住直到连上),但协议处理在另一个 lcore 上跑。解决方案是条件变量 + 内联轮询:sequenceDiagram participant APP as 应用线程 (lcore0) participant PKT as pkt_process (lcore1) APP->>APP: nconnect(): 发 SYN, 置状态 SYN_SENT loop 等待握手期间 APP->>APP: 内联执行 rte_eth_rx_burst + rte_eth_tx_burst APP->>APP: pthread_cond_timedwait(stream->cond, 1ms) end PKT->>PKT: 收到 SYN+ACK → 状态迁移 ESTABLISHED PKT->>APP: pthread_cond_signal(stream->cond) APP->>APP: 醒来检查 state == ESTABLISHED → 返回成功关键点(src/tcp_socket.c):nrecv() 阻塞在 stream->cond 上;pkt_process 收到数据入 rcvbuf 后 signal 唤醒。nconnect() 在等握手的间隙顺手帮 pkt_process 干收发包的活——反正核闲着也是闲着,这段内联轮询把连接建立延迟从"跨核通知"压到了理论下限。nclose()(P3 优化后)用 rte_pause() 忙轮询替代条件变量定时等待,关闭路径无阻塞抖动。应用层看到的 API 与 POSIX 几乎一致:int fd = nsocket(); // 创建 socket nconnect(fd, &servaddr, sizeof(servaddr)); // TCP 三次握手 ws_connect(fd, host, port, "/ws", NULL); // WS 握手 nsend(fd, buf, len, 0); // 发送(TCP 分段 + 拥塞控制) nrecv(fd, buf, len, 0); // 阻塞接收 nclose(fd); // 四次挥手八、发送路径:从 nsend 到网线nsend(fd, 1MB) → 按 MSS 1460 切成 ~700 个 ng_tcp_fragment(从 g_frag_pool 分配) → 逐个入 stream->sndbuf ring(不拷贝,指针传递) └─ pkt_process 每轮迭代: ng_tcp_out(): 从 sndbuf 出队 → 组以太网/IP/TCP 头 → 查 ARP 表解析 MAC(未命中: 发 ARP 请求 + 段重新入队等待) → 受 cwnd 窗口限制,未发出的段留在 sndbuf → 入 ring->out → lcore0 出队 → rte_eth_tx_burst 发出ARP 请求做了 10 次/秒/目的地址 的限速(src/arp.h 链表实现),避免对端无响应时刷爆网络。九、实测结果同一物理链路,rdtsc 时间戳测量(完整数据见 PUBLISH.md):指标DPDK(本项目)Linux 内核 TCP优势TCP 连接1.5 ms2.5 ms快 40%WS 握手3.0 ms5.4 ms快 45%WS echo RTT1.5 ms3.1 ms快 52%跨核包传递0.2 μsN/A硬件极限代码规模(纯 C,仅依赖 DPDK):模块行数TCP 核心(状态机 + 拥塞控制)~3500SACK + 乱序队列~700拥塞控制~600定时器~200Socket API~500WebSocket~400合计~6000十、总结一个"能用"的 TCP 客户端栈其实不需要几万行代码:11 个状态、4 个定时器、2 条无锁队列、1 张哈希表,就是全部骨架。真正拉开性能差距的,是绕开内核后的零拷贝与零系统调用,以及后续一系列数据面优化(P0-P4,见第三篇)。下一篇:在自研 TCP 栈上跑通 WebSocket:RFC 6455 帧编解码实战相关源码:src/tcp_core.h(结构体与常量)、src/tcp_input.c(状态机)、src/tcp_output.c(拥塞控制)、src/tcp_timer.c(定时器)、src/tcp_socket.c(Socket API)、src/main.c(主循环)
2026年08月16日
10 阅读
0 评论
0 点赞
2026-08-16
在自研 TCP 栈上跑通 WebSocket:RFC 6455 帧编解码实战
在自研 TCP 栈上跑通 WebSocket:RFC 6455 帧编解码实战上一篇讲了自研 TCP 协议栈。这一篇往上走一层:如何在它上面实现 WebSocket 客户端,包括 HTTP Upgrade 握手、帧编解码、掩码、粘包半包、控制帧——以及掉进去的坑。一、为什么是 WebSocket金融行情推送(股票、期货行情源)普遍用 WebSocket 对外提供实时数据流:HTTP 完成连接升级后,就是一个双向、低开销的帧协议。相比裸 TCP,它有标准化的握手、心跳(Ping/Pong)和关闭语义;相比 HTTP 轮询,它没有每秒数百次请求的开销。在自研 TCP 栈上实现 WS 客户端,整个链路是:DPDK 收包 → 自研 TCP 栈 → stream->rcvbuf → nrecv() → ws_recv_frame() 解帧 → 应用拿到完整 JSON 行情消息二、握手:一次普通的 HTTP 请求,加上点仪式感RFC 6455 的握手本质是一个 HTTP/1.1 Upgrade 请求。ws_connect()(src/ws_client.c)的实现步骤:生成 16 字节随机数,Base64 编码成 Sec-WebSocket-Key;发送 Upgrade 请求;收到响应后检查 101 状态码和 Upgrade: websocket 头;把响应中 \r\n\r\n 之后的字节(可能是服务端抢先发的第一帧)存入 leftover 缓冲。sequenceDiagram participant C as DPDK WS 客户端 participant S as WS 服务端 C->>S: GET /ws HTTP/1.1<br/>Upgrade: websocket<br/>Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==<br/>Sec-WebSocket-Version: 13 Note over S: 校验 key,按 RFC 计算<br/>SHA1(key+magic) 回写 Accept S->>C: HTTP/1.1 101 Switching Protocols<br/>Upgrade: websocket<br/>Sec-WebSocket-Accept: ... Note over C: 检查 101 + Upgrade 头 C->>C: 保存 \r\n\r\n 之后的字节<br/>到 g_ws_leftover(防丢首帧) C->>S: 应用消息(TEXT 帧,带掩码)真实的握手代码(节选):/* 生成随机 16 字节 key,base64 编码 */ uint8_t key_bytes[16]; ws_random_bytes(key_bytes, 16); base64_encode(key_bytes, 16, ws_key); /* 构建 HTTP Upgrade 请求 */ snprintf(handshake, sizeof(handshake), "GET %s HTTP/1.1\r\n" "Host: %s:%d\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Key: %s\r\n" "Sec-WebSocket-Version: 13\r\n", path, host, port, ws_key);两个值得注意的实现细节:IPv4 only:当前用 inet_pton(AF_INET, ...) 解析主机,不支持域名(协议栈没有 DNS)。生产场景需要预先解析或加 DNS 模块。leftover 缓冲(g_ws_leftover[4096]):服务端可以把手握响应和第一帧数据写进同一个 TCP 段。如果解析器只读到 \r\n\r\n 就返回,后面的帧字节就丢了。这里把多读的字节存起来,ws_recv_frame() 优先从 leftover 消费——这是 WS-over-TCP 最经典的坑之一。三、帧格式:把 RFC 的图搬进代码注释ws_send_frame() 的文件注释里直接画了帧结构(RFC 6455 §5.2): 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - -+ | Masking-key (4 bytes) | Payload Data | +--------------------------------+ - - - - - - - - - - - - - - -+要点:字段含义FIN (1bit)是否最后一帧(本项目暂未实现分片发送,但解析器兼容)opcode (4bit)帧类型:TEXT=0x1 / BINARY=0x2 / CLOSE=0x8 / PING=0x9 / PONG=0xAMASK (1bit)负载是否掩码。客户端→服务端必须为 1,服务端→客户端为 0Payload len (7bit)长度三档:≤125 直接存;=126 用后续 2 字节;=127 用后续 8 字节长度三档编码if (payload_len < 126) { /* 短帧: 7bit 直接存 */ } else if (payload_len <= 0xFFFF) { /* 126: 后续 2 字节大端 */ } else { /* 127: 后续 8 字节大端 */ }四、掩码:4 字节 XOR,安全与性能兼得客户端发出的每一帧都必须带 4 字节随机掩码,负载逐字节 XOR:/* ws_send_frame: 生成掩码并逐字节 XOR */ for (int i = 0; i < len; i++) masked[i] = payload[i] ^ mask_key[i & 3];掩码的初衷是防止"缓存投毒"(恶意代理场景),实现代价极低——XOR 一条 SIMD 指令可以并行处理 16 字节。微基准实测帧编解码吞吐 18 Gbps(50 万次编码基准,test/bench_tcp.c)。接收侧对应地解掩码:按规范,服务端发来的帧不应带掩码,但解析器做了兼容——只要 MASK 位为 1 就解。五、接收路径:一个健壮的帧解析器长什么样ws_recv_frame() 的完整决策流程:flowchart TD A["ws_read 读 2 字节头<br/>(优先消费 leftover)"] --> B{payload_len<br/>== 126 / 127?} B -->|126| C[再读 2 字节扩展长度] B -->|127| D[再读 8 字节扩展长度] B -->|≤125| E{masked?} C --> E D --> E E -->|是| F[读 4 字节掩码] E -->|否| G{opcode?} F --> G G -->|CLOSE 0x8| H[消费负载, 返回 0] G -->|PING 0x9| I["读负载 → 自动回 PONG<br/>(原样回显负载)"] G -->|PONG 0xA| J[丢弃负载] G -->|TEXT/BINARY| K{负载 > 应用缓冲?} K -->|是| L["分块读掉整个负载<br/>(保解析器同步) → 返回 -1"] K -->|否| M[读负载 → 解掩码 → 返回]三个容易做错的点,这里都处理了:Ping → Pong 自动应答:协议层自动回 PONG 并原样回显负载,应用层完全无感。配合 main.c 的 -k 30 参数(每 30 秒主动发一次 Ping),断线能被及时发现。超大帧跳过保持同步:如果服务端发来一个比应用缓冲区还大的帧(本项目上限 WS_MAX_FRAME_SIZE = 64KB),不能只丢弃返回——否则后续字节会被当成下一个帧头解析,整个流全部错位。正确做法是分块把负载读完再返回错误(这是 commit 9a596e6 修复的坑)。CLOSE 帧的负载也要消费:否则 close 的 payload 会污染下一条连接的首帧。六、测试体系:不需要 DPDK 也能测协议协议层与 DPDK 解耦的关键收益是可测试性——用内核 TCP socket 就能完整验证 WS 协议逻辑:# 1. 协议单测(内核 TCP 跑 WS 协议,不碰 DPDK) gcc -O2 -o test/test_ws_protocol test/test_ws_protocol.c -lpthread python3 test/ws_test_server.py 8080 # 起 Python 服务端(依赖 websockets) ./test_ws_protocol 127.0.0.1 8080 # 跑握手/echo/订阅/推送全流程 # 2. C 服务端(零依赖,通常跑在远端机器) gcc -O2 -o test/ws_echo_server test/ws_echo_server.c ./ws_echo_server 8080 # 3. 内核 TCP 对比基准(10 轮连接+握手+echo 延迟) python3 test/kern_c_test.py三层测试各司其职:协议单测快速迭代、C 服务端保证低延迟基准、内核对比脚本给出"反超内核"的证据。七、实测数据WS 协议栈在自研 TCP 上的端到端表现(PUBLISH.md):指标DPDK WS内核 TCP WS优势WS 握手3.0 ms5.4 ms快 45%WS echo RTT1.5 ms3.1 ms快 52%帧编解码吞吐(CPU 微基准)18 GbpsN/A50 万次编码八、总结WebSocket 协议本身很小:一个 HTTP 握手、一个 2-14 字节的帧头、一个 XOR 掩码。真正的工程量在边界情况——握手响应里夹带首帧、超大帧保同步、控制帧负载必须消费、粘包半包。把这些坑填平后,WS 层约 400 行代码,跑在自研 TCP 栈上,端到端延迟反超内核 52%。下一篇:微秒级性能优化实战:如何让自研 TCP 栈反超内核 52%相关源码:src/ws_client.c(握手 + 帧编解码)、src/ws_client.h(opcode 与 API)、test/test_ws_protocol.c(协议单测)、test/ws_echo_server.c(C 服务端)、test/kern_c_test.py(内核对比基准)
2026年08月16日
3 阅读
0 评论
0 点赞
2026-08-16
微秒级性能优化实战:自研 TCP 栈如何反超内核 52%
微秒级性能优化实战:自研 TCP 栈如何反超内核 52%五轮迭代(P0-P4),把 WS echo RTT 从"落后内核"打到 1.5ms vs 3.1ms,快 52%。本文记录每一轮优化的测量方法、改动内容和量化收益——包括一次 19 倍的分配性能提升。一、方法论:先测量,再动手优化最忌讳"我觉得这里慢"。本项目建立了一套三层测量体系:手段测什么工具rdtsc 打点数据路径各环节的 CPU 周期级延迟代码内埋点,rte_get_timer_cycles()tcpdump 抓包协议行为正确性(重传、乱序、窗口)tcpdump -i eno1 -nn -e 'arp or tcp port 8080'对照基准同一链路内核 TCP 的表现test/kern_c_test.py(10 轮连接+握手+echo)基线数据先摸清楚。首版跑通后,跨核数据路径上发现了第一个大问题:rx_enq → pkt_deq (跨核 ring) 0.2 μs ✓ 达标 pkt_deq → rcvbuf (协议处理) 0.4 μs ✓ 达标 rcvbuf → nrecv_seen (通知) 0.1 μs ✓ 达标 pkt_deq → fragment alloc 128 μs ✗✗✗ 超标 20 倍!热路径上的 rte_malloc 一次 128 μs——这是全部延迟的地板,也是 P0 的靶子。二、P0:Fragment Mempool——消除热路径 malloc(19× 提升)问题每个 TCP 段到达时,都要 rte_malloc 一个 ng_tcp_fragment 结构来装数据。通用分配器的开销(锁、元数据、碎片管理)在万兆流量下被放大成主要瓶颈。改造把结构体里的 *data 指针改成 flexible array data[],让"结构体 + 负载"成为一块连续内存,然后用 rte_mempool 预分配池管理:改造前: 改造后: struct ng_tcp_fragment { struct ng_tcp_fragment { ... ... unsigned char *data; ← 移除 int read_offset; ← 新增 → rte_malloc(sizeof+len) unsigned char data[]; ← 内嵌 → rte_mempool_get() 一次搞定/* src/tcp_core.h — mempool 配置 */ #define FRAG_POOL_SIZE 4096 /* 在途 fragment 上限 */ #define OFO_POOL_SIZE 256 /* 乱序段上限 */ #define FRAG_ELT_SIZE (sizeof(struct ng_tcp_fragment) + TCP_MSS_DEFAULT + 1) /* O(1) 无锁分配 */ static inline struct ng_tcp_fragment *frag_alloc(int data_len) { struct ng_tcp_fragment *f = NULL; if (rte_mempool_get(g_frag_pool, (void **)&f) < 0) return NULL; ... }rte_mempool_get 走 per-lcore 缓存,无锁、O(1)、缓存友好。收益fragment 分配: 128 μs → 6.6 μs (19 倍)一次改动把数据路径地板从 128μs 打到 6.6μs,端到端延迟立刻反超内核。三、P1:WS 帧动态栈分配——64KB 栈占用降到 ~100 字节问题首版 ws_recv_frame 在栈上固定开了一个 64KB 的接收缓冲。应用线程栈占用暴涨,也增加了 cache 污染。改造改为按需 alloca() 动态分配——小帧只占几十字节,大帧才多占:改造前: uint8_t buf[WS_MAX_FRAME_SIZE]; // 恒 64KB 改造后: 按帧头解析出的 payload_len alloca() // 20B ~ 1500B(典型行情消息)收益:典型消息的栈占用 64KB → 20~1500 字节。四、P2:read_offset——消灭 memmove问题nrecv(buf, n) 要求读 n 字节,但一个 fragment 里可能有 1500 字节。应用只取 200 字节时,首版把剩余 1300 字节 memmove 到头部,等待下次读取——每次部分读取都平移一次数据。改造加一个 read_offset 字段记录"已经读到哪里",读取时直接偏移,零平移:// 优化前: // 优化后: rte_memcpy(buf, f->data, n); rte_memcpy(buf, f->data + f->read_offset, n); memmove(f->data, f->data + n, f->length - n); f->read_offset += n; f->length -= n; f->length -= n;收益:部分读取场景下数据平移 1 次 → 0 次。WS 行情消息常常是"先读 2 字节帧头、再读 4 字节掩码、再读负载"的多次小读模式,这一改动直接命中了高频路径。五、P3:nclose 忙轮询——关闭路径零阻塞问题首版 nclose() 用 pthread_cond_timedwait 等待 FIN 确认,关闭路径引入了不可控的唤醒延迟。改造改为 rte_pause() 忙轮询:关闭是短时操作(毫秒级),自旋等待的代价远小于上下文切换:while (state != CLOSED) rte_pause(); // PAUSE 指令,低功耗自旋收益:关闭操作无阻塞抖动,配合 P0 让连接生命周期全程低延迟。六、P4:删除死代码——少 80 行 O(n) 扫描问题旧版 ng_tcp_out() 每轮迭代扫描全部 256 个哈希桶找待发送的 TCB。后来引入了 active_head 活跃连接链表,旧路径成了纯粹的死代码。改造删掉旧扫描路径(-80 行),pkt_process 定时器与发送统一走 active_head 链表遍历——只碰活跃连接。收益:空闲连接再多,每轮迭代的开销也恒定;代码更简单,make 后行为不变(这是删除代码的最高境界:删除而不改变行为)。七、五轮优化全景flowchart LR subgraph 基线["基线: WS echo RTT > 内核"] A["rte_malloc 128μs<br/>64KB 栈帧<br/>部分读 memmove<br/>condvar 关闭<br/>256 桶扫描"] end subgraph 优化["五轮迭代"] P0["P0 mempool<br/>128→6.6μs (19×)"] P1["P1 动态栈<br/>64KB→~100B"] P2["P2 read_offset<br/>memmove → 0"] P3["P3 忙轮询关闭"] P4["P4 删死代码<br/>-80 行"] end subgraph 结果["最终: 反超内核 52%"] Z["WS echo RTT 1.5ms<br/>跨核 0.2μs<br/>分配 6.6μs"] end A --> P0 --> P1 --> P2 --> P3 --> P4 --> Z最终成绩单(同一物理链路实测):指标DPDK(优化后)内核 TCP优势TCP 连接1.5 ms2.5 ms快 40%WS 握手3.0 ms5.4 ms快 45%WS echo RTT1.5 ms3.1 ms快 52%跨核传递0.2 μsN/A硬件级Fragment 分配6.6 μsN/A19× 于 mallocCPU 数据面微基准(无网卡依赖,i7-12700 / DDR5 / GCC -O2):测试项性能哈希表查找(1000 条目)2.1 ns/次 → 4.81 亿次/秒Ring Buffer SP/SC(1M fragments)26.87 Gbps → 230 万 ppsWS 帧编解码(50 万次)18.00 Gbpsrdtsc 延迟追踪(最新实测):路径延迟说明rx_enq → pkt_deq0.2 μsrte_ring 无锁跨核传递pkt_deq → rcvbuf0.4 μsTCP 11 状态机处理rcvbuf → nrecv_seen0.1 μscond 唤醒fragment 分配6.6 μsmempool O(1)八、复盘:这五轮优化教给我的事热路径上的通用设施是最大的敌人。rte_malloc 是"正确"的,但它在每包路径上就是慢。P0 的 19 倍提升来自一次"专用化":把通用分配器换成专用 mempool。优化顺序 = 数据说话。没有 rdtsc 打点,谁也不会先想到 128μs 的 malloc;有了数据,P0 是唯一的答案。小改动大收益往往藏在"看似无害"的地方。P2 的 read_offset 只加了一个字段,消灭的却是每次部分读取的内存平移。删除代码也是优化。P4 没有让任何操作变快,但让每一轮迭代都变短、让系统更可理解——为后续 feature 扫清道路。下一篇:从实验代码到产品:DPDK 行情接入网关的产品化实践相关源码:src/tcp_core.h(mempool 与 fragment)、src/tcp_socket.c(P2/P3)、src/ws_client.c(P1)、src/tcp_core.c(P4)、test/bench_tcp.c(微基准)、PERF_REPORT.md(完整数据)
2026年08月16日
2 阅读
0 评论
0 点赞
2026-08-16
从实验代码到产品:DPDK 行情接入网关的产品化实践
从实验代码到产品:DPDK 行情接入网关的产品化实践前三篇讲完了协议栈、WebSocket 和性能优化。这一篇把视角切到产品:如何把这套技术栈包装成一个金融行情接入网关(app/market_feed.c),以及它的部署方式、商用评估和演进路线。一、产品定位:金融行业最典型的低延迟场景在量化交易链路中,行情数据(股票、期货的实时报价)的到达延迟直接影响策略的成交质量。典型的接入形态:交易所 / 行情提供商 → [WebSocket 行情流] → 行情接入网关 → 下游交易系统 ↑ 本项目占据的位置网关要做的事:订阅多品种行情 → 以微秒级延迟接收 → 解析 → 转发/统计。用自研 TCP 栈的收益在这里被放大——行情消息高频、小包、长连接,正是内核协议栈开销占比最大的流量形态。二、产品功能(app/market_feed.c,约 420 行)功能说明多品种订阅通过 -s '{"action":"subscribe","symbols":[...]}' 订阅任意品种延迟直方图统计每品种 min / avg / P50 / P95 / P99 / max,微秒级消息速率跟踪每品种 msg/s 实时计算定时统计输出-t 秒可配置刷新间隔(默认 5s)优雅退出SIGINT/SIGTERM 触发有序关闭断线检测心跳 Ping + 断线统计架构与基础客户端同构(lcore0 RX/TX + lcore1 协议处理),产品层增加的是延迟测量和统计展示:flowchart LR subgraph S["行情服务端 (demo/market_data_server.py)"] S1["AAPL / GOOGL / MSFT / TSLA<br/>每条消息带 timestamp"] end subgraph G["DPDK 行情网关 (app/market_feed.c)"] L0["lcore0: RX burst +<br/>ws_recv_frame"] L0 -->|"ring→in"| L1["lcore1: TCP/WS<br/>状态机处理"] L1 -->|"rcvbuf + cond"| L0 subgraph STATS["统计层 (lcore0)"] H["延迟直方图<br/>64 桶 × 1μs 分辨率"] R["per-symbol<br/>msg/s 速率"] end L0 --> STATS STATS -->|"每 5s"| OUT["终端统计表输出"] end S1 -->|"WS 行情流 (TCP)"| G三、延迟直方图:64 桶 × 1μs 分辨率的极简实现延迟 = 消息携带的服务端 timestamp 到网关收到时刻的差值。测量与统计实现非常轻量(app/market_feed.c):#define LAT_BUCKETS 64 /* 直方图桶数 (1us 分辨率) */ typedef struct { uint64_t buckets[LAT_BUCKETS]; /* 延迟分布直方图 */ uint64_t min_us, max_us, sum_us; /* 累计值 */ uint64_t count; } symbol_stats_t;每收一条行情,算 now - msg_timestamp,buckets[min(lat, 63)]++;百分位直接在累计分布上找分位点(P50/P95/P99);min/max/avg 用累计值 O(1) 维护;输出时打印 ASCII 表格(见下文),无需外部依赖。这套实现约 100 行 C,换来了产品的核心卖点:可量化的延迟证据。四、端到端演示# 1. 编译产品 make app # → build/market_feed # 2. 启动模拟行情服务端(零依赖 Python,100 msg/s) python3 demo/market_data_server.py 8080 100 # 3. 启动 DPDK 行情网关 ./build/market_feed -l 0-1 -n 2 \ -d $DPDK_DRV/librte_mempool_ring.so \ -d $DPDK_DRV/librte_net_ixgbe.so \ -- -a <SERVER_IP> -p 8080 -l <LOCAL_IP> \ -s '{"action":"subscribe","symbols":["AAPL","GOOGL"]}' -t 5每 5 秒输出一次延迟统计表:┌────────┬──────────┬────────┬────────┬────────┬────────┬──────┐ │ Symbol │ Msgs │ Rate │ Min │ Avg │ P50 │ P99 │ ├────────┼──────────┼────────┼────────┼────────┼────────┼──────┤ │ AAPL │ 498 │ 99 │ 598 │ 821 │ 780 │ 1245 │ │ GOOGL │ 497 │ 99 │ 587 │ 815 │ 772 │ 1230 │ └────────┴──────────┴────────┴────────┴────────┴────────┴──────┘延迟口径:服务端 timestamp → 网关 rte_get_timer_cycles 的端到端时间,包含服务端序列化 + 内核 TCP 发送 + 网络传输 + DPDK 协议栈 + WS 解码全链路。五、部署清单生产部署需要三样东西(详见仓库 README / CLAUDE.md):Hugepages:echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages网卡绑定:dpdk-devbind --bind=vfio-pci <pci_addr>(DPDK 24.11 usertools)PMD 插件:运行参数 -d 指定 mempool/NIC 驱动(Makefile 按共享库链接,PMD 运行时加载)云上部署:AWS c5n.large+(ENA PMD)或 c5n.metal(SR-IOV),安全组放行行情端口。六、商用评估:什么能卖,什么不能就绪度分级梯队场景就绪度第一梯队(当前可商用)金融行情推送网关(券商/量化/交易所)★★★★★ 微服务 RPC 网关(长连接复用,10ms→1-2ms)★★★★☆第二梯队(3-6 月加固)5G 核心网 NRF、FIX 协议引擎★★★★☆第三梯队(6-12 月)CDN 边缘推送、工业 IoT 控制面★★★☆☆竞品对比维度本项目Solarflare Onload内核 TCP部署成本免费 + 2 天$2000+/lic免费WS echo 延迟1.5 ms<1 ms3.1 ms零拷贝✅✅❌可定制性高(开箱即改)中(闭源)低成熟度教学/验证级生产级30 年生产级推荐切入路径:金融行情网关(1-2 月 MVP)→ 微服务 RPC(3-4 月)→ 5G 边缘(6 月+)→ CDN 推送(12 月+)。不适合的场景(要诚实)场景原因公网 WebSocket 服务无 SYN flood / DDoS 防护浏览器客户端需要 TLS (WSS),当前无 TLS 集成百万并发连接无 epoll 模型、无连接池管理跨平台部署依赖 DPDK + hugepages + 专用 NIC七、演进路线图flowchart LR P5["P5 修复多核 RSS<br/>active_head 并发"] --> P6["P6 WS 分片发送<br/>header+payload 分离"] P6 --> P7["P7 网卡 checksum offload<br/>省 5% CPU"] P7 --> P8["P8 事件驱动替代忙轮询<br/>空闲降功耗"] P8 --> P9["P9 TSO/GRO 软件实现<br/>大消息吞吐翻倍"] M["商业路线: Prometheus 指标<br/>JSON 配置 → 多核 RSS + TLS<br/>→ HA 双活 + 热升级"] --> P5其中 P5 已有实验骨架:feature/multi-core 分支实现了 per-worker TCB 表、ring 数组与 RX 哈希分流,当前被主线程与 worker 对 active_head 链表的并发竞争阻塞(表现为 TCP 连接超时)。修好后吞吐可望随核数线性扩展。八、总结从 6000 行实验代码到可演示产品,中间只隔了 100 行直方图 + 1 张统计表 + 1 个演示脚本——但这一步把"技术能力"变成了"客户能看懂的东西"。低延迟基础设施类产品的说服力,永远来自一张带 P99 的延迟表。相关文件:app/market_feed.c(产品实现)、app/README.md(产品文档)、demo/market_data_server.py(模拟行情源)、PRODUCT_ANALYSIS.md(商用场景分析)、PERF_REPORT.md(性能数据)系列目录:① 用户态 TCP 协议栈 · ② WebSocket 实战 · ③ 性能优化实战 · ④ 产品化实践
2026年08月16日
2 阅读
0 评论
0 点赞
WebSocket高性能项目
· 第 1/1 页 · 本页 4 条 · 共 4 条