C++之高性能跨平台日志库spdlog

更多 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 通过预分配与零拷贝设计,在保证接口简洁的前提下实现极致性能:

  1. 环形队列:牺牲部分灵活性(固定大小),换取零分配与缓存友好
  2. 模板策略:编译期决定线程安全策略,零运行时开销
  3. 异步解耦:业务线程仅负责入队,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::mutexdetails::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 库编译实现

核心文件解析:

  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::tracelevel::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;
}

关键工程实践:

  1. 初始化顺序:先初始化线程池,再创建 logger,最后注册
  2. 错误处理:捕获 spdlog::spdlog_ex 异常,设置错误处理器
  3. 资源清理:shutdown() 必须调用,否则异步线程不会停止
  4. Flush 策略:flush_on(warn) 确保 warning 及以上级别立即写入
  5. 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 排版

相关推荐
我不是懒洋洋2 小时前
手写数字识别:从零实现一个卷积神经网络(CNN)
c++
Gopher_HBo2 小时前
Go语言加密算法
后端
青云计划2 小时前
数据库的守护者-单飞锁
后端
BestOrNothing_20152 小时前
C++零基础到工程实战(5.1):初识函数—定义调用、参数返回值、栈区内存与变量作用域分析
c++·生命周期·作用域·变量·函数·栈内存
神奇小汤圆2 小时前
每次重启能救下几十万个请求:Cloudflare 如何用 Rust 实现零停机升级
后端
阿文的代码库2 小时前
如何在C++中使用标准库的智能指针
开发语言·c++·算法
用户298698530142 小时前
Java 统计 Word 文档中的单词数量
java·后端
郝学胜-神的一滴2 小时前
Qt 高级开发 008: 使用QSetting记住上次打开路径
开发语言·c++·qt·开源软件
kyle~3 小时前
机器人感知 --- 多相机传感时间误差分析
linux·c++·数码相机·机器人·ros2·传感器