C++高性能日志库spdlog实战:从原理到生产环境部署

C++高性能日志库spdlog实战:从原理到生产环境部署

1. 项目概述:为什么我们需要一个高性能的日志库?

在C++项目里,尤其是服务器后端、游戏引擎、高频交易系统这些对性能有极致要求的领域,日志模块常常是那个“沉默的杀手”。你可能觉得,不就是往文件里写几行字吗?能有多大开销?但现实是,一个设计粗糙的日志系统,在高并发、高频次调用的场景下,其I/O阻塞、内存分配、锁竞争带来的性能损耗,足以拖垮整个应用的吞吐量。我见过太多项目,前期为了图省事,直接用std::cout或者fprintf写日志,到了性能压测阶段,发现CPU大量时间花在了日志输出上,甚至成为瓶颈,这时候再想重构,成本就非常高了。

这就是spdlog出现的意义。它不是一个简单的日志工具,而是一个为C++11及更高版本量身打造的高性能日志库。它的设计哲学非常明确:快,而且用起来要爽。“快”体现在它大量使用了现代C++的特性,如模板、移动语义、内联,并提供了异步日志这种“核武器”级别的优化模式。“爽”则体现在它拥有极其简洁直观的API,几行代码就能搭建起一个功能强大的日志系统,支持多种日志格式、多目的地输出(控制台、文件、系统日志等)、日志滚动等高级特性。

简单来说,spdlog让你能用最小的性能代价,获得最全面、最可靠的日志能力。它已经成为C++社区事实上的标准日志库之一,无论是个人小项目还是大型商业软件,都能从中受益。接下来,我将从一个实战者的角度,带你从零开始,深入spdlog的每一个核心环节,不仅告诉你“怎么用”,更会剖析“为什么这么用”,以及在实际项目中如何避开那些隐形的“坑”。

2. 核心设计思路与架构解析

要玩转spdlog,不能只停留在调API的层面,理解其背后的设计思路,才能在使用时做出最合适的选择。它的架构可以概括为“Logger-Sink”模型,这是一种非常经典且灵活的日志库设计模式。

2.1 Logger:日志的记录与分发中心

Logger是你在代码中直接打交道的对象。你调用spdlog::info(...),实际上是在调用某个Logger实例的方法。每个Logger都有一个名字,用于标识。它的核心职责是:

  1. 接收日志消息:当你调用日志宏时,Logger会收集当前的时间戳、日志级别、源文件、行号、函数名以及你传入的格式化消息。
  2. 格式化消息:根据你设定的格式模式(pattern),将上述信息组合成一条完整的日志字符串。
  3. 分发给Sink:将格式化后的字符串,分发给所有注册到该Logger上的Sink(输出槽)去处理。

一个Logger可以绑定多个Sink,这意味着一条日志可以同时输出到控制台和文件,甚至远程服务器。这种解耦设计带来了巨大的灵活性。

2.2 Sink:日志的最终输出目的地

Sink决定了日志的去向。spdlog内置了丰富的Sink类型:

  • 基础控制台Sink(stdout_color_sink_mt): 输出到标准输出,并支持彩色显示(在支持ANSI颜色的终端上),_mt后缀表示它是线程安全的(multi-threaded)。
  • 基础文件Sink(basic_file_sink_mt): 输出到单个指定的文件。
  • 滚动文件Sink(rotating_file_sink_mt): 这是生产环境最常用的Sink之一。它可以设置单个日志文件的最大尺寸和最大备份数量。当文件大小超过限制时,会自动滚动(rotate):将当前文件重命名(例如app.log变为app.log.1),然后创建一个新的app.log继续写入。这避免了单个日志文件无限膨胀。
  • 按时间滚动Sink(daily_file_sink_mt): 每天在指定时间(如午夜)创建一个新的日志文件。这对于按天归档日志非常方便。
  • 系统日志Sink(syslog_sink): 在Linux/Unix系统上,将日志输出到系统日志服务(如syslogjournald)。

注意:选择Sink时,后缀_st(单线程)和_mt(多线程)至关重要。在单线程应用或明确知道日志只在特定线程调用的场景,可以使用_st版本以获得极致的性能(避免锁开销)。但在绝大多数多线程应用中,必须使用_mt版本,否则会导致数据竞争和程序崩溃。这是一个新手极易踩的坑。

2.3 异步日志:性能提升的关键

