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

C++ Web 服务日志最佳实践:Hical 日志系统完全指南

C++ Web 服务日志最佳实践:Hical 日志系统完全指南 引子:生产环境 printf 调试?该升级了 不少 C++ 服务器项目在早期会这样写日志: 1 2 printf("[INFO] user login: uid=%d\n", uid); fprintf(stderr, "[ERROR] db connect failed\n"); 这没什么问题——直到你的服务跑到生产环境,遇到以下场景: 日志文件膨胀:跑了三天,单个 app.log 已经 8GB,grep 一下需要等几分钟 性能抖动:每次写日志都 fwrite + fflush,高并发时 I/O 成为瓶颈 信息不够:出了问题只知道"某某接口报错",不知道是哪个请求、哪个用户 无法动态调级:想临时开 DEBUG 排查问题,必须重启服务 日志散落各处:访问日志、审计日志、业务日志混在同一个文件里,难以分析 Hical 的日志系统正是为了解决这五个问题而设计的。本文从最简用法出发,逐步覆盖文件轮转、异步写盘、结构化日志、通道分流、HTTP 集成到运行时调级,每个场景都给出可直接复制的代码。 1. 快速上手:三种 API 对比 Hical 日志提供三种书写风格,适用不同场景: std::format 风格(首选) 1 2 3 4 5 #include <hical/Log.h> HICAL_LOG_INFO("server started on port={}", 8080); HICAL_LOG_WARN("connection pool low, available={}", pool.available()); HICAL_LOG_ERROR("db query failed: sql={} err={}", sql, ec.message()); 格式字符串在编译期校验(借助 std::format_string<Args...>),参数类型不匹配直接报错,不会等到运行时才崩溃。这是最常见的用法。 ...

May 6, 2026 · 6 min · 1208 words

从零构建现代C++ Web服务器(七):生产级日志系统

从零构建现代C++ Web服务器(七):生产级日志系统 系列导航:第一篇:设计理念 | 第二篇:协程与内存池 | 第三篇:路由与中间件 | 第四篇:实战与调优 | 第五篇:Cookie 与 Session | 第六篇:数据库中间件 | 第七篇:日志系统(本篇) 前置知识 阅读过第三篇的中间件洋葱模型 了解 C++20 std::format、std::jthread 了解日志系统的基本概念(级别、格式化、输出目标) 目录 1. 为什么需要自研日志系统? 2. Phase 1:基础增强——从 fprintf 到 std::format 3. Phase 2:异步后端——从同步到生产级 4. Phase 3:结构化日志与可观测性 5. 性能深度分析 6. 线程安全设计 7. 实战:5 分钟搭建完整日志体系 8. 总结与设计决策表 9. 核心要点 10. 知识图谱 1. 为什么需要自研日志系统? 前六篇把 hical 的核心骨架搭完了:协程 I/O、内存池、路由、中间件、SSL、会话、数据库。唯一的短板是日志——每次排查问题只能翻 stderr,没有文件、没有结构、没有追踪 ID。真正的生产环境里,日志比功能代码重要得多:功能代码决定程序的行为,日志决定你能不能在凌晨三点用最短时间还原那行让服务崩掉的数据路径。 1.1 现有轮子的取舍 先看市面上的主流方案: 库 优点 对 hical 的问题 spdlog 成熟、快(每秒数百万条)、格式丰富 外部依赖;fmt 与 std::format 语义略有差异 glog Google 出品,稳定 C++03 风格 API;宏冗余;不支持 std::format Trantor drogon 自带,协程时代设计 与 drogon 强耦合,无法独立引入 log4cxx 功能完备、配置文件驱动 重量级;Java 移植风格与现代 C++ 格格不入 自研 零依赖、API 与框架深度融合 需要投入设计成本 hical 的核心约束是零外部依赖——整个框架只依赖 Boost 和 OpenSSL,任何新模块都不能打破这条线。spdlog 引入 fmt 库就已经违规,glog 的 API 风格与现代 C++ 割裂,Trantor 无法解耦。 ...

May 1, 2026 · 25 min · 5197 words

trantor 日志系统

第 1 课:trantor 日志系统 对应源文件: trantor/utils/LogStream.h — 底层缓冲区 + 流写入 trantor/utils/Logger.h — 核心日志类 + 所有宏定义 trantor/utils/AsyncFileLogger.h — 异步文件写入 一、整体架构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 用户代码: LOG_INFO << "玩家登录" << playerId; │ ▼ [Logger 对象 (临时变量)] 构造时:记录时间/文件名/行号/级别 .stream() 返回 LogStream& │ ▼ [LogStream 对象] operator<< 链式写入 数据落入 FixedBuffer (4000字节栈内存) │ ▼ [Logger 析构时] 调用 outputFunc_(index)(buf, len) 默认 → fwrite(stdout) 生产环境 → AsyncFileLogger::output() 核心思想:Logger 是一个临时对象,构造写头,析构输出,<< 运算符把数据攒进缓冲区。整个过程利用 C++ 的 RAII 机制自动完成,用户只需一行代码。 ...

March 4, 2025 · 7 min · 1444 words