Hical 性能优化全记录

优化背景 Hical 是我写的 C++20/26 Web 框架,跑 Hello World 压测时起初只有 27K QPS,而同类框架(Cinatra 165K、Drogon 170K)差了将近一个数量级。目标很明确:追平 Cinatra/Drogon 的水平。 整个优化过程分 6 个阶段,不是拍脑袋乱改,每一步都是 perf + 火焰图定位瓶颈 → 想方案 → 写代码 → 跑压测验证 的循环。能看到数字变化才算数。 阶段 1:协程帧削减(v2.5.1-v2.5.2) 发现问题 perf 火焰图第一个大头:14.5% CPU 在 scheduler::wake_one_thread_and_unlock + pthread_cond_signal。 一开始以为是跨线程调度问题,仔细一看不是——是 Boost.Asio scheduler 每次 co_await resume 都要走的内部调度流程太重了。一个 Hello World 请求居然走了 4 个协程帧: 1 2 3 4 5 handleSession: co_await async_read → 帧 1(必需,I/O 等待) co_await router_.dispatch() → 帧 2(Router 本身是协程) co_await handler(req) → 帧 3(同步 handler 被包装成协程,不必要!) co_await async_write → 帧 4(必需,I/O 等待) 帧 1 和 4 是真正的 I/O 等待不可消除,但帧 2 和 3 完全是浪费——一个同步的 return HttpResponse("Hello") 被裹了两层协程。 ...

May 22, 2026 · 9 min · 1794 words

深入学习 Boost.Asio(三):协程进阶与实战项目

系列导航:入门篇 | 进阶篇 | 实战篇 前置知识 阅读本篇前,请确保已掌握: 入门篇:io_context、异步操作生命周期、定时器 进阶篇:协程 Echo Server、多线程模型、strand 1. 协程进阶技巧 1.1 co_spawn 的第三个参数 co_spawn 的第三个参数决定了协程完成后的行为: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <boost/asio.hpp> #include <boost/asio/co_spawn.hpp> #include <boost/asio/detached.hpp> #include <boost/asio/use_awaitable.hpp> using boost::asio::awaitable; using boost::asio::use_awaitable; awaitable<int> compute() { co_return 42; } awaitable<void> mayFail() { throw std::runtime_error("oops"); co_return; } void examples(boost::asio::io_context& ioCtx) { // 方式1:detached —— 忽略返回值和异常 // 适用:独立运行的协程(如连接处理) boost::asio::co_spawn(ioCtx, compute(), boost::asio::detached); // 方式2:回调 —— 协程完成时执行回调 // 适用:需要捕获协程异常或获取返回值 boost::asio::co_spawn(ioCtx, mayFail(), [](std::exception_ptr e) { if (e) { try { std::rethrow_exception(e); } catch (const std::exception& ex) { std::cerr << "协程异常: " << ex.what() << "\n"; } } }); // 方式3:use_awaitable —— 在协程中等待另一个协程 // 适用:父子协程关系 // (需要在协程内使用) } // 方式3 完整示例 awaitable<void> parent(boost::asio::io_context& ioCtx) { // 等待子协程完成并获取返回值 int result = co_await boost::asio::co_spawn( ioCtx, compute(), boost::asio::use_awaitable); std::cout << "子协程返回: " << result << "\n"; // 42 } 1.2 超时控制 生产环境中,你不能无限等待一个操作完成。Asio 提供了 awaitable_operators 实现竞争式等待: ...

May 21, 2026 · 10 min · 1926 words

Hical 生产部署实践:从编译优化到 Kubernetes 容器化

