最近用很简洁的代码(500行以内)实现了一个高效可拓展的异步C++日志库:RING LOG,本文分享了了其设计方案与技术原理等内容
详细代码见github路径:点击打开链接
导论
同步日志与缺点
传统的日志也叫同步日志,每次调用一次打印日志API就对应一次系统调用write写日志文件,在日志产生不频繁的场景下没什么问题
可是,如果日志打印很频繁,同步日志有什么问题?
- 一方面,大量的日志打印陷入等量的write系统调用,有一定系统开销
- 另一方面,使得打印日志的进程附带了大量同步的磁盘IO,影响性能
那么,如何解决如上的问题?就是
异步日志与队列实现的缺点
异步日志,按我的理解就是主线程的日志打印接口仅负责生产日志数据(作为日志的生产者),而日志的落地操作留给另一个后台线程去完成(作为日志的消费者),这是一个典型的生产-消费问题,如此一来会使得:
主线程调用日志打印接口成为非阻塞操作,同步的磁盘IO从主线程中剥离出来,有助于提高性能
对于异步日志,我们很容易借助队列来一个实现方式:主线程写日志到队列,队列本身使用条件变量、或者管道、eventfd等通知机制,当有数据入队列就通知消费者线程去消费日志
但是,这样的异步队列也有一定的问题:
- 生产者线程产生N个日志,对应后台线程就会被通知N次,频繁日志写入会造成一定性能开销
- 不同队列实现方式也各有缺点:
- 用数组实现:空间不足时,队列内存不易拓展
- 用链表实现:每条消息的生产消费都对应内存的创建销毁,有一定开销
好了,可以开始正文了
简介
RING LOG是一个适用于C++的异步日志, 其特点是效率高(实测每秒支持125+万日志写入)、易拓展,尤其适用于频繁写日志的场景
一句话介绍原理:
使用多个大数组缓冲区作为日志缓冲区,多个大数组缓冲区以双循环链表方式连接,并使用两个指针p1
和p2
指向链表两个节点,分别用以生成数据、与消费数据
生产者可以是多线程,共同持有p1
来生产数据,消费者是一个后台线程,持有p2
去消费数据
大数组缓冲区 + 双循环链表的设计,使得日志缓冲区相比于队列有更强大的拓展能力、且避免了大量内存申请释放,提高了异步日志在海量日志生成下的性能表现
此外,RING LOG还优化了每条日志的UTC格式时间的生成,明显提高日志性能
具体工作原理
数据结构
Ring Log的缓冲区是若干个cell_buffer
以双向、循环的链表组成
cell_buffer
是简单的一段缓冲区,日志追加于此,带状态:
FREE
:表示还有空间可追加日志FULL
:表示暂时无法追加日志,正在、或即将被持久化到磁盘;
Ring Log有两个指针:
Producer Ptr
:生产者产生的日志向这个指针指向的cell_buffer
里追加,写满后指针向前移动,指向下一个cell_buffer
;Producer Ptr
永远表示当前日志写入哪个cell_buffer
,被多个生产者线程共同持有Consumer Ptr
:消费者把这个指针指向的cell_buffer
里的日志持久化到磁盘,完成后执行向前移动,指向下一个cell_buffer
;Consumer Ptr
永远表示哪个cell_buffer
正要被持久化,仅被一个后台消费者线程持有