这是spdlog高性能的“灵魂”。在同步模式下,每次调用日志函数,都会阻塞当前线程,直到日志被真正写入文件或控制台。I/O操作(尤其是文件写入)相对CPU计算来说是非常慢的,频繁的阻塞会严重拖慢主业务逻辑。

异步日志模式引入了一个后台工作线程和一个内存队列。其工作流程如下:

  1. 前端业务线程调用日志函数时,并不直接执行I/O操作。
  2. 日志消息被快速格式化后,作为一个任务推入内存队列。这个操作通常只涉及内存拷贝和指针操作,非常快。
  3. 前端线程随即返回,继续执行业务逻辑,几乎无感知延迟。
  4. 后台有一个独立的线程(或线程池)不断从队列中取出日志任务,批量地、顺序地执行实际的I/O写入操作。

这样做的好处是,将耗时的I/O操作与业务逻辑的执行在时间上分离开来,由后台线程承担,前端线程的延迟极低。对于写日志非常频繁的应用,性能提升可以达到几个数量级。

当然,异步模式也有代价:在程序异常崩溃时,内存队列中尚未被写入的日志可能会丢失。spdlog提供了flush()方法和策略来缓解这个问题,我们会在后面详细讨论。

3. 从零开始的spdlog实战配置

理论说再多,不如动手搭一个。我们从一个最简单的控制台日志开始,逐步构建一个适合生产环境的、功能完备的日志系统。

3.1 基础安装与项目集成

首先是如何获取spdlog。对于现代C++项目,最推荐的方式是使用包管理器。

使用vcpkg (Windows/Linux/macOS):

# 安装spdlog vcpkg install spdlog

然后在你的CMakeLists.txt中:

find_package(spdlog CONFIG REQUIRED) target_link_libraries(your_target PRIVATE spdlog::spdlog)

使用Conan:

# 添加spdlog依赖 conan install spdlog/1.x.y@

在你的conanfile.txt或CMake中配置相应的依赖。

直接包含头文件(最快速):spdlog是一个头文件库(Header-only),你也可以直接下载源码,将include目录添加到你的项目头文件路径中即可。这是快速原型开发时最方便的方式。

3.2 创建你的第一个Logger

让我们写一个最简单的例子,感受一下spdlog的简洁。

#include <iostream> #include “spdlog/spdlog.h” #include “spdlog/sinks/stdout_color_sinks.h” // 需要包含特定的sink头文件 int main() { // 1. 创建一个输出到控制台(带颜色)的logger,命名为 “console_logger” auto console_logger = spdlog::stdout_color_mt(“console_logger”); // 2. 设置该logger的日志级别为 “debug” // 级别从低到高: trace, debug, info, warn, error, critical, off console_logger->set_level(spdlog::level::debug); // 3. 设置该logger的输出格式 // %Y-%m-%d %H:%M:%S: 年月日 时分秒 // %^ ... %$: 颜色范围(对支持颜色的sink有效) // %l: 日志级别缩写 // %v: 用户实际传入的日志消息 console_logger->set_pattern(“[%Y-%m-%d %H:%M:%S] [%^%l%$] %v”); // 4. 使用它! console_logger->trace(“This is a trace message.”); // 由于级别为debug,这条不会输出 console_logger->debug(“This is a debug message.”); console_logger->info(“Welcome to spdlog!”); console_logger->warn(“This is a warning.”); console_logger->error(“This is an error message.”); console_logger->critical(“Critical issue encountered!”); // 5. 也可以使用全局默认logger(spdlog默认已创建了一个stdout的logger) spdlog::set_level(spdlog::level::info); spdlog::set_pattern(“[%H:%M:%S] [%l] %v”); spdlog::info(“Using the default logger!”); spdlog::error(“An error via default logger.”); // 6. 确保所有缓冲的日志被刷新到目的地(对于文件输出尤其重要) spdlog::shutdown(); return 0; }

编译运行,你会看到彩色的、带时间戳和级别的日志输出到控制台。debug级别的消息没有出现,因为我们把级别设为了info。这就是日志级别过滤的作用,在生产环境我们可以设置为warnerror,避免海量的调试信息淹没有效日志。

3.3 配置一个生产级文件Logger

控制台日志适合开发调试,生产环境我们更需要可靠的、可管理的文件日志。下面配置一个同时支持按大小滚动异步写入的Logger。

