基于多设计模式下的同步异步日志系统

news2024/11/27 0:22:19

基于多设计模式下的同步&异步日志系统

代码链接:https://github.com/Janonez/Log_System

1. 项目介绍

本项目主要实现一个日志系统, 其主要支持以下功能:

  • 支持多级别日志消息
  • 支持同步日志和异步日志
  • 支持可靠写入日志到标准输出、文件以及滚动文件中
  • 支持多线程程序并发写日志
  • 支持扩展不同的日志落地目标地

2. 开发环境

  • CentOS 7
  • VSCode/vim
  • g++/gdb
  • Makefile

3. 核心技术

  • 类层次设计(继承和多态的应用)
  • C++11(多线程、auto、智能指针、右值引用等)
  • 双缓冲区
  • 生产消费模型
  • 多线程
  • 设计模式(单例、工厂、代理、模板等)

4. 日志系统介绍

4.1 为什么需要日志系统

  1. 生产环境的产品为了保证其稳定性及安全性是不允许开发人员附加调试器去排查问题, 可以借助日志系统来打印一些日志帮助开发人员解决问题,上线客户端的产品出现bug无法复现并解决, 可以借助日志系统打印日志并上传到服务端帮助开发人员进行分析
  2. 对于一些高频操作(如定时器、心跳包)在少量调试次数下可能无法触发我们想要的行为,通过断点的暂停方式,我们不得不重复操作几十次、上百次甚至更多,导致排查问题效率是非常低下, 可以借助打印日志的方式查问题。
  3. 在分布式、多线程/多进程代码中, 出现bug比较难以定位, 可以借助日志系统打印log帮助定位bug
  4. 帮助首次接触项目代码的新开发人员理解代码的运行流程

4.2 日志系统技术实现

日志系统的技术实现主要包括三种类型:

  1. 利用printf、std::cout等输出函数将日志信息打印到控制台
  2. 对于大型商业化项目, 为了方便排查问题,我们一般会将日志输出到文件或者是数据库系统方便
  3. 查询和分析日志, 主要分为同步日志和异步日志方式

4.2.1 同步写日志

同步日志是指当输出日志时,必须等待日志输出语句执行完毕后,才能执行后面的业务逻辑语句,日志输出语句与程序的业务逻辑语句将在同一个线程运行。每次调用一次打印日志API就对应一次系统调用write写日志文件。

在高并发场景下,随着日志数量不断增加,同步日志系统容易产生系统瓶颈:

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

4.2.2 异步写日志

异步日志是指在进行日志输出时,日志输出语句与业务逻辑语句并不是在同一个线程中运行,而是有专门的线程用于进行日志输出操作。业务线程只需要将日志放到一个内存缓冲区中不用等待即可继续执行后续业务逻辑(作为日志的生产者),而日志的落地操作交给单独的日志线程去完成(作为日志
的消费者), 这是一个典型的生产-消费模型。

这样做的好处是即使日志没有真的地完成输出也不会影响程序的主业务,可以提高程序的性能:

  • 主线程调用日志打印接口成为非阻塞操作
  • 同步的磁盘IO从主线程中剥离出来交给单独的线程完成

5. 日志系统框架设计

将一条消息,进行格式化成为指定格式的字符串后,写入到指定位置

本项目实现的是一个多日志器日志系统,主要实现的功能是让程序员能够轻松的将程序运行日志信息落地到指定的位置,且支持同步与异步两种方式的日志落地方式。

5.1 模块划分

  • 日志等级模块:对输出日志的等级进行划分,以便于控制日志的输出,并提供等级枚举转字符串功能。
  • 日志消息模块:中间存储日志输出所需的各项要素信息
  • 日志消息格式化模块:设置日志输出格式,并提供对日志消息进行格式化功能。
  • 日志消息落地模块:决定了日志的落地方向,可以是标准输出,也可以是日志文件,也可以滚动文件输出…
  • 日志器模块:此模块是对以上几个模块的整合模块,用户通过日志器进行日志的输出,有效降低用户的使用难度。包含有:日志消息落地模块对象,日志消息格式化模块对象,日志输出等级
  • 日志器管理模块:创建的所有日志器进行统一管理。并提供一个默认日志器提供标准输出的日志输出。
  • 异步线程模块:实现对日志的异步输出功能,用户只需要将输出日志任务放入任务池,异步线程负责日志的落地输出功能,以此提供更加高效的非阻塞日志输出。

5.2 模块关系图

6. 代码设计

6.1 实用类设计

提前完成一些零碎的功能接口,以便于项目中会用到。

  • 获取系统时间
  • 判断文件是否存在
  • 获取文件的所在目录路径
  • 创建目录

