每秒百万级高效C++异步日志实践

本文介绍了RING LOG,一个每秒支持百万级日志写入的C++异步日志库。通过使用双循环链表和大数组缓冲区,它优化了UTC时间生成并提高了性能。在单线程和多线程测试中,RING LOG的性能远超传统同步日志。
摘要由CSDN通过智能技术生成

最近用很简洁的代码(500行以内)实现了一个高效可拓展的异步C++日志库:RING LOG,本文分享了了其设计方案与技术原理等内容 
详细代码见github路径:点击打开链接

导论

同步日志与缺点

传统的日志也叫同步日志,每次调用一次打印日志API就对应一次系统调用write写日志文件,在日志产生不频繁的场景下没什么问题

可是,如果日志打印很频繁,同步日志有什么问题?

  • 一方面,大量的日志打印陷入等量的write系统调用,有一定系统开销
  • 另一方面,使得打印日志的进程附带了大量同步的磁盘IO,影响性能

那么,如何解决如上的问题?就是

异步日志与队列实现的缺点

异步日志,按我的理解就是主线程的日志打印接口仅负责生产日志数据(作为日志的生产者),而日志的落地操作留给另一个后台线程去完成(作为日志的消费者),这是一个典型的生产-消费问题,如此一来会使得:

主线程调用日志打印接口成为非阻塞操作,同步的磁盘IO从主线程中剥离出来,有助于提高性能

对于异步日志,我们很容易借助队列来一个实现方式:主线程写日志到队列,队列本身使用条件变量、或者管道、eventfd等通知机制,当有数据入队列就通知消费者线程去消费日志

但是,这样的异步队列也有一定的问题:

  • 生产者线程产生N个日志,对应后台线程就会被通知N次,频繁日志写入会造成一定性能开销
  • 不同队列实现方式也各有缺点: 
    • 用数组实现:空间不足时,队列内存不易拓展
    • 用链表实现:每条消息的生产消费都对应内存的创建销毁,有一定开销

好了,可以开始正文了

简介

RING LOG是一个适用于C++的异步日志, 其特点是效率高(实测每秒支持125+万日志写入)、易拓展,尤其适用于频繁写日志的场景

一句话介绍原理:

使用多个大数组缓冲区作为日志缓冲区,多个大数组缓冲区以双循环链表方式连接,并使用两个指针p1p2指向链表两个节点,分别用以生成数据、与消费数据 
生产者可以是多线程,共同持有p1来生产数据,消费者是一个后台线程,持有p2去消费数据

大数组缓冲区 + 双循环链表的设计,使得日志缓冲区相比于队列有更强大的拓展能力、且避免了大量内存申请释放,提高了异步日志在海量日志生成下的性能表现

此外,RING LOG还优化了每条日志的UTC格式时间的生成,明显提高日志性能

具体工作原理

数据结构

Ring Log的缓冲区是若干个cell_buffer以双向、循环的链表组成 
cell_buffer是简单的一段缓冲区,日志追加于此,带状态:

  • FREE:表示还有空间可追加日志
  • FULL:表示暂时无法追加日志,正在、或即将被持久化到磁盘;

Ring Log有两个指针:

  • Producer Ptr:生产者产生的日志向这个指针指向的cell_buffer里追加,写满后指针向前移动,指向下一个cell_bufferProducer Ptr永远表示当前日志写入哪个cell_buffer被多个生产者线程共同持有
  • Consumer Ptr:消费者把这个指针指向的cell_buffer里的日志持久化到磁盘,完成后执行向前移动,指向下一个cell_bufferConsumer Ptr永远表示哪个cell_buffer正要被持久化,仅被一个后台消费者线程持有

/****************************************************************************** Module: VC-Logger Purpose: 记录程序日志。 1. 把日志信息输出到指定文件 2. 对于 GUI 程序,可以把日志信息发送到指定窗口 3. 对于Console应用程序,可以把日志信息发往标准输出 (std::cout) Desc: 1、功能: -------------------------------------------------------------------------------------- a) 把日志信息输出到指定文件 b) 每日生成一个日志文件 c) 对于 GUI 程序,可以把日志信息发送到指定窗口 d) 对于Console应用程序,可以把日志信息发往标准输出 (std::cout) e) 支持 MBCS / UNICODE,Console / GUI,win32 / x64 程序 f) 支持动态加载和静态加载日志组件 DLL g) 支持 DEBUG/TRACE/INFO/WARN/ERROR/FATAL 等多个日志别 2、可用性: -------------------------------------------------------------------------------------- a) 简单纯净:不依赖任何程序库或框架 b) 使用接口简单,不需复杂的配置或设置工作 c) 提供 CStaticLogger 和 CDynamicLogger 包装类用于静态或动态加载以及操作日志组件,用户无需关注加载细节 d) 程序如果要记录多个日志文件只需为每个日志文件创建相应的 CStaticLogger 或 CDynamicLogger 对象 e) 只需调用 Log()/Debug()/Trace()/Info()/Warn()/Error()/Fatal() 等方法记录日志 f) 日志记录方法支持可变参数 g) 日志输出格式: 3、性能: -------------------------------------------------------------------------------------- a) 支持多线程同时发送写日志请求 b) 使用单独线程在后台写日志,不影响工作线程的正常执行 c) 采用批处理方式批量记录日志 Usage: 方法一:(静态加载 Logger DLL) -------------------------------------------------------------------------------------- 0. 应用程序包含 StaticLogger.h 头文件 1. 创建 CStaticLogger 对象(通常为全局对象) 2. 调用 CStaticLogger->Init(...) 初始化日志组件 3. 使用 CStaticLogger->Log()/Debug()/Trace()/Info()/Warn()/Error()/Fatal() 等方法写日志 4. 调用 CStaticLogger->UnInit(...) 清理日志组件(CStaticLogger 对象析构时也会自动清理日志组件) 方法二:(动态加载 Logger DLL) -------------------------------------------------------------------------------------- 0. 应用程序包含 DynamicLogger.h 头文件 1. 创建 CDynamicLogger 对象(通常为全局对象) 2. 调用 CDynamicLogger->Init(...) 初始化日志组件 3. 使用 CDynamicLogger->Log()/Debug()/Trace()/Info()/Warn()/Error()/Fatal() 等方法写日志 4. 调用 CDynamicLogger->UnInit(...) 清理日志组件(CDynamicLogger 对象析构时也会自动清理日志组件) 方法三:(直接用导出函数加载 Logger DLL) -------------------------------------------------------------------------
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值