一、前言
对于任何一个服务器而言,日志系统的设计是非常重要的,尝试设计一个简易的同步异步日志系统来完成系统日志的记录。
二、基础知识
日志
,由服务器自动创建,并记录运行状态,错误信息,访问数据的文件。
同步日志
,日志写入函数与工作线程串行执行,由于涉及到I/O操作,当单条日志比较大的时候,同步模式会阻塞整个处理流程,服务器所能处理的并发能力将有所下降,尤其是在峰值的时候,写日志可能成为系统的瓶颈。
生产者-消费者模型
,并发编程中的经典模型。以多线程为例,为了实现线程间数据同步,生产者线程与消费者线程共享一个缓冲区,其中生产者线程往缓冲区中push消息,消费者线程从缓冲区中pop消息。
任何时刻,只能有一个生产者或消费者可以访问缓冲区
阻塞队列
,将生产者-消费者模型进行封装,使用循环数组实现队列,作为两者共享的缓冲区。
push成员是生产者,pop成员是消费者。
异步日志
,将所写的日志内容先存入阻塞队列,写线程从阻塞队列中取出内容,写入日志。
单例模式
,最简单也是被问到最多的设计模式之一,保证一个类只创建一个实例,同时提供全局访问的方法
三、 单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
单例模式有两种实现方法,分别是懒汉和饿汉模式。顾名思义,懒汉模式,即非常懒,不用的时候不去初始化,所以在第一次被使用时才进行初始化;饿汉模式,即迫不及待,在程序运行时立即初始化。
实现思路:私有化它的构造函数,以防止外界创建单例类的对象;使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法获取该实例.
1 为什么要用双检测,只检测一次不行吗?
如果只检测一次,在每次调用获取实例的方法时,都需要加锁,这将严重影响程序性能。双层检测可以有效避免这种情况,仅在第一次创建单例的时候加锁,其他时候都不再符合NULL == p的情况,直接返回已创建好的实例。
2 局部静态变量之线程安全懒汉模式
前面的双检测锁模式,写起来不太优雅,《Effective C++》(Item 04)中的提出另一种更优雅的单例模式实现,使用函数内的局部静态对象,这种方法不用加锁和解锁操作。
3 为什么要把调用线程放入条件变量的请求队列后再解锁?
线程是并发执行的,如果在把调用线程A放在等待队列之前,就释放了互斥锁,这就意味着其他线程比如线程B可以获得互斥锁去访问公有资源,这时候线程A所等待的条件改变了,但是它没有被放在等待队列上,导致A忽略了等待条件被满足的信号。
倘若在线程A调用pthread_cond_wait开始,到把A放在等待队列的过程中,都持有互斥锁,其他线程无法得到互斥锁,就不能改变公有资源。
四、日志系统的运行机制
步骤:
1:单例模式(局部静态变量懒汉方法)获取实例
2:主程序一开始Log::get_instance()->init()初始化实例。初始化后:服务器启动按当前时刻创建日志(前缀为时间,后缀为自定义log文件名,并记录创建日志的时间day和行数count)。如果是异步(通过是否设置队列大小判断是否异步,0为同步),工作线程将要写的内容放进阻塞队列,还创建了写线程用于在阻塞队列里取出一个内容(指针),写入日志。
3:其他功能模块调用write_log()函数写日志。(write_log:实现日志分级、分文件、按天分类,超行分类的格式化输出内容。)里面会根据异步、同步实现不同的写方式。
- 日志文件
- 局部变量的懒汉模式获取实例
- 生成日志文件,并判断同步和异步写入方式
- 同步
- 判断是否分文件
- 直接格式化输出内容,将信息写入日志文件
- 异步
- 判断是否分文件(通过队列的大小来决定)
- 格式化输出内容,将内容写入阻塞队列,创建一个写线程,从阻塞队列取出内容写入日志文件
同步和异步日志的处理代码
// 若异步,则将日志信息加入阻塞队列,同步则加锁向文件中写
if (m_is_async && !m_log_queue->full())
{
m_log_queue->push(log_str);
}
else
{
m_mutex.lock();
fputs(log_str.c_str(), m_fp);
m_mutex.unlock();
}
五、重点知识
1 同步异步日志是怎么实现的?(CVTE)
在C++编写服务器的时候,涉及到Io操作的时候,会阻塞整个线程,同步日志可能比较简单,但是异步日志的话就需要注意一下,我们将所写的内容存入阻塞队列,创建写线程从阻塞队列中读取出内容,写入日志。
将消费者和生产者模式封装成阻塞队列。
2 日志的分级和分文件
- Debug,调试代码时的输出,在系统实际运行时,一般不使用。
- Warn,这种警告与调试时终端的warning类似,同样是调试代码时使用。
- Info,报告系统当前的状态,当前执行的流程或接收的信息等。
- Erro,输出系统的错误信息
超行、按天分文件逻辑,具体的,
- 日志写入前会判断当前day是否为创建日志的时间,行数是否超过最大行限制
- 若为创建日志时间,写入日志,否则按当前时间创建新log,更新创建时间和行数
- 若行数超过最大行限制,在当前日志的末尾加count/max_lines为后缀创建新log
3 针对高并发情况下,写线程数量不足,如何处理
之前被问到的一个很好的问题,现在还没有一个很好的解决办法,后期如果有新的思路,会补一补
如果大佬们有好的建议,非常期待你们的答疑,麻烦在下方评论区中解答,非常感谢!
六、总结
同步异步日志系统,其中异步日志系统主要是解决单条日志过大造成的问题,日志系统设计模块的学习还需要不断的进行,这对于服务器开发者来说是非常重要的。
七、参考资料
- 《两猿社》
- 《Linux高性能服务器》
- 最新版Web服务器项目详解 - 09 日志系统(上)
- 最新版Web服务器项目详解 - 10 日志系统(下)
- muduo第五章:高效的多线程日志