解决MySql布尔型新旧版本兼容问题,采用枚举来表示布尔型的数据表。由正向工程赋值
|
# 流式架构性能测试报告
> **这份报告想回答什么**:本轮重构引入的"数据管道(Pipe)+ 帧泵 + 流式发送(SendAsync(Stream))+ HTTP 流式响应"这套新架构,每帧、每块到底花多少时间、分配多少内存?分块多大最划算?优化前后差多少?
>
> **数据来源**:本机 BenchmarkDotNet 实测(2026-09-17)。基准代码:`Benchmark/StreamingBenchmarks/` 下四个基准类(`PipeBenchmark`、`SessionStreamBenchmark`、`HttpStreamBenchmark`、`MessageFrameBenchmark`)。原始结果:`BenchmarkDotNet.Artifacts/results/Benchmark.StreamingBenchmarks.*`。
>
> **后续变更(2026-10-01)**:出站发送队列(`SendPipe`/`SendPump`)已整块移除,`SendAsync(Stream)` 改为“读一块写一块”直发(64KB 分块与文件零拷贝发送不变)。本报告中的数据仍是当时的实测值:结论 1(分块大小是瓶颈)与结论 4/5(帧定界、每帧结构性分配)不受影响;结论 3 的写侧回压 API(`FlushAsync`)已不存在,写侧背压现由内核发送缓冲承担(后续:2026-10-01 加回可选的发送队列 `SendQueue`/`SendQueuedAsync`,累积到水位时入队等待;默认直发路径不变)。重测计划见 §1 末尾。
>
> **先给结论(人话版)**:
>
> 1. **瓶颈是"分块大小",不是管道**。流式发送按 16KB 分块时,吞吐只有 64KB 分块的一半(回环 1MB:1138µs → 492µs,2.3×);改成 64KB 后,`SendAsync(Stream)` 与"最优手工分块"、"一次整段发送"三者持平。
> 2. **纯包级零拷贝回环恒定约 85ns**,从 64B 到 64KB 与数据大小无关;写入缓冲拷贝路径按大小线性(64KB ≈ 1.34µs,与《内存分配与拷贝成本报告》的拷贝成本一致)。
> 3. **写侧回压是"零成本保险"**:`FlushAsync(waitForResume:true)` 在未暂停时只要 28ns、零分配;只有真正积压时才挂起等待。
> 4. **帧定界几乎免费**:SRMP `TryParse` 单段 11~13ns、头部跨段 30~36ns,全部零分配;WebSocket 定界 8~14ns 零分配。
> 5. **每帧结构性分配很小但存在**:管道段节点 56B/帧、头先行绑定多一个限长读取器 32B/帧、消息头部包 80B/消息。属"每帧一次"的固定成本,非按数据量放大。
---
## 1. 测试目标
- 量化流式架构四层的单次成本与内存分配:管道层(Pipe,入站)、会话发送层(`TcpSession.SendAsync(Stream)`,直发)、消息帧层(`TryParse` / 头先行绑定 / 头部包构建)、HTTP 流式响应(`BodyStream`)。
- 对比三条发送路线:流式管道 / 手工分块 / 整段发送。
- 验证数据驱动优化(分块粒度 16KB → 64KB)的前后差异。
- 定位剩余分配热点,评估是否值得继续优化。
## 2. 测试环境
| 项 | 值 |
|---|---|
| 操作系统 | Windows 10 22H2(10.0.19045.6456) |
| CPU | Intel Core i9-10900K 3.70GHz(10 物理核 / 20 逻辑核) |
| 运行时 | .NET 10.0.12(SDK 10.0.401),X64 RyuJIT x86-64-v3 |
| 基准框架 | BenchmarkDotNet 0.15.8 |
| 网络口径 | loopback(127.0.0.1),客户端与服务端共享 CPU |
> **读表提醒**:`Allocated` 列为基准线程(客户端线程)的分配。Socket 类基准中服务端工作发生在接收线程,不计入该列;分配对比以微基准(4.1)/(4.4)为准。
## 3. 基准设计
| 基准 | 覆盖 | 维度 | 作业配置 |
|---|---|---|---|
| `PipeBenchmark` | 包级零拷贝回环 / 写入缓冲拷贝回环 / 背压提交快路径 | 64B / 4KB / 64KB | warmup 2 + 10 迭代 |
| `SessionStreamBenchmark` | `SendAsync(Stream)` / 手工 16KB 分块 / 手工 64KB 分块 / 整段发送 | 1MB / 8MB(回环 TCP) | warmup 2 + 5 迭代 |
| `HttpStreamBenchmark` | `BodyStream` 流式响应 / `Body` 整段响应 | 1MB / 8MB(回环 HTTP) | warmup 2 + 5 迭代 |
| `MessageFrameBenchmark` | SRMP 定界(单段/跨段)、头先行绑定、整帧切出、头部包构建、WS 定界 | 64B / 4KB / 64KB 负载 | warmup 2 + 10 迭代 |
复现命令:
```bash
dotnet run --project Benchmark/Benchmark.csproj -c Release -- --filter "*PipeBenchmark*"
dotnet run --project Benchmark/Benchmark.csproj -c Release -- --filter "*SessionStreamBenchmark*"
dotnet run --project Benchmark/Benchmark.csproj -c Release -- --filter "*HttpStreamBenchmark*"
dotnet run --project Benchmark/Benchmark.csproj -c Release -- --filter "*MessageFrameBenchmark*"
```
## 4. 测试结果
### 4.1 Pipe(管道写读回环)
| 方法 | 大小 | Mean | Allocated | 说明 |
|---|---:|---:|---:|---|
| Append/Read 回环(包级零拷贝) | 64B | 85.4 ns | 96 B | `Append` 所有权转移不拷贝 |
| Append/Read 回环(包级零拷贝) | 4KB | 85.4 ns | 96 B | 与大小无关 |
| Append/Read 回环(包级零拷贝) | 64KB | 86.7 ns | 96 B | 与大小无关 |
| WriteAsync 回环(写入缓冲拷贝) | 64B | 156.4 ns | 136 B | `PipeWriter` 形态,含拷贝 |
| WriteAsync 回环(写入缓冲拷贝) | 4KB | 216.1 ns | 136 B | 拷贝成本随大小增长 |
| WriteAsync 回环(写入缓冲拷贝) | 64KB | 1,337.1 ns | 136 B | ≈ 64KB 拷贝成本(对照另报告 ~1.05µs) |
| 背压提交快路径(未暂停) | — | 28.0 ns | 0 B | `FlushAsync(waitForResume:true)` |
**分析**:
- 包级路径恒定 ~85ns、零数据拷贝,验证零拷贝设计成立;写入缓冲路径的增量 ≈ 纯拷贝成本,两条路线分工明确(有包用包、有跨度用缓冲)。
- **背压是零成本保险**:开启"可挂起提交"后,未触发暂停时提交 28ns、零分配;只有真正到达暂停水位才付出一次挂起/唤醒(约 1µs 级)。
- 每回路 96B 分配构成:段节点 56B(`PacketSequenceSegment`,每追加一帧一个)+ 结构体装箱 40B(基准用 `ArrayPacket` 作 `IPacket` 入参所致;真实网络接收路径传入的是拥有句柄 `OwnerPacket`(class),无此装箱)。
### 4.2 会话流式发送(回环,1MB / 8MB)
**优化前(16KB 分块)→ 优化后(64KB 分块,`SendAsync(Stream)`)**:
| 方法 | 大小 | Mean(前) | Mean(后) | 提升 | Allocated(后) |
|---|---:|---:|---:|---:|---:|
| `SendAsync(Stream)` | 1MB | 1,156.9 µs | **510.3 µs** | **2.27×** | 12.4 KB |
| `SendAsync(Stream)` | 8MB | 9,471.8 µs | **4,081.6 µs** | **2.32×** | 99.2 KB |
**同场对照(优化后)**:
| 方法 | 1MB | 8MB | Allocated(1MB/8MB) |
|---|---:|---:|---:|
| `SendAsync(Stream)`(64KB 分块 + 管道) | 510.3 µs | 4,081.6 µs | 12.4 KB / 99.2 KB |
| 手工分块 16KB(无管道) | 1,160.8 µs | 9,610.5 µs | 10.0 KB / 80.2 KB |
| 手工分块 64KB(无管道) | 503.4 µs | 3,907.6 µs | 10.0 KB / 80.5 KB |
| 整段发送(一次 `Send`) | 520.4 µs | 8,477 µs(方差大) | 11.0 KB / 88.7 KB |
**分析**:
- **16KB→64KB 分块是本轮最大的性能改进(2.3×)**,来源是两端 syscall/唤醒次数减少到 1/4;64KB 后 `SendAsync(Stream)` 与手工 64KB 分块持平(差距 ≈ 1%,即**管道自身开销约 1~2%**),与整段发送持平。
- 分配随分块数线性(1MB = 16 块 → 12.4KB,约 780B/块:拥有句柄 + 段节点 + 泵取窗),**数据本身零拷贝**(分配不随数据量放大)。
- 8MB 的"整段发送"本次方差极大(±2.6ms),不作对比依据;同表其余三项稳定。
### 4.3 HTTP 流式响应(回环)
**优化前(16KB 缓冲)→ 优化后(64KB 缓冲)**:
| 方法 | 大小 | Mean(前) | Mean(后) | 提升 |
|---|---:|---:|---:|---:|
| `BodyStream` 流式响应 | 1MB | 1,567.1 µs | **804.8 µs** | **1.95×** |
| `BodyStream` 流式响应 | 8MB | 10,597.7 µs | **4,906.8 µs** | **2.16×** |
**同场对照(优化后)**:
| 方法 | 1MB | 8MB |
|---|---:|---:|
| `BodyStream` 流式响应(64KB 分块) | 804.8 µs | 4,906.8 µs |
| `Body` 整段响应(一次 Build 发送) | 602.9 µs | 4,362.8 µs |
**分析**:
- 流式响应仍比整段慢 12%(8MB)~33%(1MB)——多出的 syscall 与两端调度是明码标价的;换来的是**服务端零物化**:整段路径每次请求要拷贝一整份响应体(1~8MB)并持有整段缓冲,流式路径恒为 64KB 池化块(无论响应多大)。
- 大文件/仪表盘等场景选流式(内存有界),小响应用 `SetResult` 整段(更低延迟)。
### 4.4 消息帧层
| 方法 | 64B | 4KB | 64KB | Allocated |
|---|---:|---:|---:|---:|
| SRMP `TryParse`(单段) | 11.61 ns | 10.96 ns | 12.78 ns | 0 B |
| SRMP `TryParse`(头部跨段) | 29.99 ns | 29.81 ns | 36.23 ns | 0 B |
| 头先行绑定(`TryReadHeader`) | 150.4 ns | 145.0 ns | 149.1 ns | 128 B |
| 整帧切出(`TryReadFrame`) | 94.8 ns | 92.9 ns | 96.3 ns | 136 B |
| 头部包构建(`BuildHeader`) | 25.4 ns | 25.2 ns | 26.6 ns | 80 B |
| 整帧构建(`Build`) | 33.2 ns | 37.8 ns | 36.9 ns | 80 B |
| WebSocket `TryParse` | 8.3 ns | 13.7 ns | 10.9 ns | 0 B |
**分析**:
- **帧定界便宜到可以忽略**(单段 ~11ns 零分配):一条消息走一次定界,百万消息/秒也只占 1% CPU。
- 头先行绑定(150ns)比整帧切出(95ns)贵约 55ns:多一个限长读取器(32B)与窗口裁剪;这是"头到齐即交付、负载流式读"的能力溢价,值。
- 两个构建方法(`BuildHeader`/`Build`)的 80B 为 `OwnerPacket` + `ArrayOwner` 的固定对象成本(每消息一次),头部包与整帧一致。
- 分配扣除基准侧的 40B 装箱后:段节点 56B/帧、限长读取器 32B/帧 —— 都是"每帧一次"的固定成本。
## 5. 发现与行动
### 5.1 优化 1:流式分块 16KB → 64KB(已实施)
| 代码点 | 修改 |
|---|---|
| `SendPump.StreamChunkSize` | 16KB → 64KB(`SendAsync(Stream)` 读块) |
| `HttpSession.SendStreamBody` 缓冲 | 16KB → 64KB |
| `SocketRemoteHelper.Send(ISocketRemote, Stream)` 默认缓冲 | 8192 → 64KB(同步流发送路径同规则) |
理由:回环实测 16KB 分块的吞吐只有 64KB 的一半(2.3×差距),瓶颈在两端 syscall/唤醒次数而非管道;64KB 仍远低于 LOH 阈值(85KB),对内存画像无影响。收益:`SendAsync` 1MB 2.27×、8MB 2.32×;HTTP 流式 1.95× / 2.16×。
### 5.2 缺陷修复:回环流式测试改为并发排空(已实施)
性能测试期间发现 `SendPipeTests` 三个大负载回环用例存在设计缺陷:发送期间客户端不读取,整段依赖内核缓冲容量"恰好装得下";满套件并行负载下缓冲不足 → 发送泵阻塞 → 会话超时关闭 → `Send pipe has been completed` 异常。已改为**客户端并发排空**(先启动读取任务再发送),消除对内核缓冲容量的依赖。
### 5.3 候选未实施:管道段节点池化(56B/帧)
- 数据:每帧追加产生一个 `PacketSequenceSegment`(56B);RPC 热点按 10 万帧/秒估算约 5.6MB/s Gen0 垃圾。
- 收益:中小;代价:段对象回收需保证"旧读取窗口不可再被引用"的租期契约(BCL Pipelines 同样做池化,但契约更严),核心管道引入对象复用的正确性风险偏高。
- 结论:本轮不做,记录备查;若后续出现"小帧 + 百万级 tps"实测瓶颈再评估。
## 6. 结论汇总
| 结论 | 数据 |
|---|---|
| 包级零拷贝回环恒定 | 85ns(64B~64KB 相同) |
| 管道自身开销 | ≈ 1~2%(64KB 分块 vs 手工分块) |
| 流式发送分块 16KB→64KB | 2.3× 吞吐提升,此后与整段发送持平 |
| 背压提交(未暂停) | 28ns / 0B |
| SRMP 帧定界 | 11ns(单段)/ 30ns(跨段)/ 0B |
| HTTP 流式 vs 整段 | 慢 12~33%,换服务端零物化(恒定 64KB 内存) |
| 每帧固定分配 | 段节点 56B + (头先行)限长读取器 32B |
**与其他性能资料互链**:《内存分配与拷贝成本报告》(拷贝/分配单价)、《网络库编解码器 Echo 性能测试报告》(请求-响应全链路吞吐)、《IPacket 性能测试报告》(数据包原语)、《数据管道 Pipe》(契约设计)。
---
*本报告由基准代码 `Benchmark/StreamingBenchmarks/` 实测生成;同机同口径可直接复现。*
|