C++之高性能跨平台日志库spdlog
作者:水悠悠予安
时间:2026-07-01
来源:互联网
浏览:0
spdlog是C++高性能日志库,异步模式可达每秒百万条消息。采用无锁队列、环形队列与零拷贝设计,支持同步/异步双模式。核心模块包括Logger、Sink、Formatter、Registry、线程池及MPMC队列,通过预分配与模板策略实现极致性能。
spdlog 是目前 C++ 社区里相当热门的一个日志框架,核心卖点就是快——快到什么程度?异步模式下每秒百万条消息的吞吐量,比 glog、log4cpp 快了不止一个量级。
它的几个关键特性值得一说:异步模式采用无锁队列配合线程池,主线程把日志消息丢进队列就返回,几乎不产生阻塞;预分配的环形队列和零拷贝设计,在高并发场景下内存分配压力极低;支持同步/异步双模式切换,同步模式直接写 I/O,简单直接,异步模式后台线程统一处理,业务逻辑完全不卡。
从应用角度来看,不同场景下各模块的适用性也有所不同:
class circular_q {
size_t max_items_ = 0;
typename std::vector::size_type head_ = 0;
typename std::vector::size_type tail_ = 0;
size_t overrun_counter_ = 0;
std::vector v_;
void push_back(T &&item) {
if (max_items_ > 0) {
v_[tail_] = std::move(item);
tail_ = (tail_ + 1) % max_items_;
if (tail_ == head_) { // 队列满,覆盖最旧消息
head_ = (head_ + 1) % max_items_;
++overrun_counter_;
}
}
}
};
```
几个关键设计点值得注意:
- 预分配:构造时一次性分配 `max_items + 1` 个元素,运行时不做任何内存分配
- 覆盖策略:队列满了怎么办?直接覆盖最旧的消息,保证写入操作永不阻塞
- 零拷贝:`std::move` 转移所有权,避免深拷贝带来的性能损耗
- 计数器:`overrun_counter_` 记录被覆盖的消息数量,方便监控
class mpmc_blocking_queue {
std::mutex queue_mutex_;
std::condition_variable push_cv_;
std::condition_variable pop_cv_;
spdlog::details::circular_q q_;
std::atomic discard_counter_{0};
void enqueue(T &&item) {
{
std::unique_lock lock(queue_mutex_);
pop_cv_.wait(lock, [this] { return !this->q_.full(); });
q_.push_back(std::move(item));
}
push_cv_.notify_one();
}
bool dequeue_for(T &popped_item, std::chrono::milliseconds wait_duration) {
{
std::unique_lock lock(queue_mutex_);
if (!push_cv_.wait_for(lock, wait_duration,
[this] { return !this->q_.empty(); })) {
return false;
}
popped_item = std::move(q_.front());
q_.pop_front();
}
pop_cv_.notify_one();
return true;
}
};
```
并发控制方面有几个亮点:
- 双条件变量:`push_cv_` 负责唤醒消费者线程,`pop_cv_` 唤醒生产者线程,避免盲目轮询
- 细粒度锁:锁的持有时间只覆盖队列操作,不包含 I/O 等耗时步骤
- 超时机制:`dequeue_for` 支持超时返回,防止死锁
- 丢弃计数:`discard_counter_` 记录 `enqueue_if_ha ve_room` 失败的次数
sink_formatter) = 0;
void set_level(level::level_enum log_level);
level::level_enum level() const;
bool should_log(level::level_enum msg_level) const;
protected:
level_t level_{level::trace};
};
```
线程安全是通过模板策略实现的,位于 `base_sink.h`:
```cpp
template
class base_sink : public sink {
public:
void log(const details::log_msg &msg) final override {
std::lock_guard lock(mutex_);
sink_it_(msg);
}
void flush() final override {
std::lock_guard lock(mutex_);
flush_();
}
protected:
std::unique_ptr formatter_;
Mutex mutex_;
virtual void sink_it_(const details::log_msg &msg) = 0;
virtual void flush_() = 0;
};
```
这个设计有几个明显优势:
- 编译期多态:通过模板参数选择 `std::mutex` 或者 `details::null_mutex`
- 零运行时开销:单线程场景下使用 `null_mutex`,锁操作被编译器完全优化掉
- CRTP 模式:基类控制整体流程,派生类只需要实现具体的 I/O 逻辑
> formatters_;
std::tm get_time_(const details::log_msg &msg) {
if (need_localtime_) {
auto time_now = log_clock::to_time_t(msg.time);
if (last_log_secs_ != time_now) {
cached_tm_ = localtime(time_now);
last_log_secs_ = time_now;
}
return cached_tm_;
}
return gmtime(log_clock::to_time_t(msg.time));
}
};
```
性能优化的几个关键点:
- 时间戳缓存:同一秒内的日志共享同一个 `std::tm` 结构,避免重复转换
- 编译期解析:模式字符串在构造时就解析为 `flag_formatter` 链表,运行时无需解析
- 内存复用:`memory_buf_t` 使用 fmt 的 `memory_buffer`,避免反复分配内存
一个值得注意的权衡是:异步模式下,日志消息在入队时已经完成了格式化,但 `log_msg_buffer` 仍然需要拷贝 payload。这么做的目的是确保业务线程释放原始字符串后,后台线程仍然能安全访问日志内容。
(模板基类,提供线程安全)
├─ basic_file_sink (单文件)
├─ rotating_file_sink (滚动文件)
├─ daily_file_sink (按日期分割)
└─ stdout_sink (控制台)
```
扩展机制也很灵活:
- 自定义 Sink:继承 `base_sink`,实现 `sink_it_()` 和 `flush_()` 即可
- 自定义 Formatter:继承 `custom_flag_formatter` 并注册到 `pattern_formatter`
- 自定义错误处理:通过 `set_error_handler()` 注册回调函数
#include
#include
#include
#include
#include
#include
class GameLogger {
public:
static bool Initialize() {
try {
// 初始化线程池,队列大小8192,工作线程数1
spdlog::init_thread_pool(8192, 1);
// 创建控制台 sink(彩色输出)
auto console_sink = std::make_shared();
console_sink->set_level(spdlog::level::debug);
console_sink->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] [%t] %v");
// 创建滚动文件 sink(单个文件5MB,保留3个备份)
auto file_sink = std::make_shared(
"logs/game.log", 1024 * 1024 * 5, 3);
file_sink->set_level(spdlog::level::info);
file_sink->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] [%t] [%s:%#] %v");
// 组合多个 sink
std::vector sinks{console_sink, file_sink};
// 创建异步 logger
auto logger = std::make_shared(
"game_logger",
sinks.begin(),
sinks.end(),
spdlog::thread_pool(),
spdlog::async_overflow_policy::block
);
logger->set_level(spdlog::level::debug);
logger->flush_on(spdlog::level::warn);
// 设置错误处理器
logger->set_error_handler([](const std::string &msg) {
std::cerr << "Logger error: " << msg << std::endl;
});
// 注册为默认 logger
spdlog::register_logger(logger);
spdlog::set_default_logger(logger);
// 启用回溯,存储最近32条日志
spdlog::enable_backtrace(32);
SPDLOG_INFO("GameLogger initialized successfully");
return true;
} catch (const spdlog::spdlog_ex &ex) {
std::cerr << "Logger initialization failed: " << ex.what() << std::endl;
return false;
}
}
static void Shutdown() {
SPDLOG_INFO("Shutting down GameLogger");
// 如果有错误,dumo 回溯信息
spdlog::dump_backtrace();
// flush 所有 logger
spdlog::apply_all([](std::shared_ptr l) {
l->flush();
});
// 释放所有 logger 并停止线程
spdlog::shutdown();
}
static void LogGameEvent(const std::string &event_name, int player_id, float value) {
SPDLOG_INFO("GameEvent: {} [player={}, value={:.2f}]",
event_name, player_id, value);
}
static void LogPerformanceMetric(const std::string &metric, double ms) {
if (ms > 16.67) { // 超过60FPS的帧时间阈值
SPDLOG_WARN("Performance warning: {} took {:.2f}ms", metric, ms);
} else {
SPDLOG_DEBUG("Performance: {} = {:.2f}ms", metric, ms);
}
}
};
int main() {
if (!GameLogger::Initialize()) {
return 1;
}
try {
// 基础日志
SPDLOG_INFO("Game started");
SPDLOG_DEBUG("Debug message (only visible in debug builds)");
// 格式化日志
GameLogger::LogGameEvent("PlayerJump", 12345, 98.5f);
GameLogger::LogPerformanceMetric("RenderFrame", 14.2);
GameLogger::LogPerformanceMetric("PhysicsUpdate", 18.5);
// 错误处理
SPDLOG_ERROR("Simulated error with code {}", 404);
// 手动 flush
spdlog::default_logger()->flush();
} catch (const std::exception &ex) {
SPDLOG_CRITICAL("Exception: {}", ex.what());
GameLogger::Shutdown();
return 1;
}
GameLogger::Shutdown();
return 0;
}
```
这个示例体现了几条工程实践:
1. 初始化顺序:先创建线程池,再创建 logger,最后注册
2. 错误处理:捕获 `spdlog::spdlog_ex` 异常,设置错误处理器
3. 资源清理:`shutdown()` 必须调用,否则异步线程不会主动停止
4. Flush 策略:`flush_on(warn)` 确保 warning 及以上级别立即写入
5. Backtrace:存储最近 N 条日志,崩溃时 dump 出来用于调试
void HandleHttpRequest(const HttpRequest &req) {
// 为当前作用域内的所有日志设置 trace ID
spdlog::mdc::put("trace_id", req.trace_id());
spdlog::mdc::put("user_id", std::to_string(req.user_id()));
SPDLOG_INFO("Processing request");
// ... 业务逻辑 ...
spdlog::mdc::remove("trace_id");
spdlog::mdc::remove("user_id");
}
// 配合 MDC 的格式化模式
// Pattern: [%Y-%m-%d %H:%M:%S.%e] [%l] [trace:%X{trace_id}] %v
```
{
protected:
void sink_it_(const spdlog::details::log_msg &msg) override {
// 增加指标计数器
metrics::LogCounter(msg.level).Increment();
// 追踪错误率
if (msg.level >= spdlog::level::err) {
metrics::ErrorRate.Mark();
}
}
void flush_() override {
// flush 指标到后端
metrics::Flush();
}
};
// 注册指标 sink
auto metrics_sink = std::make_shared();
spdlog::default_logger()->sinks().push_back(metrics_sink);
本文内容来源于互联网,如有侵权请联系删除。
核心流程
核心模块
要理解 spdlog 的设计,先看它的几个核心模块各自扮演什么角色:| 模块 | 核心职责/作用 | 输入/输出 |
|---|---|---|
| Logger | 日志级别过滤、消息分发、错误处理,统一入口,支持多 sink 组合 | 格式化字符串 + 参数,输出 log_msg 对象 |
| Sink | 实际 I/O 操作、格式化输出,可扩展输出目标,单一职责 | 输入 log_msg 对象,输出字节流到目标 |
| Formatter | 消息格式化、时间戳处理、颜色标记,支持自定义格式,缓存优化 | 输入 log_msg 对象,输出格式化字符串 |
| Registry | 全局 logger 管理、配置分发、生命周期,集中管理,避免全局变量污染 | 输入 logger 注册请求,输出 logger 引用 |
| Thread Pool | 异步消息处理、后台线程调度,解耦业务线程与 I/O 线程 | 输入 async_msg 对象,调用 logger sink_it_ |
| MPMC Queue | 线程安全消息队列、阻塞/非阻塞策略,生产者-消费者解耦,零分配 | 输入 log_msg 对象,输出出队消息 |
| 能力 | 适用场景 | 不适用场景 |
|---|---|---|
| 同步日志 | 单线程应用、调试阶段、低频日志 | 高并发生产环境、性能敏感路径 |
| 异步日志 | 高并发服务、游戏引擎、实时系统 | 需要严格顺序保证、崩溃时日志不能丢失 |
| 环形缓冲 | 固定内存预算、可容忍消息丢失 | 需要持久化所有日志、审计场景 |
| 多 Sink 组合 | 同时输出到文件/控制台/网络 | 单一输出目标、极简场景 |
| 自定义格式 | 结构化日志、日志分析系统 | 标准格式即可满足需求 |
核心执行时序
同步日志执行流程

异步日志执行流程

原理与设计
spdlog 能在保持接口简洁的同时实现极致性能,靠的是预分配与零拷贝设计这套组合拳: 1. 环形队列:牺牲部分灵活性(固定大小),换来零分配与缓存友好 2. 模板策略:在编译期决定线程安全策略,运行时零开销 3. 异步解耦:业务线程只负责入队,I/O 延迟被隔离在后台,不干扰核心路径关键抽象与机制
环形队列(Circular Queue)
环形队列是 spdlog 性能优化的基石,实现位于 `circular_q.h`: ```cpp templateMPMC 阻塞队列
多生产者-多消费者队列位于 `mpmc_blocking_q.h`,在环形队列基础上封装了线程安全机制: ```cpp templateSink 抽象与线程安全
Sink 接口定义在 `sink.h`: ```cpp class sink { public: virtual ~sink() = default; virtual void log(const details::log_msg &msg) = 0; virtual void flush() = 0; virtual void set_pattern(const std::string &pattern) = 0; virtual void set_formatter(std::unique_ptr格式化器与时间戳缓存
Pattern Formatter 位于 `pattern_formatter.h`: ```cpp class pattern_formatter final : public formatter { private: std::string pattern_; std::string eol_; pattern_time_type pattern_time_type_; bool need_localtime_; std::tm cached_tm_; std::chrono::seconds last_log_secs_; std::vector核心设计取舍
性能 vs 易用性
| 设计决策 | 性能收益 | 易用性代价 | 适用场景 |
|---|---|---|---|
| Header-only 模式 | 无编译优化,编译时间长 | 无需构建,集成简单 | 快速原型、小型项目 |
| 编译模式 | 编译时间短,二进制体积小 | 需要 CMake 配置 | 生产环境、大型项目 |
| 异步模式 | 业务线程零阻塞 | 崩溃时可能丢失日志 | 高并发服务 |
| 同步模式 | 日志顺序严格保证 | I/O 阻塞业务线程 | 调试、审计场景 |
一致性 vs 可用性
```cpp enum class async_overflow_policy { block, // 阻塞直到有空间(强一致性) overrun_oldest, // 覆盖最旧消息(高可用性) discard_new // 丢弃新消息(低延迟优先) }; ``` 不同场景下该选哪种策略? - block:审计日志的首选,不能容忍任何丢失,代价是可能阻塞业务线程 - overrun_oldest:监控日志的好选择,能容忍旧数据丢失,保证实时性 - discard_new:调试日志的理想方案,队列满了直接丢弃新消息,避免阻塞抽象程度 vs 灵活性
Sink 的继承层级相当清晰: ``` sink (接口) └─ base_sink源码地图
``` spdlog-1.15.2/ ├── include/spdlog/ │ ├── spdlog.h # 全局 API 入口,registry 访问 │ ├── logger.h # 核心 logger 类,同步日志实现 │ ├── async_logger.h # 异步 logger,继承 logger │ ├── formatter.h # 格式化器接口 │ ├── pattern_formatter.h # 默认格式化器实现 │ ├── async.h # 异步工厂函数 │ ├── common.h # 公共类型定义,编译配置 │ │ │ ├── details/ │ │ ├── registry.h # 全局 logger 注册表 │ │ ├── thread_pool.h # 异步线程池 │ │ ├── mpmc_blocking_q.h # 多生产者多消费者队列 │ │ ├── circular_q.h # 环形队列底层实现 │ │ ├── log_msg.h # 日志消息结构 │ │ ├── log_msg_buffer.h # 带缓冲的消息,用于异步 │ │ └── file_helper.h # 文件操作辅助类 │ │ │ └── sinks/ │ ├── sink.h # Sink 接口定义 │ ├── base_sink.h # 线程安全模板基类 │ ├── basic_file_sink.h # 基础文件 Sink │ ├── rotating_file_sink.h # 滚动文件 Sink │ ├── daily_file_sink.h # 日期分割 Sink │ ├── stdout_sinks.h # 控制台 Sink │ └── dist_sink.h # 分发 Sink(多目标) │ └── src/ ├── spdlog.cpp # 编译模式实现 ├── async.cpp # 异步相关实现 └── bundled_fmtlib_format.cpp # fmt 库编译实现 ``` 几个核心文件需要重点关注: 1. logger.h:定义了 `logger` 类,涵盖日志级别过滤、错误处理、sink 管理 2. async_logger.h:重写了 `sink_it_()` 和 `flush_()`,负责把消息推送到线程池 3. mpmc_blocking_q.h:核心并发数据结构,直接决定异步性能的上限 4. pattern_formatter.h:格式化逻辑,时间戳缓存优化是性能关键 5. registry.h:全局状态管理,logger 的整个生命周期都靠它掌控API 使用
常用 API
全局 API
通过 `spdlog::` 命名空间可以直接访问:| API | 参数说明 | 功能说明 |
|---|---|---|
spdlog::info(fmt, args...) | fmt: 格式化字符串,args: 参数 | 使用默认 logger 输出 INFO 级别日志 |
spdlog::set_level(level) | level: level::trace 到 level::off | 设置全局日志级别 |
spdlog::set_pattern(pattern) | pattern: 格式模式字符串 | 设置全局格式模式 |
spdlog::get(name) | name: logger 名称 | 获取已注册的 logger,不存在返回 nullptr |
spdlog::drop(name) | name: logger 名称 | 从 registry 移除 logger |
spdlog::shutdown() | 无 | 停止所有线程,清理资源 |
spdlog::flush_on(level) | level: 触发 flush 的级别 | 设置自动 flush 级别 |
spdlog::flush_every(interval) | interval: 时间间隔 | 启动周期性 flush 线程 |
Logger API
| API | 参数说明 | 功能说明 |
|---|---|---|
logger->log(level, fmt, args...) | level: 日志级别 | 输出指定级别日志 |
logger->set_level(level) | level: 日志级别 | 设置该 logger 的日志级别 |
logger->flush() | 无 | 手动 flush 所有 sink |
logger->sinks() | 无 | 返回 sink 列表引用,可动态添加 sink |
logger->set_formatter(formatter) | formatter: 格式化器指针 | 设置自定义格式化器 |
logger->error_handler() | 无 | 获取当前错误处理器 |
logger->set_error_handler(handler) | handler: 错误处理函数 | 设置错误处理回调 |
Sink API
| API | 参数说明 | 功能说明 |
|---|---|---|
sink->set_level(level) | level: 日志级别 | 设置 sink 级别过滤 |
sink->set_pattern(pattern) | pattern: 格式模式 | 设置 sink 格式 |
sink->flush() | 无 | flush 该 sink |
异步 API
| API | 参数说明 | 功能说明 |
|---|---|---|
spdlog::create_async | Sink: sink 类型,name: logger 名称 | 创建异步 logger |
spdlog::init_thread_pool(q_size, n_threads) | q_size: 队列大小,n_threads: 线程数 | 初始化全局线程池 |
async_logger(logger_name, sink, tp, policy) | tp: 线程池,policy: 溢出策略 | 构造异步 logger |
样例 Demo
下面是一个完整的示例,涵盖了错误处理、多 sink、异步模式和资源清理: ```cpp #include场景建议
配置管理
```cpp // config/logger_config.h struct LoggerConfig { std::string log_file_path = "logs/app.log"; size_t max_file_size = 5 * 1024 * 1024; // 5MB int max_files = 3; spdlog::level::level_enum level = spdlog::level::info; bool async_mode = true; size_t queue_size = 8192; std::string pattern = "[%Y-%m-%d %H:%M:%S.%e] [%l] [%t] %v"; static LoggerConfig LoadFromFile(const std::string &config_path); }; ```日志追踪
```cpp // 使用 MDC(Mapped Diagnostic Context)进行请求追踪 #include性能调优
```cpp // 监控异步队列健康状态 void MonitorLoggerQueue() { auto tp = spdlog::thread_pool(); if (tp) { size_t overrun = tp->overrun_counter(); size_t discard = tp->discard_counter(); size_t queue_size = tp->queue_size(); if (overrun > 0 || discard > 0) { SPDLOG_WARN("Logger queue issues: overrun={}, discard={}, size={}", overrun, discard, queue_size); } // 监控完后重置计数器 tp->reset_overrun_counter(); tp->reset_discard_counter(); } } // 编译期日志级别控制,release 版本屏蔽 debug 日志 #ifndef NDEBUG #define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_DEBUG #else #define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_INFO #endif // 使用宏代替函数调用,编译期自动过滤 SPDLOG_INFO("This call may be optimized out in release"); ```监控集成
```cpp // 自定义 sink 收集指标 class MetricsSink : public spdlog::sinks::base_sink
作者最新文章
灵活计算器
2026-09-16 17:45
苹果折叠屏iPhone预计售价是多少
2026-09-14 13:44
OpenAI GPT-6 Astra 自主通关《传送门》:技术原理与实验成本解析
2026-09-08 19:08
苹果与铠侠签署NAND长期供应协议:3-5年长约与不设价格上限背后的供应链战略
2026-09-08 16:58
PDF转PPT操作指南:在线、本地与批量转换及结果核对
2026-09-04 15:04
上一篇:
Apache配置时如何实现防盗爬虫
热门文章
更多
精品专题
更多
Mac软件
更多
WINDOWS
更多
Windows 10
Windows
Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。
极度公式
Windows/macOS/Linux
极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。
















