Spdlog异步日志解析

2023/04/19 日志 共 1814 字,约 6 分钟

TL;DL

spdlog是一个head-only的日志库,其中sink簇类用于把日志写到不同的地方,比如 syslog、终端或者文件。Spdlog依靠fmt库格式化,这篇文章主要研究spdlog的异步日志机制。

How to use

首先简单介绍一下spdlog如何创建异步日志logger的,代码如下所示,首先线程池需要指定环形队列的大小以及线程数目,然后创建file_sink,即负责把日志写到文件的sink类,最终创建异步日志loggerasync_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,注册中心负责设置levelformatter,管理全部日志logger(其实就是一个哈希表,记录loggername)。用注册中心注册日志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>; 

spdlog.drawio

日志前端线程(即使用日志的线程)调用logger->info等API,这些API会调用post_log把日志送到线程池的环形队列中,队列由一把锁保护,线程池中的工作线程从环形队列中取出日志,调用process_next_msg,最终会调用logger中设置的不同的sink类的写日志,把日志写到不同的地方。这文件这里,最终会调用 std::fwrite

spdlogflush策略还算是比较灵活

  1. 为了性能,默认情况下,不主动flush,让libc决定什么时候flush

  2. 调用 logger->flush()手动flush

  3. 可以设置flush的等级,比如 my_logger->flush_on(spdlog::level::err); 这会在出现错误的情况下flush

  4. spdlog也支持定时flush,这是由一个单独的线程执行的,这个线程会在每个logger定时调用flush,因此只能在线程安全的logger中使用

    spdlog::flush_every(std::chrono::seconds(5));
    

    在使用这个的时候需要注意一点,在共享库中,在unload这个共享库的时候,必须调用 spdlog::shutdown()

Conclusion

spdlog的设计给我的感觉不是很高性能,比较偏向于功能化,其中提供了大量的sink类,可以把日志输出到各种地方,以及基于fmt的各种格式化日志,功能多得眼花缭乱。单对于性能来说,感觉就不是很出众。甚至async_msg拷贝入队列,拷贝出队列(虽然是调用的移动复制函数,但是对象本身的大小也不小) ,环形队列加锁也是一把锁锁死。也许这种日志的瓶颈并不是在线程池吧。

Search

    Table of Contents