Hical 生产部署实践:从编译优化到容器化 框架开发完了,测试也通过了——然后呢?“本地跑得好好的"和"线上稳定运行"之间,隔着编译优化、进程管理、反向代理、监控告警、容器编排一整套工程实践。这篇文章把 Hical 从开发环境搬到生产环境的完整链路走一遍,每个环节都给出可直接复用的配置模板。 目录 Hical 生产部署实践:从编译优化到容器化 目录 一、编译优化:榨干最后一点性能 1.1 Release 基础参数 1.2 LTO(链接时优化) 1.3 PGO(Profile-Guided Optimization) 1.4 静态链接 vs 动态链接 二、进程管理:别让服务裸奔 2.1 systemd 服务配置 2.2 信号处理与 Graceful Shutdown 2.3 多线程与多 acceptor(SO_REUSEPORT) 三、反向代理:Nginx 挡在前面 3.1 HTTP 反向代理 3.2 WebSocket 代理 3.3 SSL 终止策略 四、监控与可观测性 4.1 Prometheus 指标暴露 4.2 日志接入 ELK / Loki 4.3 健康检查端点 五、容器化部署 5.1 多阶段 Dockerfile 5.2 docker-compose 完整示例 5.3 Kubernetes 部署参考 六、性能调优检查清单 系统级 Hical 应用级 PMR 内存池 数据库连接池 日志系统 调优流程 一、编译优化:榨干最后一点性能 1.1 Release 基础参数 开发阶段用 Debug 方便调试,上线必须切 Release。区别不只是 -O2,还有 assert 消除、NDEBUG 定义(Hical 的 HICAL_LOG_TRACE 宏在 NDEBUG 下编译期完全消除): ...

May 17, 2026 · 15 min · 2992 words

连接级 Atomic 时间戳超时的实现决策

起因 最初 Hical 的空闲超时实现就是传统做法:每个 HTTP 请求/每次 keep-alive 读等待都注册一个 steady_timer,读完成后取消,超时则关闭连接。实现上用的是 shared_ptr<function> 自引用环做回调链续期——每连接 2 次堆分配(shared_ptr 控制块 + function 对象),且每次续期都要重新构造回调。 v2.5.2 压测到 132K QPS 时,做热路径Review代码发现这个 timer 机制的问题: 每请求 2 次 epoll_ctl(注册 + 取消 timer) shared_ptr<function> 自引用环本身就有堆分配开销 140K QPS 下整体约产生 100 万次 epoll_ctl/sec,38% CPU 花在内核 _raw_spin_unlock_irqrestore(TCP spin_lock),而用户态框架代码只占不到 5% 瓶颈已经从用户态转移到内核态,减少进内核的次数成为核心策略。空闲超时的 timer 是明确可以砍掉的——30-60s 的超时精度要求本来就极低。 改良过程 分两步走: 第一步:先把 shared_ptr<function> 回调链改为独立协程 idleTimerLoop,消除自引用环和 2 次堆分配。这一步还是 per-connection 一个 timer 协程,只是实现更干净了。 第二步:发现即便使用协程,per-connection timer 仍然意味着每次 timer 到期时要走 scheduler 调度 + epoll_ctl。最终演化为"TcpServer 统一扫描"的设计——整个 server 只需要一个扫描协程,连接侧只写一个 atomic 值。 ...

May 12, 2026 · 2 min · 314 words

Hical v2.6.0 性能优化心得:从 27K 到 159K QPS 的完整旅程

Hical v2.6.0 性能优化心得:从 27K 到 159K QPS 的完整旅程 这篇文章记录了 Hical 从 v2.5.2 到 v2.6.0 的完整性能优化历程。不是罗列"我做了什么改动",而是分享怎么发现问题、怎么思考方案、怎么验证效果——以及那些"看起来应该有用但实际没用"的弯路。希望对做 C++ 高性能服务器开发的同学有参考价值。 目录 Hical v2.6.0 性能优化心得:从 27K 到 159K QPS 的完整旅程 目录 1. 起点:27K QPS,差距 6 倍 2. 第一个教训:不要猜,要量 3. 找对方向:火焰图告诉你真相 4. 三阶段优化路线 5. 阶段一:调度模型重构(27K → 132K) 5.1 SO_REUSEPORT:消除跨线程调度 5.2 连接级 Timer + atomic 时间戳 5.3 结果 6. 阶段二:去 Beast,自研 HTTP/WS 栈(132K → 140K) 6.1 四个 Phase 6.2 零拷贝请求解析 6.3 结果 7. 阶段三:热路径微优化(140K → 159K) 7.1 修复 readBuf 残留数据丢弃(功能 BUG + 性能) 7.2 scatter-gather I/O 替代单 buffer 合并 7.3 其他微优化(含后续延迟分配优化) 7.4 结果 8. 最终火焰图:确认优化到位 9. 走过的弯路 弯路 1:优化不是瓶颈的代码 弯路 2:FixedBuffer 栈缓冲区太大 弯路 3:过早放弃 10. 总结:性能优化的方法论 原则一:Profiling 驱动,不靠直觉 原则二:按占比排序,从大到小 原则三:每步验证,不要积累 原则四:知道何时停手 最终成绩单 1. 起点:27K QPS,差距 6 倍 v2.5.1 的 Hical 在 Docker 环境(Ubuntu 24.04, GCC 14, 4 线程)下跑 Hello World benchmark,wrk 报出 ~27K QPS。 ...