#include “spdlog/spdlog.h” #include “spdlog/async.h” // 异步日志核心头文件 #include “spdlog/sinks/rotating_file_sink.h” #include “spdlog/sinks/stdout_color_sinks.h” void setup_production_logger() { try { // 第一步:配置异步日志器。这是高性能的关键。 // 参数:队列大小(1048576条),后台线程数(1),如果队列满则阻塞(block)。 spdlog::init_thread_pool(8192, 1); // 初始化全局线程池 auto async_factory = std::make_shared<spdlog::async_factory>(); // 第二步:创建Sinks(输出目的地) // 1. 滚动文件Sink:单个文件最大100MB,最多保留5个备份文件。 // 文件命名规则:app.log, app.log.1, app.log.2 ... auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>( “logs/app.log”, 1048576 * 100, 5); // 100MB * 5 // 2. 控制台Sink(可选,生产环境可能关闭) auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>(); // 第三步:创建Logger,并绑定多个Sinks // 使用async_factory来创建异步logger std::vector<spdlog::sink_ptr> sinks {file_sink, console_sink}; auto combined_logger = spdlog::create_async_nb<spdlog::sinks::stdout_color_sink_mt>(“prod_logger”, sinks.begin(), sinks.end()); // 第四步:配置Logger combined_logger->set_level(spdlog::level::info); // 生产环境通常设为info或warn // 一个更详细的生产环境格式,包含线程ID combined_logger->set_pattern(“[%Y-%m-%d %H:%M:%S.%e] [%t] [%^%l%$] [%s:%#] %v”); // %e: 毫秒 // %t: 线程ID // %s: 源文件名 // %#: 行号 // 第五步:设置为全局默认logger,这样可以直接使用spdlog::info()等宏 spdlog::set_default_logger(combined_logger); // 第六步:注册一个定期刷新和异常处理(可选但推荐) // 例如,每3秒自动刷新一次日志到磁盘,减少意外崩溃时的日志丢失。 // spdlog::flush_every(std::chrono::seconds(3)); spdlog::info(“Production logger setup successfully.”); } catch (const spdlog::spdlog_ex& ex) { // spdlog自身的异常,通常是文件权限、路径问题 std::cerr << “Log initialization failed: “ << ex.what() << std::endl; // 可以考虑降级到简单的控制台日志或退出 } }

这个setup_production_logger函数做了几件关键事情:

  1. 初始化异步线程池:创建了一个能容纳8192条日志的消息队列和1个后台工作线程。async_factory是创建异步Logger的工厂。
  2. 创建滚动文件Sink:这是核心。它保证了日志文件不会无限大,自动归档,便于管理和查看。
  3. 组合多个Sink:日志会同时写入文件和终端,这在部署初期排查问题非常有用。
  4. 设置了丰富的格式:包含了毫秒、线程ID、文件名和行号。在线程并发和问题定位时,这些信息是黄金。
  5. 异常处理:任何库都可能出错,特别是文件I/O。捕获spdlog_ex异常并做降级处理,是编写健壮代码的好习惯。

实操心得:关于异步队列大小。8192是一个比较折中的值。设置太小,在高并发日志洪峰下,队列可能迅速填满,导致前端线程阻塞(如果使用阻塞策略)或丢日志(如果使用非阻塞策略)。设置太大,则会消耗更多内存,并且在程序崩溃时潜在丢失的日志也更多。你需要根据自己应用的日志频率来调整。一个经验法则是,估算每秒峰值日志条数,然后设置一个能缓冲2-3秒日志量的队列。

4. 高级特性与性能调优实战

基础功能搭建好后,我们来看看如何利用spdlog的高级特性来应对更复杂的场景,并进一步压榨性能。

4.1 条件日志与频率限制日志

有时候,我们只希望在特定条件下记录日志,或者避免某些高频日志刷屏。

// 条件日志:只有当条件满足时才记录,避免不必要的字符串格式化开销。 int retry_count = 5; SPDLOG_LOGGER_WARN_IF(my_logger, retry_count > 3, “Retry count ({}) is unusually high.”, retry_count); // 等价于:if (retry_count > 3) my_logger->warn(...); // 使用全局默认logger的条件日志宏 SPDLOG_WARN_IF(some_condition, “This warning only appears if condition is true.”); // 频率限制日志:无论调用多少次,在指定时间间隔内只输出一次。 // 这对于循环内的高频调试日志或心跳日志非常有用。 auto rate_logger = spdlog::default_logger(); for (int i = 0; i < 10000; ++i) { // 这条日志每5秒最多输出一次 SPDLOG_LOGGER_INFO_EVERY_N(rate_logger, std::chrono::seconds(5), “Processing iteration {}, this log is throttled.”, i); // … 业务逻辑 }