6.2 日志等级类设计

定义出日志系统所包含的所有日志等级,分别为:

  1. UNKNOW:未知等级日志
  2. DEBUG:调试等级的日志
  3. INFO:提示等级的日志
  4. WARN:警告等级的日志
  5. ERROR:错误等级的日志
  6. FATAL:致命错误等级的日志
  7. OFF:关闭所有日志输出

每个项目都会设置一个默认的日志输出等级,只有输出的日志等级大于等于默认限制等级的时候才可以进行输出

提供一个接口,将对应等级的枚举,转换为一个对应的字符串,例如DEBUG -->> “DEBUG”

6.3 日志消息类设计

日志消息类主要是封装一条完整的日志消息所需的内容,其中包括日志输出时间、日志等级、日志源文件名称、源代码行号、线程ID、具体的日志信息等内容。

6.4 日志输出格式化类设计

日志格式化(Formatter)类主要负责格式化日志消息,组织成为指定格式的字符串。其主要包含以下内容:

  1. 格式化字符串
    %d 日期
    %T 缩进
    %t 线程id
    %p 日志级别
    %c 日志器名称
    %f 文件名
    %l 行号
    %m 日志消息
    %n 换行

  2. 格式化子项数组

    MsgFormatItem :有效日志数据
    LevelFormatItem :日志等级
    NameFormatItem :日志器名称
    ThreadFormatItem :线程ID
    TimeFormatItem :时间戳
    CFileFormatItem :文件名
    CLineFormatItem :行号
    TabFormatItem :制表符缩进
    NLineFormatItem :换行
    OtherFormatItem :非格式化的原始字符串

6.5 日志落地(LogSink)类设计(简单工厂模式)

日志落地类主要负责将格式化完成后的日志消息字符串,输出到指定位置。

目前实现了三个不同方向上的日志落地:

  • 标准输出:StdoutSink
  • 固定文件:FileSink
  • 滚动文件:RollSink

6.6 日志器类(Logger)设计(建造者模式)

日志器主要是对前面所有模块进行整合,向外提供接口完成不同等级的日志输出

  1. 管理的成员:
    格式化模块对象(不同输出等级的日志)
    落地模块对象数组(一个日志器可能会向多个位置进行日志输出)
    默认的日志输出限制等级(大于等于限制等级的日志才能输出)
    互斥锁(保证日志输出是线程安全的,不会出现交叉日志)
    日志器名称(日志器的唯一表示,以便于查找)
  2. 实现:
    抽象Logger基类(派生出同步日志器类 & 异步日志器类),两个不同的日志器在日志的落地方式上有所不同:
    同步日志器:直接对日志消息进行输出
    异步日志器:将日志消息放入缓冲区,由异步线程进行输出

使用建造者模式来建造日志器,不要让用户直接去构造日志器,简化用户操作

  1. 抽象一个日志器建造者类

    设置日志器类型

    将不同类型日志器的创建放到同一个日志器建造者类中完成

  2. 派生出具体的建造者类 – 局部日志器的建造者 & 全局的日志器建造者

6.7 双缓冲区异步任务处理器(AsyncLooper)设计

设计思想:异步处理线程 + 数据池
使用者将需要完成的任务添加到任务池中,由异步线程来完成任务的实际执行操作。
任务池的设计思想:双缓冲区阻塞数据池
优势:避免了空间的频繁申请释放,且尽可能的减少了生产者与消费者之间锁冲突的概率,提高了任务处理效率。
在任务池的设计中,有很多备选方案,比如循环队列等等,但是不管是哪一种都会涉及到锁冲突的情况,因为在生产者与消费者模型中,任何两个角色之间都具有互斥关系,因此每一次的任务添加与取出都有可能涉及锁的冲突,而双缓冲区不同,双缓冲区是处理器将一个缓冲区中的任务全部处理完毕
后,然后交换两个缓冲区,重新对新的缓冲区中的任务进行处理,虽然同时多线程写入也会冲突,但是冲突并不会像每次只处理一条的时候频繁(减少了生产者与消费者之间的锁冲突),且不涉及到空间的频繁申请释放所带来的消耗。

单个缓冲区的设计:直接存放格式化后的日志消息字符串,这样做的优点是:

  1. 减少了LogMsg对象频繁的构造的消耗
  2. 可以针对缓冲区中的日志消息,一次性进行IO操作,减少IO次数,提高效率

6.8 异步日志器(AsyncLogger)设计

异步日志器类继承自日志器类, 并在同步日志器类上拓展了异步消息处理器。

  1. 异步工作使用双缓冲区
    外界将任务数据添加到输入缓冲区中。异步线程对处理缓冲区中的数据进行处理,如果处理缓冲区中没有数据就交换缓冲区
  2. 回调函数:针对缓冲区中数据的处理接口,外界传入一个函数,告诉异步工作器如何处理