May 11, 2026 · 6 min · 1235 words

Hical v2.5.2 性能优化实战:SO_REUSEPORT + 连接级 Timer 实现 3 倍 QPS 提升

Hical v2.5.2 性能优化实战:SO_REUSEPORT + 连接级 Timer 实现 3 倍 QPS 提升 在 火焰图分析中,我们定位到 Hical 的 QPS 瓶颈在 Boost.Asio 的 epoll 交互模型——跨线程调度(14.5%)和 timer 相关 epoll_ctl(12.5%)合计吃掉了 27% 的 CPU。[P1 优化](Router 同步快速路径)无实质提升后,本文记录 P2/P3 两项优化的设计思路、实现细节和实测结果。 目录 1. 背景回顾 2. 优化方案 A:SO_REUSEPORT 多 Acceptor 3. 优化方案 B:连接级 Timer + Atomic 时间戳 4. 实测结果 5. 剩余差距与后续方向 6. 复现指南 1. 背景回顾 1.1 P1 优化无效的原因 v2.5.2 实现了 Router::dispatchSync() 同步快速路径,在无中间件场景下跳过协程帧分配。三轮 Docker 压测结果: 轮次 QPS 变化 v2.5.1(基线) 27,493 — v2.5.1(静态链接) 19,381 系统波动 v2.5.2(dispatchSync) 20,940 无实质提升 原因:Router::dispatch 在火焰图中仅占 0.24% CPU,同步快速路径省掉的协程帧(~40-130ns)被 Asio 调度层(27%)完全淹没。 ...

May 10, 2026 · 6 min · 1271 words

Hical 踩坑实录五部曲(四):PMR 三层内存池——从理论完美到实战翻车

Hical 踩坑实录五部曲(四):PMR 三层内存池——从理论完美到实战翻车 引言 Hical 的内存管理采用 C++20 PMR(Polymorphic Memory Resource)三层池架构:全局同步池 → 线程本地无锁池 → 请求级单调缓冲。理论上完美——每一层解决一个特定的性能瓶颈。 但理论和实战之间,隔着一堆坑。 这篇记录了三层 PMR 池在开发和压测过程中遇到的 7 个真实问题——从跨线程 UAF 到 GC 永远不触发、从 CAS 自旋到缓冲区膨胀,每个都是排查半天以上的教训。 目录 Hical 踩坑实录五部曲(四):PMR 三层内存池——从理论完美到实战翻车 引言 目录 坑 1:configure() 原地重建的 use-after-free 坑 2:generation 缓存失效的竞争窗口 坑 3:GC 标记了但永远不回收——死线程的内存泄漏 坑 4:CAS 峰值更新的缓存行风暴 坑 5:请求级单调池的 upstream 选错 坑 6:PmrBuffer 缩容不及时导致内存膨胀 坑 7:allocator 传播链断裂——PMR 白忙一场 总结:PMR 三层池的使用清单 坑 1:configure() 原地重建的 use-after-free 现象:在服务启动流程中调用 MemoryPool::configure() 后,偶发崩溃,堆栈指向 synchronized_pool_resource 的内部结构。 根因:configure() 使用 placement new 原地重建全局池——先析构旧池,再构造新池: ...

May 10, 2026 · 7 min · 1379 words