Appearance
实验 2.2:长跑稳态 — 10000 请求下 LT vs ET + 失败模式
所属 Day:Day 2 — epoll/多进程/短连接压测 前置依赖:短跑基线(说明短跑盲区) 更新时间:2026-08-20(重构:从原 Day 2 单篇 128KB 文档拆分,对应原 §7.2 + §7.4) 配套实验:syscall 归因
一、本实验要回答的问题
短跑测不出架构差异(三架构 QPS 打平)。本实验把请求量放大 100 倍到 10000 请求:
Q1:10000 req 稳态下 LT vs ET 到底差多少?Q2:为什么连跑几轮会出现 ~18% 的失败?失败是 LT/ET 的 bug 吗?
二、实验设计
- 工具:
echo-bench(100 连接 × 100 轮 = 10000 请求,串行) - 机器:20 核虚拟机(
shrdlab31),ip_local_port_range = 32768-60999(可用临时端口 ≈ 28231) - 流程:LT 连续 3 跑(每跑间不等待 TIME-WAIT 过期);ET 连续 4 跑(每跑间手动等待 >60s 让 TIME-WAIT 自然过期)
- 每跑前后记录
ss -tan | grep TIME-WAIT | wc -l
三、实验数据
3.1 LT 版(10000 req × 3 跑,连续不等待)
| 运行 | QPS | P50 | P90 | P99 | P999 | max | 成功率 | 跑前 TW | 跑后 TW |
|---|---|---|---|---|---|---|---|---|---|
| Run 1 | 17625.1 | 46 μs | 83 μs | 110 μs | 137 μs | 234 μs | 10000/10000 | 0 | 10000 |
| Run 2 | 15082.7 | 58 μs | 88 μs | 111 μs | 139 μs | 218 μs | 10000/10000 | 10000 | 20000 |
| Run 3 | 2146.2 ⚠️ | 38 μs | 64 μs | 111 μs | 129 μs | 227 μs | 8225/10000 ⚠️ | 20000 | 28225 |
3.2 ET 版(10000 req × 4 跑,每跑间等待 TIME-WAIT 过期)
| 运行 | QPS | P50 | P90 | P99 | P999 | max | 成功率 | 跑前 TW | 跑后 TW |
|---|---|---|---|---|---|---|---|---|---|
| Run 1 | 15852.5 | 54 μs | 88 μs | 113 μs | 136 μs | 257 μs | 10000/10000 | 28225(继承) | 20000 |
| Run 2 | 22931.9 | 37 μs | 45 μs | 113 μs | 135 μs | 240 μs | 10000/10000 | 20000 | — |
| Run 3 | 11474.9 | 83 μs | 109 μs | 135 μs | 178 μs | 278 μs | 10000/10000 | — | 10016 |
| Run 4 | 19178.4 | 42 μs | 72 μs | 109 μs | 123 μs | 218 μs | 10000/10000 | 10016 | — |
3.3 瞬态型失败(独立观测,Run 1 失败)
| 运行 | QPS | 成功率 | P50 | P90 | P99 | P999 | max |
|---|---|---|---|---|---|---|---|
| Run 1 | 1344.2 | 75% | 63 μs | 86 μs | 126 μs | 156 μs | 2147 μs |
| Run 2 | 14691.3 | 100% | 62 μs | 73 μs | 101 μs | 137 μs | 221 μs |
| Run 3 | 13668.7 | 100% | 65 μs | 82 μs | 106 μs | 132 μs | 227 μs |
四、实验分析
4.1 LT Run 3 失败根因:客户端 TIME-WAIT 端口耗尽(累积型)
bash
TIME-WAIT 累积(每跑完一次跑下一跑,没有重启)
─────────────────────────────────────────────
Run 1 跑后: 10000 ← 仅 Run 1 留下的 10000 个
Run 2 跑后: 20000 ← Run 1+2 累积 20000
Run 3 跑后: 28225 ← 接近 28231 端口上限!剩下 6 个可用
客户端 connect() 随机选用本地临时端口:
→ 选到仍在 TIME_WAIT(60s) 的端口 = EADDRINUSE
→ connect() 失败 = 请求无法发出
→ Run 3 失败 1775 / 10000 = 17.75%根因不是服务端,而是客户端临时端口耗尽。详见 concepts/network/tcp-timewait.md。
ET 4 跑全过的反证:ET 跑之间手动等待 >60s 让 TIME-WAIT 自然过期,所以都没碰端口上限。两次实验唯一变量是"每跑间是否等待",结果 LT 失败、ET 全过——这正是端口耗尽论的直接验证。
4.2 LT vs ET 长跑对比
| 维度 | LT | ET | 结论 |
|---|---|---|---|
| 最佳 QPS | 17625.1 | 22931.9 | ET 峰值更高但不可靠 |
| 均值 QPS | 17625.1 (Run 1) | 17359.4 (4 跑均值) | 差异 < 2%,几乎持平 |
| QPS 波动 | ±15% | ±50%(极差 2 倍) | LT 稳定得多 |
| P50 | 46 μs | 37-83 μs | ET 中位数更优但波动大 |
| P99 | 110 μs | 109-135 μs | 相当 |
| P999 | 137 μs | 123-178 μs | ET 长尾控制略好 |
| 累积失败 | ✅ Run 3 触发 | ❌ 全部通过 | 根因是 TIME-WAIT,非模式差异 |
均值打平(< 2%):20 核新机器 CPU 缓存/内存带宽更宽,EPOLLET "减少重复通知"的微优势被稀释(旧机器曾测到 ET +6.8%,见 deep-dive.md)。
波动差异显著(±15% vs ±50%):LT "水平触发"每次 epoll_wait 主动查询 fd 状态 → 唤醒次数多但时机稳定;ET "边缘触发"只在状态变化时通知 → 唤醒次数少但第一次唤醒的循环深度随机(依赖缓冲区数据量)→ QPS 波动大。
4.3 两种失败模式的统一分析
| 维度 | 累积型(Run 3) | 瞬态型(Run 1) |
|---|---|---|
| 失败出现在 | Run 3(前两次正常) | Run 1(后两次正常) |
| 失败率 | ~18% | 25% |
| QPS 变化 | 15K → 1.9K(-88%) | 1.3K → 14.7K(+10.9×) |
| max 异常 | 正常(~300 μs) | 2147 μs(10× 正常) |
| 可重现性 | 稳定重现 | 偶发 |
| 根因 | TIME-WAIT 端口耗尽(内核状态累积污染) | page cache 冷启动 / CPU 节能态 / virtio-net 重新协商 |
关键认知:累积型是可重现的确定性问题(内核 TIME-WAIT 行为),瞬态型是偶发的环境问题(系统冷启动)。测试设计必须显式分离首次运行与稳态运行——Run 1 单独记录、Run 2-5 取均值,否则会错误地将冷启动噪声归因于代码问题。
五、实验结论
| # | 结论 | 证据 |
|---|---|---|
| 1 | LT/ET 长跑均值打平(< 2%),QPS ~17.6K/17.4K | 20 核机器稳态数据 |
| 2 | LT 波动 ±15% 远小于 ET ±50% | 4 跑 ET 11.5K↔22.9K |
| 3 | LT Run 3 失败 17.75% = 客户端 TIME-WAIT 端口耗尽,非服务端 bug | ET 等待过期后 4 跑全过 |
| 4 | 失败分累积型/瞬态型,均与 LT/ET 无关 | 两型根因都是环境/内核状态 |
| 5 | Run 3 的 P50 变好是幸存者偏差(失败请求不计入延迟) | 测延迟必须先排除失败请求 |
六、回答开头的问题
Q1:10000 req 稳态下 LT vs ET 到底差多少?
答:均值几乎持平(LT 17.6K vs ET 17.4K,差异 < 2%)——但 QPS 稳定性差距显著(LT ±15% vs ET ±50%)。ET 的优势不在平均 QPS,而在峰值(22.9K)和尾延迟控制(P999 123-178μs);LT 的优势是鲁棒性。两者在短连接场景差异很小,真正的分野要等长连接(Day 3)。
Q2:为什么连跑几轮会出现 ~18% 的失败?是 LT/ET 的 bug 吗?
答:不是 bug。LT 连续 3 跑不等待 → TIME-WAIT 累积到 28225(逼近 28231 端口上限)→ connect() EADDRINUSE → 17.75% 失败;ET 每跑间等待 60s 让 TIME-WAIT 过期 → 4 跑全过。唯一变量是"是否等待",直接证明根因是客户端临时端口耗尽——这是测试方法学问题(tcp-timewait.md),也是 Day 3 长连接(消除每请求 close)要解决的问题。
一句话总结:长跑把短跑测不出的差异和陷阱都暴露了——LT/ET 均值打平但波动差 3 倍;~18% 的失败不是 bug 而是 TIME-WAIT 端口耗尽,这为 Day 3 长连接实验提供了直接动因。