因为磁盘 i/o 是阻塞操作,std::ofstream 直接写日志会阻塞主线程,即使只是写入 log_file。

为什么 std::ofstream 直接写日志会卡主线程
因为磁盘 I/O 是阻塞操作,哪怕只是 log_file ,一旦磁盘忙、文件系统延迟高或日志量突增,<code>operator 就会停住当前线程。这不是你代码写得慢,是操作系统在等硬件响应。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 绝不让业务线程直接调用
std::ofstream::write()或流插入操作 - 用无锁队列(如
moodycamel::ConcurrentQueue)把日志消息暂存到内存,由单独的 writer 线程消费 - 避免在 logger 接口里做字符串拼接(比如
std::to_string(x) + " " + s),这会触发多次堆分配 —— 改用fmt::format_to写入预分配的std::string_view缓冲区
spdlog 默认异步模式为什么仍可能丢日志
它用的是线程安全的 async_logger,但默认丢弃策略是 overflow_policy::block 或 overflow_policy::discard,而很多人没改配置,又没监听 spdlog::drop_count(),结果高负载时日志静默消失。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 初始化时显式指定
spdlog::drop_policy::overrun_oldest,确保队列满时不丢新日志也不卡住 - 给 async logger 设置合理大小:太小(如 8192)容易溢出,太大(>1M)导致内存占用不可控;推荐 64K~256K 条消息缓冲
- 务必调用
spdlog::flush_on(spdlog::level::err),否则 ERROR 级别以下的日志可能滞留在缓冲区不落盘
格式化开销到底来自哪?fmt::v8 和 std::format 怎么选
不是模板展开慢,是每次格式化都涉及参数类型擦除、动态分发和临时字符串构造。实测显示,对相同日志模板,fmt::v8::format_to 比 std::format 快 2–3 倍,且支持编译期检查格式串。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 禁用
fmt的 locale 支持(定义FMT_LOCALE为 0),避免隐式std::locale构造开销 - 用
fmt::memory_buffer替代std::string作目标缓冲 —— 它内部预分配 256 字节,避免短日志反复 malloc - 不要在日志宏里写
fmt::format("{}", expensive_func());应先判断日志级别是否启用,再计算参数
如何让日志系统真正“可扩展”而不是只撑住单机
所谓可扩展,不是加个线程就完事,而是要能横向切分、按需路由、故障隔离。比如一个微服务集群里,不同模块日志应能独立滚动、限速、投递到不同后端(本地文件 / Kafka / Loki)。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 每个模块用独立
spdlog::logger实例,命名带前缀(如"net.http"、"db.query"),便于运行时开关和配置粒度控制 - 用
spdlog::sinks::dist_sink_mt组合多个 sink,比如同时写文件 + 推送 Kafka,但注意 Kafka sink 必须自己实现非阻塞发送(不能用 librdkafka 同步 API) - 滚动策略别只看时间,加 size-based 触发(
rotating_file_sink_mt的max_size),否则半夜流量低时日志文件空转一整晚不切
真正难的不是并发写,是当 writer 线程崩溃、磁盘满、网络断开时,日志要不要降级到 stderr、要不要触发告警、旧缓冲区怎么回收 —— 这些逻辑必须提前想清楚,不能靠“理论上不会出问题”。