SPDLOG_LOGGER_xxx_IFSPDLOG_xxx_IF宏在条件为false时,不会进行参数格式化,这对于性能敏感路径上的调试日志至关重要。而EVERY_N相关的宏则能有效防止日志文件被无用的重复信息填满。

4.2 日志级别动态切换

在生产环境,我们可能需要在运行时临时调低日志级别来收集更多调试信息,而不需要重启服务。spdlog通过信号或外部配置可以轻松实现。

一种常见做法是将loggerlevel设置为spdlog::level::trace(即记录所有级别),然后通过一个自定义的filter来控制输出。更简单的方式是直接更换loggerlevel。你可以通过一个HTTP接口、配置文件监听或信号(如SIGUSR1)来触发级别的变更。

// 假设我们有一个全局可访问的logger指针 g_logger extern std::shared_ptr<spdlog::logger> g_logger; void handle_signal(int sig) { if (sig == SIGUSR1) { // 收到SIGUSR1信号,将日志级别切换到debug g_logger->set_level(spdlog::level::debug); g_logger->info(“Log level changed to DEBUG via signal.”); } else if (sig == SIGUSR2) { // 收到SIGUSR2信号,将日志级别切换回info g_logger->set_level(spdlog::level::info); g_logger->info(“Log level changed back to INFO.”); } } // 在主函数中注册信号处理 int main() { // … 初始化g_logger … std::signal(SIGUSR1, handle_signal); std::signal(SIGUSR2, handle_signal); // … 主循环 … }

这样,运维人员可以在服务器上执行kill -USR1 <pid>来动态开启调试日志,问题排查完后再用kill -USR2 <pid>关闭,非常方便。

4.3 异步模式下的性能陷阱与优化

异步模式虽好,但配置不当也会成为问题。

陷阱一:队列满策略创建异步logger时,可以指定队列满时的策略:

  • async_overflow_policy::block: 队列满时,前端线程阻塞等待。这能保证不丢日志,但可能引起业务线程卡顿。
  • async_overflow_policy::overrun_oldest: 队列满时,丢弃队列中最老的日志。这能保证业务线程不阻塞,但会丢日志。
// 创建异步logger时指定策略(默认为block) auto logger = spdlog::create_async_nb<spdlog::sinks::stdout_color_sink_mt>( “async_logger”, spdlog::thread_pool(), async_overflow_policy::overrun_oldest // 指定丢弃最旧日志 );

如何选择?对于必须保证日志完整性的场景(如审计、关键交易流水),应使用block并设置足够大的队列。对于性能极度敏感、可以容忍少量日志丢失的场景(如高频指标打点),可以考虑overrun_oldest绝大多数情况下,建议使用block,并通过增大队列尺寸来避免阻塞。

陷阱二:格式化开销即使使用了异步,格式化日志消息这个动作仍然发生在前端线程。如果日志消息的格式化非常复杂(例如拼接大字符串、转换复杂对象),这个开销依然可观。

