Hical 踩坑实录五部曲(五):Boost.MySQL 协程集成的 5 个坑

Hical 踩坑实录五部曲(五):Boost.MySQL 协程集成的 5 个坑 引言 Hical 的数据库模块(src/db/)是一个基于协程的连接池 + 中间件系统,后端使用 Boost.MySQL 的 any_connection。从 “能跑” 到 “能在生产环境跑”,中间踩了不少坑。 这篇文章记录了 Boost.MySQL 协程集成过程中遇到的 5 个真实问题,每个都附带完整的解决方案代码。 目录 Hical 踩坑实录五部曲(五):Boost.MySQL 协程集成的 5 个坑 引言 目录 坑 1:any_connection vs 强类型连接的取舍 坑 2:PreparedStatement 失效与自动重试 坑 3:SET NAMES 注入风险——validateCharset 白名单 坑 4:连接池 acquire 超时的竞争窗口 坑 5:事务忘记 commit/rollback 的自动回滚设计 第一道防线:DbMiddleware 洋葱模型 第二道防线:连接池 release 兜底回滚 附:StmtCache LRU 缓存设计 总结:Boost.MySQL 集成检查清单 坑 1:any_connection vs 强类型连接的取舍 现象:第一版连接池用 Boost.MySQL 的强类型连接(tcp_ssl_connection),结果泛型代码全部被迫模板化——编译时间爆炸,且无法在运行时根据配置切换 TCP/SSL。 强类型方式的问题: 1 2 3 4 5 6 7 8 9 10 11 // ❌ 强类型——泛型代码必须模板化 template <typename Connection> class DbPool { std::vector<std::unique_ptr<Connection>> idle_; // Connection 是 tcp_connection 还是 tcp_ssl_connection? // 中间件也要模板化、查询日志也要模板化... }; // 编译时决定,运行时无法切换 using Pool = DbPool<boost::mysql::tcp_ssl_connection>; 解决方案:any_connection——类型擦除,运行时决定传输层: ...

May 11, 2026 · 8 min · 1605 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

Hical 踩坑实录五部曲(三):自研日志系统的 8 个血泪教训

Hical 踩坑实录五部曲(三):自研日志系统的 8 个血泪教训 引言 Hical 没有用 spdlog、glog 或任何第三方日志库——整套日志系统完全自研,覆盖 20+ 个源文件:格式化、Sink 后端、文件轮转、异步双缓冲、通道分流、HTTP 集成、运行时调级。 自研日志的好处在开发心得里聊过了。这篇只聊坑——从"能跑"到"能在生产环境跑"的过程中,踩过的 8 个真实问题。 目录 Hical 踩坑实录五部曲(三):自研日志系统的 8 个血泪教训 引言 目录 坑 1:异步写盘的背压——日志不应该成为延迟来源 坑 2:双缓冲析构时丢日志 坑 3:多线程 Sink 分发的锁竞争——COW 模式的引入 坑 4:日志注入——一条恶意日志伪造十行告警 坑 5:LogAdmin 的审计致盲攻击 坑 6:TRACE 日志在 Release 构建中的隐性开销 坑 7:trace-id 生成的性能瓶颈——OpenSSL 全局锁 坑 8:文件轮转的误删风险 总结:自研日志系统的检查清单 坑 1:异步写盘的背压——日志不应该成为延迟来源 现象:压测时发现 P99 延迟间歇性飙高。排查发现不是业务逻辑慢,而是日志写入阻塞了请求线程。 根因:早期版本的日志直接在调用线程同步 fwrite。高并发下磁盘 I/O 成为瓶颈,日志调用从微秒级退化为毫秒级。 引入异步 AsyncFileSink 后,新的问题出现——如果日志产生速度远超写盘速度(比如某个 bug 触发了大量 ERROR 日志),前端缓冲区会无限增长直到 OOM。 解决方案:背压保护——缓冲区超限时主动丢弃,而非阻塞或 OOM: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // AsyncFileSink.cpp — write() 中的背压丢弃 void AsyncFileSink::write(std::string_view formattedLine) { std::lock_guard<std::mutex> lock(m_bufMutex); // 背压保护:缓冲区过大时丢弃(防止内存爆炸) if (m_curBuf.size() > m_opts.backpressureLimit) { m_dropped.fetch_add(1, std::memory_order_relaxed); return; // 丢弃这条日志,但不阻塞调用线程 } m_curBuf.append(formattedLine.data(), formattedLine.size()); // 缓冲区接近满时通知后台线程 if (m_curBuf.size() >= m_opts.bufferSize) { m_cond.notify_one(); } } 关键细节:丢弃计数不是默默吞掉的——后台线程在每次刷盘时检查丢弃计数,并将统计写入日志文件: ...

