Appearance
实验 2.3:syscall 归因 — 每个系统调用花多少钱?
所属 Day:Day 2 — epoll/多进程/短连接压测 前置依赖:长跑稳态 更新时间:2026-08-20(重构:从原 Day 2 单篇 128KB 文档拆分,对应原 §7.3 + §8.4 核心数据) 定位:本实验回答"QPS 为什么只有 ~10K",用 strace 把每个请求的内核路径拆开
一、本实验要回答的问题
Q:单次 echo 请求为什么耗时 ~110μs?钱花在哪个系统调用上?
理论估算 QPS 上限 ≈ 1Mμs / 每请求耗时。只要把"每请求耗时"拆开,就知道瓶颈在哪——也能预判 Day 3 长连接(消除 close/connect)的收益。
二、实验设计
- 理论分解:从 TCP 生命周期出发,估算单请求 kernel path 开销
- 实测验证:
strace -c追踪 LT 服务端 100 连接串行测试,三次运行取均值
bash
# 服务端侧 strace(100 连接 × 1 轮)
strace -f -c -o lt-syscall.sum ./echo-epoll-lt-server
# 客户端压测
./echo-bench 127.0.0.1 9988 100 1三、实验数据
3.1 理论分解:单次请求的 kernel path
bash
TCP 三次握手 (10–20 μs)
→ accept() 出队 (5–10 μs)
→ epoll_wait 返回 EPOLLIN (5–10 μs)
→ read() 读取请求 (3–5 μs)
→ epoll_ctl(MOD, EPOLLOUT) 切换状态 (5–10 μs)
→ write() 发送响应 (3–5 μs)
→ epoll_ctl(MOD, EPOLLIN) 切回 (5–10 μs)
→ read() 收到 FIN=0 (3–5 μs)
→ epoll_ctl(DEL) + close() (5–10 μs)
TCP 四次挥手 (10–20 μs)
───────────────────────────
合计:≈ 55–105 μs / 请求理论 QPS 上限 = 1Mμs / 55-105μs ≈ 9.5K-18K——与实测值高度吻合。
3.2 LT 版 strace 实测(100 req × 3 跑均值)
| syscall | %time | 次/连接 | 单次耗时 | 累计/请求 |
|---|---|---|---|---|
| close | 25.08% | 1 | 25–33 μs | ~30 μs |
| write | 17.42% | 2 | 8–11 μs | ~20 μs |
| epoll_wait | 16.13% | 2 | 8–10 μs | ~20 μs |
| accept | 15.32% | 1 | 7–9 μs | ~8 μs |
| read | 13.58% | 2 | 6–9 μs | ~16 μs |
| epoll_ctl | 12.48% | 2 | 6–8 μs | ~14 μs |
| 合计 | 100% | 11 | — | ≈110 μs |
3.3 ET 版 strace 实测(100 req × 3 跑均值)
| syscall | %time | 次/连接 | 单次耗时 | 累计/请求 |
|---|---|---|---|---|
| close | 25.49% | 1 | 28–32 μs | ~31 μs |
| write | 17.47% | 2 | 10–11 μs | ~21 μs |
| epoll_wait | 16.10% | 2 | 9–10 μs | ~20 μs |
| accept | 15.41% | 1 | 8–9 μs | ~9 μs |
| read | 13.21% | 2 | 7–8 μs | ~16 μs |
| epoll_ctl | 12.32% | 2 | 7 μs | ~14 μs |
| 合计 | 100% | 11 | — | ≈111 μs |
3.4 ET vs LT(strace 视角)
| 指标 | LT | ET | 差异 |
|---|---|---|---|
| syscall 总次数 | 11 | 11 | 相同 |
| 单请求总耗时 | ~110 μs | ~111 μs | < 1% |
| 总耗时(100 req) | ~12 ms | ~12.27 ms | +2% |
四、实验分析
4.1 三个核心发现
① close 最贵(25%):回收 socket 结构、清理 epoll、释放 skb——Keep-Alive 省的就是这个。单次 25-33μs,占每请求 110μs 的四分之一。
② I/O 本身不贵:read/write 合计 4 次但仅占 30%——数据拷贝不是瓶颈。echo 只有 12 字节,skb 拷贝微乎其微。
③ epoll_wait 占 16%:串行场景下每次最多 1-2 个事件,唤醒/阻塞切换开销显著——但这是串行测试模型的特有开销,并发场景会摊薄。
4.2 瓶颈分层
bash
TCP 握手/挥手(三次握手 + 四次挥手): 30-50% ← 协议栈工作
epoll syscall(wait/ctl/accept): 40-50% ← 事件驱动开销
真正的 read/write: 10-15% ← 应用层数据路径瓶颈在内核协议栈,不在应用层。服务端每请求 11 次 syscall、~110μs 里,应用层的 read/write 只占 ~36μs(33%)。
4.3 strace 看不到 LT/ET 差异
100 req 短跑下 LT/ET 的 syscall 次数(11)和耗时(~110/111μs)几乎相同——差异需 10000 req 长跑 + 完整 syscall 统计才能暴露。这印证了短跑"三架构打平"的结论:短连接场景的每请求成本由固定 syscall 节奏主导,与 epoll 通知模式无关。
五、实验结论
| # | 结论 | 证据 |
|---|---|---|
| 1 | 每请求 11 次 syscall、~110μs | strace LT/ET 均为 11 次、110/111μs |
| 2 | close() 最贵(25%,25-33μs/次) | 最直接的 Day 3 长连接动因 |
| 3 | 瓶颈在内核协议栈(握手/挥手 30-50%),不在应用层 | read/write 仅占 30% |
| 4 | strace 100 req 看不到 LT/ET 差异 | 两者 syscall 数/耗时几乎相同 |
| 5 | 理论 QPS 上限 9.5K-18K 与实测吻合 | 55-105μs/req 换算 |
六、回答开头的问题
Q:单次 echo 请求为什么耗时 ~110μs?钱花在哪个系统调用上?
答:11 次 syscall 合计 ~110μs。最贵的是 close()(25%,25-33μs/次)——回收 socket 结构、清理 epoll、释放 skb;其次是 write(17%)、epoll_wait(16%)、accept(15%)、read(14%)、epoll_ctl(12%)。TCP 握手/挥手占每请求成本 30-50%——瓶颈在内核协议栈而非应用层。这个分解直接预测了 Day 3:消除 close()/connect() 后每请求可省 ~30-40μs + 2 个 RTT(本地 ~10μs)→ 预期 QPS 提升 2-5×(Day 3 实验 3.1 验证了 ET 2.15×)。
一句话总结:strace 把每请求 ~110μs 拆开了——close() 最贵占 25%、TCP 握手/挥手占 30-50%、应用层 read/write 只占三成,瓶颈在内核协议栈,这直接锁定了 Day 3 长连接的优化目标。