代码链接:https://github.com/Janonez/Log_System
本项目主要实现一个日志系统, 其主要支持以下功能:
日志系统的技术实现主要包括三种类型:
同步日志是指当输出日志时,必须等待日志输出语句执行完毕后,才能执行后面的业务逻辑语句,日志输出语句与程序的业务逻辑语句将在同一个线程运行。每次调用一次打印日志API就对应一次系统调用write写日志文件。
在高并发场景下,随着日志数量不断增加,同步日志系统容易产生系统瓶颈:
异步日志是指在进行日志输出时,日志输出语句与业务逻辑语句并不是在同一个线程中运行,而是有专门的线程用于进行日志输出操作。业务线程只需要将日志放到一个内存缓冲区中不用等待即可继续执行后续业务逻辑(作为日志的生产者),而日志的落地操作交给单独的日志线程去完成(作为日志
的消费者), 这是一个典型的生产-消费模型。
这样做的好处是即使日志没有真的地完成输出也不会影响程序的主业务,可以提高程序的性能:
将一条消息,进行格式化成为指定格式的字符串后,写入到指定位置
本项目实现的是一个多日志器日志系统,主要实现的功能是让程序员能够轻松的将程序运行日志信息落地到指定的位置,且支持同步与异步两种方式的日志落地方式。
提前完成一些零碎的功能接口,以便于项目中会用到。
定义出日志系统所包含的所有日志等级,分别为:
每个项目都会设置一个默认的日志输出等级,只有输出的日志等级大于等于默认限制等级的时候才可以进行输出
提供一个接口,将对应等级的枚举,转换为一个对应的字符串,例如DEBUG -->> “DEBUG”
日志消息类主要是封装一条完整的日志消息所需的内容,其中包括日志输出时间、日志等级、日志源文件名称、源代码行号、线程ID、具体的日志信息等内容。
日志格式化(Formatter)类主要负责格式化日志消息,组织成为指定格式的字符串。其主要包含以下内容:
格式化字符串
%d 日期
%T 缩进
%t 线程id
%p 日志级别
%c 日志器名称
%f 文件名
%l 行号
%m 日志消息
%n 换行
格式化子项数组
MsgFormatItem :有效日志数据
LevelFormatItem :日志等级
NameFormatItem :日志器名称
ThreadFormatItem :线程ID
TimeFormatItem :时间戳
CFileFormatItem :文件名
CLineFormatItem :行号
TabFormatItem :制表符缩进
NLineFormatItem :换行
OtherFormatItem :非格式化的原始字符串
日志落地类主要负责将格式化完成后的日志消息字符串,输出到指定位置。
目前实现了三个不同方向上的日志落地:
日志器主要是对前面所有模块进行整合,向外提供接口完成不同等级的日志输出
使用建造者模式来建造日志器,不要让用户直接去构造日志器,简化用户操作
抽象一个日志器建造者类
设置日志器类型
将不同类型日志器的创建放到同一个日志器建造者类中完成
派生出具体的建造者类 – 局部日志器的建造者 & 全局的日志器建造者
设计思想:异步处理线程 + 数据池
使用者将需要完成的任务添加到任务池中,由异步线程来完成任务的实际执行操作。
任务池的设计思想:双缓冲区阻塞数据池
优势:避免了空间的频繁申请释放,且尽可能的减少了生产者与消费者之间锁冲突的概率,提高了任务处理效率。
在任务池的设计中,有很多备选方案,比如循环队列等等,但是不管是哪一种都会涉及到锁冲突的情况,因为在生产者与消费者模型中,任何两个角色之间都具有互斥关系,因此每一次的任务添加与取出都有可能涉及锁的冲突,而双缓冲区不同,双缓冲区是处理器将一个缓冲区中的任务全部处理完毕
后,然后交换两个缓冲区,重新对新的缓冲区中的任务进行处理,虽然同时多线程写入也会冲突,但是冲突并不会像每次只处理一条的时候频繁(减少了生产者与消费者之间的锁冲突),且不涉及到空间的频繁申请释放所带来的消耗。
单个缓冲区的设计:直接存放格式化后的日志消息字符串,这样做的优点是:
异步日志器类继承自日志器类, 并在同步日志器类上拓展了异步消息处理器。
日志的输出,我们希望能够在任意位置都可以进行,但是当我们创建了一个日志器之后,就会受到日志器所在作用域的访问属性限制。
为了突破访问区域的限制,我们创建一个日志器管理类,且这个类是一个单例类,这样的话,我们就可以在任意位置来通过管理器单例获取到指定的日志器来进行日志输出了。
单例管理器创建的时候,默认创建一个用于标准输出的打印日志器,让用户再不创建任何日志器的情况下,也能进行标准输出的打印,方便用户使用。
基于单例日志器管理器的设计思想,我们对于日志器建造者类进行继承,继承出一个全局日志器建造者类,实现一个日志器在创建完毕后,直接将其添加到单例的日志器管理器中,以便于能够在任何位置通过日志器名称能够获取到指定的日志器进行日志输出。
提供全局的日志器获取接口。
使用代理模式通过全局函数或宏函数来代理Logger类的log、debug、info、warn、error、fatal等接口,以便于控制源码文件名称和行号的输出控制,简化用户操作。
当仅需标准输出日志的时候可以通过主日志器来打印日志。 且操作时只需要通过宏函数直接进行输出即可。
下面对日志系统做一个性能测试,测试一下平均每秒能打印多少条日志消息到文件。
主要的测试方法是:每秒能打印日志数 = 打印日志条数 / 总的打印日志消耗时间
主要测试要素:同步/异步 & 单线程/多线程
测试环境:
阿里云轻量应用服务器
CPU:2核 CPU
RAM:2GB
ROM:50GB ESSD
OS:CentOS 7.6
[Janonez@linux bench]$ ./bench
// sync_logger - 同步单线程
测试日志: 1000000 条, 总大小: 97656KB
线程[0]: 输出日志数量: 1000000, 耗时: 1.8201s
总耗时: 1.8201 s
每秒输出日志数量: 549420 条
每秒输出日志大小: 53654KB
// async_logger - 异步单线程
测试日志: 1000000 条, 总大小: 97656KB
线程[0]: 输出日志数量: 1000000, 耗时: 1.67192s
总耗时: 1.67192 s
每秒输出日志数量: 598113 条
每秒输出日志大小: 58409KB
[Janonez@linux bench]$ ./bench
// sync_logger - 同步多线程
测试日志: 1000000 条, 总大小: 97656KB
线程[2]: 输出日志数量: 333333, 耗时: 1.57987s
线程[1]: 输出日志数量: 333333, 耗时: 1.69374s
线程[0]: 输出日志数量: 333333, 耗时: 1.72153s
总耗时: 1.72153 s
每秒输出日志数量: 580876 条
每秒输出日志大小: 56726KB
// async_logger - 异步多线程
测试日志: 1000000 条, 总大小: 97656KB
线程[0]: 输出日志数量: 333333, 耗时: 1.14526s
线程[2]: 输出日志数量: 333333, 耗时: 1.18942s
线程[1]: 输出日志数量: 333333, 耗时: 1.26282s
总耗时: 1.26282 s
每秒输出日志数量: 791876 条
每秒输出日志大小: 77331KB