优化建议:

  1. 使用SPDLOG_LOGGER_xxx宏:它们会在判断级别不满足时提前返回,避免格式化。
  2. 惰性求值:对于构造日志消息成本很高的参数,可以使用lambda表达式进行惰性求值(spdlog支持回调)。
    auto expensive_to_string = []() -> std::string { // … 非常耗时的字符串构造过程 … return result; }; logger->info(“Data: {}”, expensive_to_string()); // 这样写,无论级别如何,lambda都会被执行 // 更好的方式:使用条件判断包裹 if (logger->should_log(spdlog::level::info)) { logger->info(“Data: {}”, expensive_to_string()); }
  3. 简化格式模式:过于复杂的set_pattern(例如包含获取调用栈)会影响每次日志调用的性能。在生产环境,使用必要的信息即可。

陷阱三:后台线程异常如果后台写线程在写入时发生异常(如磁盘满、权限错误),默认情况下异常会被捕获并丢弃,你可能会发现日志突然停止写入而不明所以。可以为logger设置error handler

spdlog::set_error_handler([](const std::string& msg) { // 当后台写入发生错误时,这个回调会被调用。 // 你可以在这里向另一个备用位置(如系统日志、标准错误)报告错误。 std::cerr << “SPDLOG ERROR: “ << msg << std::endl; // 或者触发告警 });

5. 集成到大型项目与最佳实践

当项目变得庞大,有多个模块时,如何优雅地管理日志?

5.1 多Logger与日志分类

不要所有模块都共用同一个默认logger。为不同的组件或功能域创建独立的logger,可以更精细地控制日志级别和输出目的地。

// network_module.cpp namespace network { auto logger = spdlog::stdout_color_mt(“network”); // 名为“network”的logger logger->set_pattern(“[%H:%M:%S] [%^%l%$] [NET] %v”); void connect() { logger->info(“Attempting to connect…”); } } // database_module.cpp namespace db { auto logger = spdlog::stdout_color_mt(“database”); logger->set_pattern(“[%H:%M:%S] [%^%l%$] [DB] %v”); void query() { logger->debug(“Executing query…”); } }

这样,在日志中通过[NET][DB]前缀就能清晰地区分来源。你甚至可以配置让networklogger只输出到文件A,databaselogger只输出到文件B。

5.2 与现有基础设施集成

集成到系统日志(Linux/Unix):

#include <spdlog/sinks/syslog_sink.h> auto syslog_logger = spdlog::syslog_logger_mt(“syslog”, “myapp”, LOG_PID); syslog_logger->info(“This message goes to system journal/syslog.”);

这样,你的应用日志就可以用journalctl -u myapp等标准命令来查看和管理。

自定义Sink:如果内置Sink不满足需求(比如想写入Kafka、Redis或远程HTTP服务),你可以继承spdlog::sinks::base_sink来实现自己的Sink。你需要重写sink_it_flush_方法。

template<typename Mutex> class my_custom_sink : public spdlog::sinks::base_sink<Mutex> { protected: void sink_it_(const spdlog::details::log_msg& msg) override { // msg包含所有日志信息:时间、级别、logger名、原始payload等 spdlog::memory_buf_t formatted; spdlog::sinks::base_sink<Mutex>::formatter_->format(msg, formatted); // 将格式化后的字符串 formatted 发送到你的自定义目的地,例如一个消息队列 // send_to_kafka(fmt::to_string(formatted)); } void flush_() override { // 执行刷新操作,确保所有数据被发送 // flush_kafka_producer(); } }; // 使用 using my_custom_sink_mt = my_custom_sink<std::mutex>; auto custom_sink = std::make_shared<my_custom_sink_mt>(); auto logger = std::make_shared<spdlog::logger>(“custom”, custom_sink);

5.3 生产环境部署清单

在将使用spdlog的应用部署到生产环境前,请对照此清单检查:

  1. 日志级别:确保默认级别设置为infowarn,避免debug/trace日志刷屏。
  2. 日志滚动:必须使用rotating_file_sinkdaily_file_sink,并设置合理的文件大小(如100MB-1GB)和备份数量(如5-10个)。定期清理过期日志的脚本也需要准备。
  3. 异步模式:对于性能要求高的服务,务必启用异步日志。仔细评估并设置队列大小(如8192,16384)和满队列策略(通常用block)。
  4. 定期刷新:调用spdlog::flush_every(std::chrono::seconds(3)),可以降低崩溃时的日志丢失风险。在程序正常退出的地方(如main函数返回前、信号处理函数中)调用spdlog::shutdown()spdlog::drop_all()来确保所有日志被刷新。
  5. 错误处理:设置spdlog::set_error_handler,监控日志系统自身的异常。
  6. 日志格式:包含足够的信息,至少应有时间戳级别线程ID消息文件名行号对调试有帮助,但会轻微影响性能,可根据需要取舍。
  7. 日志目录权限:确保运行进程的用户对日志目录有写权限。这是最常出问题的地方之一。
  8. 监控与告警:不要只写不管。需要有监控机制来关注日志文件是否在正常增长,是否有大量的ERRORCRITICAL级别日志出现,并配置相应的告警。

6. 常见问题排查与性能压测

即使配置得当,在实际运行中也可能遇到各种问题。这里记录一些典型场景和排查思路。

6.1 问题排查速查表

现象可能原因排查步骤与解决方案
日志完全不输出1. 日志级别设置过高。
2. Logger未正确创建或注册。
3. Sink初始化失败(如文件路径错误)。
4. 异步模式下,程序崩溃过快,后台线程来不及写。
1. 检查logger->level(),临时设为spdlog::level::trace
2. 确认spdlog::get(“logger_name”)能返回有效的logger指针。
3. 检查try-catch块是否捕获了spdlog_ex异常。
4. 在程序退出前调用spdlog::shutdown()sleep一小会儿观察。
日志输出到控制台但没到文件1. 文件Sink未成功添加到logger。
2. 文件路径权限不足。
3. 磁盘空间已满。
1. 确认创建logger时传入了file_sink。
2. 检查日志文件所在目录的写权限。
3. 检查磁盘使用情况。
异步日志丢失1. 队列满且策略为overrun_oldest
2. 程序崩溃或强制终止,内存队列中的数据未写入磁盘。
1. 增加队列大小,或改用block策略。
2. 缩短flush_every的间隔,或在关键逻辑后手动调用logger->flush()
性能差,CPU占用高1. 使用了同步模式且日志频率极高。
2. 格式模式(pattern)过于复杂。
3. 日志消息本身的格式化开销大(如频繁转换大数据结构)。
1. 切换到异步模式。
2. 简化格式,移除不必要的字段(如调用栈)。
3. 使用条件日志宏,或将对数语句放入级别判断中,避免不必要的格式化。
日志文件没有按预期滚动1.rotating_file_sink的大小阈值设置过大,还未触发。
2. 多个进程同时写同一个日志文件(未使用_mt版本或进程间未协调)。
1. 检查设置的max_size参数。
2. 确保每个进程使用不同的日志文件,或使用支持进程间锁的机制(spdlog默认不处理多进程)。

6.2 简易性能对比测试

说一千道一万,不如一个简单的测试有说服力。我们可以写个小程序对比同步和异步模式的性能差异。

#include <spdlog/spdlog.h> #include <spdlog/async.h> #include <spdlog/sinks/basic_file_sink.h> #include <chrono> #include <thread> void benchmark_logging(const std::string& logger_name, bool async, int thread_count, int messages_per_thread) { spdlog::drop_all(); // 清理之前的logger if (async) { spdlog::init_thread_pool(8192, 1); // 初始化线程池 } auto sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(“benchmark.log”, true); std::shared_ptr<spdlog::logger> logger; if (async) { logger = spdlog::create_async_nb<spdlog::sinks::basic_file_sink_mt>(logger_name, {sink}); } else { logger = std::make_shared<spdlog::logger>(logger_name, sink); } logger->set_pattern(“%v”); // 最简单的格式,只记录消息本身,减少格式化开销 std::vector<std::thread> threads; auto start = std::chrono::high_resolution_clock::now(); for (int i = 0; i < thread_count; ++i) { threads.emplace_back([logger, messages_per_thread, i]() { for (int j = 0; j < messages_per_thread; ++j) { logger->info(“Thread {}: Message {}”, i, j); } }); } for (auto& t : threads) { t.join(); } if (async) { // 对于异步logger,需要等待队列清空 spdlog::shutdown(); } auto end = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count(); long total_messages = thread_count * messages_per_thread; double msg_per_sec = total_messages / (duration / 1000.0); printf(“Mode: %s, Threads: %d, Messages/Thread: %d, Total: %ld, Time: %ld ms, Msg/sec: %.2f\n”, async ? “Async” : “Sync”, thread_count, messages_per_thread, total_messages, duration, msg_per_sec); } int main() { // 测试单线程,写10万条日志 benchmark_logging(“sync_single”, false, 1, 100000); benchmark_logging(“async_single”, true, 1, 100000); // 测试4个线程,每个写2万5千条,总共10万条 benchmark_logging(“sync_multi”, false, 4, 25000); benchmark_logging(“async_multi”, true, 4, 25000); return 0; }

在我的测试环境(普通SSD,4核CPU)下,结果趋势非常明显:异步模式的吞吐量(Msg/sec)远高于同步模式,尤其是在多线程场景下,差距可达数十甚至上百倍。同步模式下的多线程会因为竞争文件锁而导致性能急剧下降,而异步模式则能保持高吞吐。这个测试直观地展示了为什么在高性能场景下异步日志是必选项。

最后,关于spdlog的使用,我的体会是,它完美地平衡了性能、易用性和功能性。它让你几乎不需要关心底层细节,就能获得一个工业级的日志解决方案。但“不需要关心”不等于“不应该了解”,理解其异步机制、Sink模型和性能影响因素,能帮助你在面对复杂场景时做出正确的决策和调优。记住,日志是线上问题排查的生命线,一个稳定、高效、可靠的日志系统,是任何严肃C++项目的基石。花点时间把它配置好,绝对是一笔划算的投资。