一、前言

对于任何一个服务器而言,日志系统的设计是非常重要的,尝试设计一个简易的同步异步日志系统来完成系统日志的记录。
C++ LinuxWebServer项目(5)同步异步日志系统_阻塞队列

二、基础知识

​日志​​,由服务器自动创建,并记录运行状态,错误信息,访问数据的文件。

​同步日志​,日志写入函数与工作线程串行执行,由于涉及到I/O操作,当单条日志比较大的时候,同步模式会阻塞整个处理流程,服务器所能处理的并发能力将有所下降,尤其是在峰值的时候,写日志可能成为系统的瓶颈。

​生产者-消费者模型​,并发编程中的经典模型。以多线程为例,为了实现线程间数据同步,生产者线程与消费者线程共享一个缓冲区,其中生产者线程往缓冲区中push消息,消费者线程从缓冲区中pop消息。

任何时刻,只能有一个生产者或消费者可以访问缓冲区

​阻塞队列​,将生产者-消费者模型进行封装,使用循环数组实现队列,作为两者共享的缓冲区。

push成员是生产者,pop成员是消费者。

​异步日志​,将所写的日志内容先存入阻塞队列,写线程从阻塞队列中取出内容,写入日志。

​单例模式​,最简单也是被问到最多的设计模式之一,保证一个类只创建一个实例,同时提供全局访问的方法

三、 单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

单例模式有两种实现方法,分别是懒汉和饿汉模式。顾名思义,懒汉模式,即非常懒,不用的时候不去初始化,所以在第一次被使用时才进行初始化;饿汉模式,即迫不及待,在程序运行时立即初始化。

实现思路:私有化它的构造函数,以防止外界创建单例类的对象;使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法获取该实例.

C++ LinuxWebServer项目(5)同步异步日志系统_阻塞队列_02

1 ​​为什么要用双检测,只检测一次不行吗?​

如果只检测一次,在每次调用获取实例的方法时,都需要加锁,这将严重影响程序性能。双层检测可以有效避免这种情况,仅在第一次创建单例的时候加锁,其他时候都不再符合NULL == p的情况,直接返回已创建好的实例。

2 局部静态变量之线程安全懒汉模式

前面的双检测锁模式,写起来不太优雅,《Effective C++》(Item 04)中的提出另一种更优雅的单例模式实现,使用函数内的局部静态对象,这种方法不用加锁和解锁操作。

C++ LinuxWebServer项目(5)同步异步日志系统_日志系统_03

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 针对高并发情况下,写线程数量不足,如何处理

之前被问到的一个很好的问题,现在还没有一个很好的解决办法,后期如果有新的思路,会补一补

如果大佬们有好的建议,非常期待你们的答疑,麻烦在下方评论区中解答,非常感谢!

六、总结

同步异步日志系统,其中异步日志系统主要是解决单条日志过大造成的问题,日志系统设计模块的学习还需要不断的进行,这对于服务器开发者来说是非常重要的。

七、参考资料