更多 C++ 文章见《修远之路(C++集萃)》专栏
spdlog 是一个基于 fmt 库的高性能、头文件优先的 C++ 日志框架,通过预分配环形队列与异步线程池实现零拷贝日志记录。
-
超高速:异步模式可达百万条 / 秒吞吐,比 glog/log4cpp 快数倍。
-
无锁设计:异步用无锁队列 + 线程池,主线程仅入队即返回,几乎不阻塞。
-
零拷贝 / 内存池:减少内存分配,高并发更稳。
-
同步 / 异步双模式:
- 同步:直接写 I/O,简单直接。
- 异步:后台线程处理 I/O,主线程无阻塞。
核心流程
核心模块
| 模块 | 核心职责/作用 | 输入/输出 |
|---|---|---|
| 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 通过预分配与零拷贝设计,在保证接口简洁的前提下实现极致性能:
- 环形队列:牺牲部分灵活性(固定大小),换取零分配与缓存友好
- 模板策略:编译期决定线程安全策略,零运行时开销
- 异步解耦:业务线程仅负责入队,I/O 延迟不影响核心路径
关键抽象与机制
环形队列(Circular Queue)
spdlog 的核心性能优化来自预分配的环形队列,其实现位于 circular_q.h:
ini
template <typename T>
class circular_q {
size_t max_items_ = 0;
typename std::vector<T>::size_type head_ = 0;
typename std::vector<T>::size_type tail_ = 0;
size_t overrun_counter_ = 0;
std::vector<T> 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_记录丢失消息数,用于监控
MPMC 阻塞队列
多生产者-多消费者队列位于 mpmc_blocking_q.h,封装了环形队列并提供线程安全:
arduino
template <typename T>
class mpmc_blocking_queue {
std::mutex queue_mutex_;
std::condition_variable push_cv_;
std::condition_variable pop_cv_;
spdlog::details::circular_q<T> q_;
std::atomic<size_t> discard_counter_{0};
void enqueue(T &&item) {
{
std::unique_lock<std::mutex> 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<std::mutex> 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_have_room失败次数
Sink 抽象与线程安全
Sink 接口定义于 sink.h:
arduino
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<spdlog::formatter> 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:
arduino
template <typename Mutex>
class base_sink : public sink {
public:
void log(const details::log_msg &msg) final override {
std::lock_guard<Mutex> lock(mutex_);
sink_it_(msg);
}
void flush() final override {
std::lock_guard<Mutex> lock(mutex_);
flush_();
}
protected:
std::unique_ptr<spdlog::formatter> 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 模式:基类控制流程,派生类实现具体逻辑
格式化器与时间戳缓存
Pattern Formatter 位于 pattern_formatter.h:
c
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<std::unique_ptr<details::flag_formatter>> 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,避免多次分配
核心设计取舍
性能 vs 易用性
| 设计决策 | 性能收益 | 易用性代价 | 适用场景 |
|---|---|---|---|
| Header-only 模式 | 无编译优化,编译时间长 | 无需构建,集成简单 | 快速原型、小型项目 |
| 编译模式 | 编译时间短,二进制体积小 | 需要 CMake 配置 | 生产环境、大型项目 |
| 异步模式 | 业务线程零阻塞 | 崩溃时可能丢失日志 | 高并发服务 |
| 同步模式 | 日志顺序严格保证 | I/O 阻塞业务线程 | 调试、审计场景 |
关键权衡:异步模式下,日志消息在入队时已格式化,但 log_msg_buffer 仍需拷贝 payload。这是为了确保业务线程释放原始字符串后,后台线程仍能安全访问日志内容。
一致性 vs 可用性
kotlin
enum class async_overflow_policy {
block, // 阻塞直到有空间(强一致性)
overrun_oldest, // 覆盖最旧消息(高可用性)
discard_new // 丢弃新消息(低延迟优先)
};
场景分析:
- block:适用于审计日志,不能容忍丢失,但可能阻塞业务线程
- overrun_oldest:适用于监控日志,容忍丢失旧数据,保证实时性
- discard_new:适用于调试日志,队列满时丢弃新消息,避免阻塞
抽象程度 vs 灵活性
Sink 抽象层级:
scss
sink (接口)
└─ base_sink<Mutex> (模板基类,提供线程安全)
├─ 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()注册回调
源码地图
csharp
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 库编译实现
核心文件解析:
- logger.h:定义
logger类,包含日志级别过滤、错误处理、sink 管理 - async_logger.h:重写
sink_it_()和flush_(),将消息推送到线程池 - mpmc_blocking_q.h:核心并发数据结构,决定异步性能上限
- pattern_formatter.h:格式化逻辑,时间戳缓存优化
- 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>(name, args...) |
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、异步模式和资源清理:
c
#include <spdlog/spdlog.h>
#include <spdlog/async.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/rotating_file_sink.h>
#include <iostream>
#include <exception>
class GameLogger {
public:
static bool Initialize() {
try {
// Initialize thread pool with 8192 queue size and 1 worker thread
spdlog::init_thread_pool(8192, 1);
// Create console sink (stdout with color)
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
console_sink->set_level(spdlog::level::debug);
console_sink->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] [%t] %v");
// Create rotating file sink (5MB per file, max 3 files)
auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
"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");
// Combine sinks
std::vector<spdlog::sink_ptr> sinks{console_sink, file_sink};
// Create async logger
auto logger = std::make_shared<spdlog::async_logger>(
"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);
// Set error handler
logger->set_error_handler([](const std::string &msg) {
std::cerr << "Logger error: " << msg << std::endl;
});
// Register as default logger
spdlog::register_logger(logger);
spdlog::set_default_logger(logger);
// Enable backtrace for debug (store last 32 messages)
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");
// Dump backtrace if there were errors
spdlog::dump_backtrace();
// Flush all loggers
spdlog::apply_all([](std::shared_ptr<spdlog::logger> l) {
l->flush();
});
// Release all loggers and stop threads
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) { // Below 60 FPS
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 {
// Basic logging
SPDLOG_INFO("Game started");
SPDLOG_DEBUG("Debug message (only visible in debug builds)");
// Formatted logging
GameLogger::LogGameEvent("PlayerJump", 12345, 98.5f);
GameLogger::LogPerformanceMetric("RenderFrame", 14.2);
GameLogger::LogPerformanceMetric("PhysicsUpdate", 18.5);
// Error handling
SPDLOG_ERROR("Simulated error with code {}", 404);
// Flush manually
spdlog::default_logger()->flush();
} catch (const std::exception &ex) {
SPDLOG_CRITICAL("Exception: {}", ex.what());
GameLogger::Shutdown();
return 1;
}
GameLogger::Shutdown();
return 0;
}
关键工程实践:
- 初始化顺序:先初始化线程池,再创建 logger,最后注册
- 错误处理:捕获
spdlog::spdlog_ex异常,设置错误处理器 - 资源清理:
shutdown()必须调用,否则异步线程不会停止 - Flush 策略:
flush_on(warn)确保 warning 及以上级别立即写入 - Backtrace:存储最近 N 条日志,崩溃时 dump 用于调试
场景建议
配置管理
ini
// 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);
};
日志追踪
c
// Use MDC (Mapped Diagnostic Context) for request tracing
#include <spdlog/mdc.h>
void HandleHttpRequest(const HttpRequest &req) {
// Set trace ID for all logs in this scope
spdlog::mdc::put("trace_id", req.trace_id());
spdlog::mdc::put("user_id", std::to_string(req.user_id()));
SPDLOG_INFO("Processing request");
// ... business logic ...
spdlog::mdc::remove("trace_id");
spdlog::mdc::remove("user_id");
}
// Custom formatter with MDC support
// Pattern: [%Y-%m-%d %H:%M:%S.%e] [%l] [trace:%X{trace_id}] %v
性能调优
scss
// Monitor async queue health
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);
}
// Reset counters after monitoring
tp->reset_overrun_counter();
tp->reset_discard_counter();
}
}
// Compile-time log level for release builds
#ifndef NDEBUG
#define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_DEBUG
#else
#define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_INFO
#endif
// Use SPDLOG_INFO instead of spdlog::info for compile-time filtering
SPDLOG_INFO("This call may be optimized out in release");
监控集成
arduino
// Custom sink for metrics collection
class MetricsSink : public spdlog::sinks::base_sink<std::mutex> {
protected:
void sink_it_(const spdlog::details::log_msg &msg) override {
// Increment metrics counter
metrics::LogCounter(msg.level).Increment();
// Track error rate
if (msg.level >= spdlog::level::err) {
metrics::ErrorRate.Mark();
}
}
void flush_() override {
// Flush metrics to backend
metrics::Flush();
}
};
// Register metrics sink
auto metrics_sink = std::make_shared<MetricsSink>();
spdlog::default_logger()->sinks().push_back(metrics_sink);
本文使用 markdown.com.cn 排版