Tinynet日志简介

2023/03/05 tinynet 日志 共 1786 字,约 6 分钟

写在前面

日志一般分为同步日志以及异步日志,同步日志把日志写入到磁盘,而异步日志把写入磁盘的操作交给后端线程,因为IO操作会影响日志的性能。在tinynet中实现了同步以及异步日志,使用C print风格,性能微秒级别。

Sync Log

同步日志的实现比较简单,利用可变参数把数据整理后输入到4KB的栈中,写入磁盘(默认是终端),在写日志的时候,需要用到 __FILE__宏得到文件名字,依靠编译期得到文件名字会稍微增加性能,C++14允许constexptr修饰的函数体内有多个语句。但是C++11只能有return语句以及using、typedef等语句。因此C++11得到文件名字是稍有难度的。

// get the filename during the compile time
constexpr const char* str_end(const char* str) {
  return *str ? str_end(str + 1) : str;
}
constexpr const char* last_slant(const char* str) {
  return *str == '/' ? str + 1 : last_slant(str - 1);
} 
constexpr bool has_slant(const char* str) {
  return *str == '/' ? true : (*str ? has_slant(str + 1) : false);
}
constexpr const char* staticGetFilename(const char* str) {
  return has_slant(str) ? last_slant(str_end(str)) : str;
}
constexpr int staticGetLength(const char* str) {
  return *str ?  staticGetLength(str + 1) + 1 : 1;
}

代码写出来还是很容易理解,主要思想使用? : 递归访问字符串。

Asynchronous log

异步日志分为2部分,暂称为前端部分和后端部分。前端使用LOG_TRACE等宏定义写入缓存队列,后端把缓存队列的日志写入文件中。采用双缓冲区(double buffering)交互技术,可以减少持锁时间。

log

首先,介绍前端的流程,LOG_TRACE等日志的宏每一条写入4KB的栈上,最后压入4MB的currentBuffer_中,当currentBuffer_满了之后压入前端日志缓冲队列,使用备用的nextBuffer_,这时候唤醒后端的写日志线程。

如果写入的速度特别快,nextBuffer_都不够用,会new出新的Buffer。

后端线程加锁,保护缓存队列,把提前准备的缓存队列和前端的缓存队列进行交换,大大减少持锁时间,之后立马把前端没有写满的nextBuffer_也相同的压入队列。然后用提前准备的存在后端的2个Buffer补充前端的currentBuffer_nextBuffer_,在这之后,就可以解锁了。前端日志又可以继续写入,可以看出来,这个结构尽可能的降低了持锁时间。

解锁之后,后端就简单的把日志写入文件,然后把后端的2个备用Buffer补充好即可。如果日志过多,直接丢弃一部分,不然队列在极端情况下会越来越长。

整个结构非常的高效,提高了程序的并发性

Performance

一个日志最重要的就是性能,如果在日志中耗费了大量时间,就会让我们忌惮性能损失而不敢放手写log,不便于调试和维护

目前只是粗略的判断了性能。

写一条普通的日志卡同步日志前端需要耗时60us左右,而异步日志仅需2us,虽然速度也不算是非常快,但写入超长日志也仅需4us一条。比如下面这条日志

string longstr = "Hello 0123456789  abcdefghijklmnopqrstuvwxyz" + string(3000, 'X');

如何提高日志性能

我认为目前这个日志的瓶颈大概在2个地方,首先是时间的获取太昂贵,如果能够通过一些指令访问CPU的tick,说不定会更快,但可能会降低通用性。另一个非常重要的点是格式化字符串是异常昂贵的,在这里,我们是格式化好了字符串,后端只负责写入,如果格式化字符串能交给后端解决会更快,这里应该需要用到模板元编程,把meta信息从编译期剥离出来,一开始保存所有的日志格式,只需要把数据传送即可,例如NanoLog,实现了纳秒级别的写入

Search

    Table of Contents