TL;DL
spdlog是一个head-only的日志库,其中sink簇类用于把日志写到不同的地方,比如 syslog、终端或者文件。Spdlog依靠fmt库格式化,这篇文章主要研究spdlog的异步日志机制。
How to use
首先简单介绍一下spdlog如何创建异步日志logger的,代码如下所示,首先线程池需要指定环形队列的大小以及线程数目,然后创建file_sink,即负责把日志写到文件的sink类,最终创建异步日志logger,async_overflow_policy::block
是一种策略,表示当环形队列满了的时候,前端线程会阻塞而不是覆盖,当然,还有另一种策略 overrun_oldest
即覆盖最久的日志。
auto tp = std::make_shared<details::thread_pool>(queue_size, 1);
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(filename, true);
auto logger = std::make_shared<async_logger>("async_logger", std::move(file_sink), std::move(tp), async_overflow_policy::block);
//调用info等成员函数即可写日志到文件
logger->info("Hello logger: msg number {}", i);
也可以使用全局的注册中心来注册一个日志logger,这样做的好处是方便管理多个logger,注册中心负责设置level,formatter,管理全部日志logger(其实就是一个哈希表,记录logger和name)。用注册中心注册日志logger,默认线程池一个线程+8192条日志的容量。
auto async_file = spdlog::basic_logger_mt<spdlog::async_factory>("async_file_logger", "logs/async_log.txt");
How it works
其中异步日志的核心就是一个线程池,如下图所示。线程池中的环形队列中存储类 async_msg
(而不是指向日志的指针)。
日志是存储在
async_msg
的成员变量memory_buf_t中, 定义如下,前250个char存储在对象本身,之后需要alloc分配。using memory_buf_t = fmt::basic_memory_buffer<char, 250>;
日志前端线程(即使用日志的线程)调用logger->info
等API,这些API会调用post_log
把日志送到线程池的环形队列中,队列由一把锁保护,线程池中的工作线程从环形队列中取出日志,调用process_next_msg
,最终会调用logger中设置的不同的sink
类的写日志,把日志写到不同的地方。这文件这里,最终会调用 std::fwrite
!
spdlog的flush策略还算是比较灵活
为了性能,默认情况下,不主动flush,让libc决定什么时候flush
调用
logger->flush()
手动flush,可以设置flush的等级,比如
my_logger->flush_on(spdlog::level::err);
这会在出现错误的情况下flush
spdlog
也支持定时flush
,这是由一个单独的线程执行的,这个线程会在每个logger
定时调用flush
,因此只能在线程安全的logger
中使用spdlog::flush_every(std::chrono::seconds(5));
在使用这个的时候需要注意一点,在共享库中,在unload这个共享库的时候,必须调用
spdlog::shutdown()
Conclusion
spdlog的设计给我的感觉不是很高性能,比较偏向于功能化,其中提供了大量的sink类,可以把日志输出到各种地方,以及基于fmt
的各种格式化日志,功能多得眼花缭乱。单对于性能来说,感觉就不是很出众。甚至async_msg
拷贝入队列,拷贝出队列(虽然是调用的移动复制函数,但是对象本身的大小也不小) ,环形队列加锁也是一把锁锁死。也许这种日志的瓶颈并不是在线程池吧。