6.9 单例日志器管理类设计(单例模式)

日志的输出,我们希望能够在任意位置都可以进行,但是当我们创建了一个日志器之后,就会受到日志器所在作用域的访问属性限制。

为了突破访问区域的限制,我们创建一个日志器管理类,且这个类是一个单例类,这样的话,我们就可以在任意位置来通过管理器单例获取到指定的日志器来进行日志输出了。

单例管理器创建的时候,默认创建一个用于标准输出的打印日志器,让用户再不创建任何日志器的情况下,也能进行标准输出的打印,方便用户使用。

基于单例日志器管理器的设计思想,我们对于日志器建造者类进行继承,继承出一个全局日志器建造者类,实现一个日志器在创建完毕后,直接将其添加到单例的日志器管理器中,以便于能够在任何位置通过日志器名称能够获取到指定的日志器进行日志输出。

6.10 日志宏&全局接口设计(代理模式)

提供全局的日志器获取接口。
使用代理模式通过全局函数或宏函数来代理Logger类的log、debug、info、warn、error、fatal等接口,以便于控制源码文件名称和行号的输出控制,简化用户操作。
当仅需标准输出日志的时候可以通过主日志器来打印日志。 且操作时只需要通过宏函数直接进行输出即可。

7. 性能测试

下面对日志系统做一个性能测试,测试一下平均每秒能打印多少条日志消息到文件。
主要的测试方法是:每秒能打印日志数 = 打印日志条数 / 总的打印日志消耗时间
主要测试要素:同步/异步 & 单线程/多线程

  • 100w+条指定长度的日志输出所耗时间
  • 每秒可以输出多少条日志
  • 每秒可以输出多少MB日志

测试环境:

阿里云轻量应用服务器

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/969250.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

uni-app之android原生插件开发

一 插件简介 1.1 当HBuilderX中提供的能力无法满足App功能需求,需要通过使用Andorid/iOS原生开发实现时,可使用App离线SDK开发原生插件来扩展原生能力。 1.2 插件类型有两种,Module模式和Component模式 Module模式:能力扩展&…

S32K324芯片学习笔记

文章目录 Core and architectureDMASystem and power managementMemory and memory interfacesClocksSecurity and integrity安全与完整性Safety ISO26262Analog、Timers功能框图内存mapflash Signal MultiplexingPort和MSCR寄存器的mapping Core and architecture 两个Arm Co…

数学建模:Yalmip求解线性与非线性优化问题

🔆 文章首发于我的个人博客:欢迎大佬们来逛逛 线性优化 使用 Yalmip 求解线性规划最优值: m i n { − x 1 − 2 x 2 3 x 3 } x 1 x 2 ⩾ 3 x 2 x 3 ⩾ 3 x 1 x 3 4 0 ≤ x 1 , x 2 , x 3 ≤ 2 \begin{gathered}min\{-x_1-2x_23x_3\} \…

networkX-01-基础

文章目录 创建一个图1. 节点方式1 :一次添加一个节点方式2:从list中添加节点方式3:添加节点时附加节点属性字典方式4:将一个图中的节点合并到另外一个图中 2. 边方式1:一次添加一条边方式2:列表&#xff08…

23062网络编程day2

1. TCP的服务器 客户端的代码 服务器 #include <myhead.h>#define ERR_MSG(msg) do{\fprintf(stderr,"__%d__:",__LINE__);\perror(msg);\ }while(0)#define PORT 8888#define IP "192.168.114.104"int main(int argc, const char *argv[]) {//创建…

大数据技术原理与应用学习笔记第1章

黄金组合访问地址&#xff1a;http://dblab.xmu.edu.cn/post/7553/ 1.《大数据技术原理与应用》教材 官网&#xff1a;http://dblab.xmu.edu.cn/post/bigdata/ 2.大数据软件安装和编程实践指南 官网林子雨编著《大数据技术原理与应用》教材配套大数据软件安装和编程实践指…

Windows 操作系统下 Python 及其模块的管理

Python 是一款解释型语言&#xff0c;理论上一个.py文件可以当成一个稍微复杂一些的字符串指令集本文不涉及jupyter,VS,VScode,Pycharm 等集成开发环境&#xff0c;这不是我们这篇文章所关心的东西 这篇文章面向的是Python 的初学者 最近没有写太多长文章&#xff0c;多写几篇&…

8、暴力递归