May 9, 2026 · 8 min · 1702 words

Hical 踩坑实录五部曲(二):MSVC / GCC / Clang 三平台 C++20 编译差异

Hical 踩坑实录五部曲(二):MSVC / GCC / Clang 三平台 C++20 编译差异 引言 Hical 从第一天起就要求在 GCC 14+、Clang 20+、MSVC 2022+ 三个编译器上通过 CI。框架大量使用了 C++20 新特性:Concepts、co_await 协程、PMR 内存池、std::format、__VA_OPT__ 递归宏。 三平台兼容的代价就是踩三倍的坑。 这篇文章记录了开发 Hical 过程中遇到的编译器差异踩坑——每个坑按统一结构展开:现象 → 最小复现 → 根因 → 解决方案。 目录 Hical 踩坑实录五部曲(二):MSVC / GCC / Clang 三平台 C++20 编译差异 引言 目录 坑 1:模板参数推导差异——GCC 过、MSVC 报错 坑 2:Concepts 约束检查时机差异 坑 3:__VA_OPT__ 宏展开行为差异 坑 4:PMR allocator 传播行为差异 坑 5:std::format 可用性与行为差异 坑 6:协程 promise_type 与异常处理差异 坑 7:链接顺序敏感——Windows 特有的 ws2_32 问题 经验总结:三平台兼容清单 编译器设置 C++20 特性 协程 一般性 坑 1:模板参数推导差异——GCC 过、MSVC 报错 现象:一段在 GCC 14 上完美编译的透明哈希代码,在 MSVC 上报 C2672: no matching overloaded function found。 ...

May 8, 2026 · 7 min · 1357 words

Hical 踩坑实录五部曲(一):Boost.Asio 协程开发的 N 个坑

Hical 踩坑实录五部曲(一):Boost.Asio 协程开发的 N 个坑 引言 Hical 的所有异步 I/O 都基于 Boost.Asio 协程(co_await + boost::asio::use_awaitable)。路由处理器返回 Awaitable<HttpResponse>,中间件用洋葱模型 co_await next(req),连接池用 co_await timer.async_wait() 做非阻塞等待。 协程消除了回调地狱,但引入了一套全新的陷阱。这篇记录的每一个坑,都是在压测或线上环境中真实触发过的。 目录 Hical 踩坑实录五部曲(一):Boost.Asio 协程开发的 N 个坑 引言 目录 坑 1:co_await 后 this 悬挂——对象已析构 坑 2:协程异常传播——catch 里不能 co_await 坑 3:steady_timer 当协程信号量的技巧 坑 4:jthread vs thread——精准匹配停止信号 坑 5:多线程 io_context + 协程的线程安全陷阱 坑 6:detached 协程的异常黑洞 坑 7:io_context::stop() 不等于安全退出 总结:协程安全编程检查清单 坑 1:co_await 后 this 悬挂——对象已析构 现象:压测时低概率崩溃,堆栈指向 TcpServer 的 accept 循环,访问了已释放的内存。 最小复现: 1 2 3 4 5 6 7 8 9 10 11 // ❌ 危险的写法 Awaitable<void> TcpServer::acceptLoop() { while (running_) { auto socket = co_await acceptor_.async_accept(use_awaitable); // ⚠️ 如果在 co_await 期间 TcpServer 被析构, // this 已经是悬空指针! this->createConnection(std::move(socket)); // 💥 use-after-free } } 根因:协程帧通过 co_spawn(io_context, coroutine, detached) 提交到 io_context。协程帧的生命周期由 io_context 管理,与创建协程的对象完全分离。 ...

May 7, 2026 · 8 min · 1586 words