大家好,作为一名网络编程新手,我在一个月前决定从零开始构建一个基于epoll的HTTP服务器,并在此过程中对每个主要架构变化进行了基准测试。
性能基准
基准命令:
wrk -t4 -c10000 -d10s http://127.0.0.1:8080/
请求:GET /index.html
响应:静态HTML文件(约1500字节)
CPU:Intel i5-13420H(第13代)
编译器:Clang(O3)
| 架构 | 吞吐量 (req/sec) | 描述 |
|---|---|---|
| Blocking | ~15k | 单线程阻塞接受/读取/写入 |
| Epoll (LT) | ~34k | 单线程事件循环使用非阻塞I/O多路复用 |
| Epoll (LT, keep alive) | ~37.5k | 单线程事件循环,持久连接 |
| Epoll (LT, keep alive, sendfile) | ~41k | 单线程事件循环,持久连接与零拷贝文件服务 |
| Epoll (LT, keep alive, sendfile, multithreading) | ~125k | 多线程架构,运行4个并发epoll循环(在测试机器上最优) |
一些令人惊讶的观察
sendfile的效果比我预期的要小……对于一个专门用来服务文件的服务器,我原本期待更大的增益,但可能因为我的文件仅有约1.5KB,所以没有太大帮助。
更多线程反而让性能变差:
| 工作线程 | 吞吐量 (req/sec) |
|---|---|
| 1 | ~40k |
| 2 | ~95k |
| 3 | ~115k |
| 4 | ~125k |
| 5 | ~90k |
| 6 | ~90k |
| 8 | ~75k |
| 10 | ~70k |
| 12 | ~65k |
我的CPU有6个物理核心和12个逻辑处理器,我怀疑在较高线程数下,所有循环的系统调用成本、上下文切换和共享内核对象上的锁竞争占据了主导地位,尽管我还没有完全调查清楚。
使用perf进行分析
| 函数 | 近似CPU样本 |
|---|---|
| readSock() | ~22% |
| writeSock() | ~16% |
| parse() | ~8% |
| std::format() | ~7% |
| open() | ~3% |
| sendfile() | ~2.5% |
结果发现,我花在读取和解析请求上的时间仍然比发送响应的时间要多,这意味着未来可能还可以尝试批量读取或缓冲池。
最后思考
我可以继续寻找微优化的可能性,甚至实验边缘触发架构,但此时我有些疲惫,这个项目在此结束似乎是个不错的选择。代码库相对较小(约1k LOC),如果有人感兴趣,可以查看: GitHub 代码库
博主点评: 本文展示了基于epoll的HTTP服务器优化过程中的关键技术挑战与解决方案,尤其是对多线程性能的深入分析,值得开发者们借鉴。通过对性能瓶颈的细致剖析,可以为未来的优化方向提供重要思路。