前缀树 一个字符串类型的数组arr1,另一个字符串类型的数组arr2。arr2中有哪些字符,是arr1中出现的?请打印。arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印。arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印 arr2中出现次数最大的前缀 public…

LabVIEW开发超导体电流特性的测量系统

LabVIEW开发超导体电流特性的测量系统 超导体的临界电流密度Jc不断增加&#xff0c;目前超导线已达到150MA/cm2因此&#xff0c;由于电流能力增强&#xff0c;超导体被认为应用于电力系统&#xff0c;例如传输电缆、超导磁体和超导磁储能。由于Jc是此类应用的重要值&#xff0…

STM32F4X RNG随机数发生器

STM32F4X RNG随机数发生器 随机数的作用STM32F4X 随机数发生器RNG控制寄存器RNG状态寄存器RNG数据寄存器RNG数据步骤RNG例程 随机数的作用 随机数顾名思义就是随机产生的数字&#xff0c;这种数字最大的特点就是其不确定性&#xff0c;你不知道它下一次产生的数字是什么。随机…

差分数组/前缀和

文章目录 1094. 拼车1109. 航班预定统计303. 区域和检索 - 数组不可变560. 和为K的子数组523. 连续的子数组的和 1094. 拼车 class Solution {public boolean carPooling(int[][] trips, int capacity) {int[] diff new int[1001]; // 记录每个站点改变的人数&#xff0c;比如…

c语言---指针

指针 前言 记录一个数据对象在内存中的存储位置&#xff0c;需要两个信息&#xff1a; 1、数据对象的首地址。 2、数据对象占用存储空间大小 基础数据类型所占内存空间大小&#xff08;字节&#xff09;&#xff0c;一个字节代表8个二进制位 char 1 short 2 int 4 lon…

Java中的网络编程------基于Socket的TCP编程和基于UDP的网络编程,netstat指令

Socket 在Java中&#xff0c;Socket是一种用于网络通信的编程接口&#xff0c;它允许不同计算机之间的程序进行数据交换和通信。Socket使得网络应用程序能够通过TCP或UDP协议在不同主机之间建立连接、发送数据和接收数据。以下是Socket的基本介绍&#xff1a; Socket类型&…

机器学习:可解释学习

文章目录 可解释学习为什么需要可解释机器学习可解释还是强模型可解释学习的目标可解释机器学习Local ExplanationGlobal Explanation 可解释学习 神马汉斯&#xff0c;只有在有人看的时候能够答对。 为什么需要可解释机器学习 贷款&#xff0c;医疗需要给出理由&#xff0c;让…

学生宿舍水电费自动缴费系统/基于javaweb的水电缴费系统

摘 要 “互联网”的战略实施后&#xff0c;很多行业的信息化水平都有了很大的提升。但是目前很多学校日常工作仍是通过人工管理的方式进行&#xff0c;需要在各个岗位投入大量的人力进行很多重复性工作&#xff0c;这样就浪费了许多的人力物力&#xff0c;工作效率较低&#x…

职场中的道德与伦理:如何在工作中坚守原则?

引言 在快节奏的职场环境中&#xff0c;道德与伦理问题时常出现&#xff0c;但却往往被忽视。面对各种压力和诱惑&#xff0c;如何在工作中坚守原则&#xff0c;不仅是个人修养的体现&#xff0c;也是职业成功的关键。本文将探讨职场中的道德与伦理问题&#xff0c;以及如何在…

Orangepi安装外设库 wiringPi

注意&#xff1a;mobaXterm传送文件要在SSH登陆环境下才可以。 同时电脑和orangepi都在同一个wifi下。

docker 笔记6:高级篇 DockerFile解析

目录 1.是什么&#xff1f; 2.构建三步骤 3.DockerFile构建过程解析 3.1 Dockerfile内容基础知识 3.2Docker执行Dockerfile的大致流程 总结 4.DockerFile常用保留字指令 5.案例&#xff1a;自定义镜像 5.1 要求&#xff1a; Centos7镜像具备vimifconfigjdk8 5.2编写 5…

deque容器

1 deque容器基本概念 功能&#xff1a; 双端数组&#xff0c;可以对头端进行插入删除操作 deque与vector区别&#xff1a; vector对于头部的插入删除效率低&#xff0c;数据量越大&#xff0c;效率越低deque相对而言&#xff0c;对头部的插入删除速度回比vector快vector访问…

如何在java中做基准测试

最近公司在搞新项目&#xff0c;由于是实验性质&#xff0c;且不会直接面对客户的项目&#xff0c;这次的技术选型非常激进&#xff0c;如&#xff0c;直接使用了Java 17。 作为公司里练习两年半的个人练习生&#xff0c;我自然也是深度的参与到了技术选型的工作中。不知道大家…