muduo异步日志总结

2023-11-10

muduo中的日志是指诊断日志,即通常用于故障诊断和追踪的日志,便于服务器发生故障时的线索追踪,是网络库中很重要的一个部分。
在总结异步日志之前,首先应该清楚什么是异步日志?与同步日志又有什么区别?

同步日志与异步日志

同步日志:网络IO线程或业务线程直接向磁盘文件中写日志信息,只有等一条日志消息写完之后才能执行后续的程序。同步日志容易阻塞在磁盘IO上,效率较低且影响服务器性能,应尽量避免在服务器中多次使用磁盘IO。

异步日志:网络IO线程或业务线程产生日志消息时,用一个缓冲区储存起来,等到合适的时机,用一个后台线程统一将日志消息写入磁盘文件中。异步日志避免了在网络IO线程或业务线程中阻塞在磁盘IO中,因此也称为非阻塞日志

看了同步日志和异步日志的概念之后,使用异步日志的原因也很清晰了。异步日志避免了网络IO线程和业务线程中的磁盘IO,只在后台线程中使用了一次磁盘IO,大大提升了服务器的响应性能和日志信息的处理效率。

muduo日志库的实现思想

muduo日志库分为前端和后端两个部分,前端负责将生成的日志消息储存到缓冲区中,后端负责将缓冲区的日志消息写入到磁盘文件中。
为了实现前端、后端的异步操作,同时避免前端每次生成日志消息都唤醒后端线程,从而提高日志处理效率,muduo日志库采用的是双缓冲技术,其实现思想为:准备两块缓冲区(记为buffer A、buffer B),前端负责往buffer A中写日志消息,后端负责将buffer B中的日志消息写入磁盘文件。当buffer A写满之后,后端线程中会交换buffer A和buffer B,让前端往buffer B中写入日志消息,后端将buffer A中的日志消息写到磁盘文件中,如此往复。同时,为了及时将生成的日志消息写入文件,便于管理者分析日志消息,即使buffer A未满,日志库也会每3秒执行交换写入操作。

总结,muduo日志库的优点为:避免了前端每生成一条日志消息就传送给后端,而是将多条日志消息拼成一个大buffer传送给后端线程,相当于批量处理,减少了后端线程的唤醒频率,降低了服务器开销。

muduo日志库关键代码分析

muduo实现异步日志的类是AsyncLogging.h,声明的相关变量如下:

typedef muduo::FixedBuffer<LargeBuffer> Buffer; //大小为4MB的缓冲区
typedef std::unique_ptr<Buffer> BufferPtr;  //缓冲区指针
typedef std::vector<BufferPtr> BufferVector;

muduo::MutexLock mutex_;  //互斥锁,用于保证前端、后端的线程安全
muduo::Condition cond_;  //条件变量
BufferPtr currentBuffer_; //当前缓冲指针
BufferPtr nextBuffer_;  //预备缓冲指针
BufferVector buffers_;  //储存已填满的缓冲,并移交给后端

前端的关键实现代码如下:

void AsyncLogging::append(const char* logline, int len)//向当前缓冲区中添加日志消息,如果当前缓冲区放不下了,那么就把当前缓冲区放到前端缓冲区队列中
{
    muduo::MutexLockGuard lock(mutex_);//用锁来保持同步
    if (currentBuffer_->avail() > len)//如果当前缓冲区还能放下当前日志消息
    {
        currentBuffer_->append(logline, len);//把日志消息添加到当前缓冲区中
    } 
    else//如果放不下,就把当前缓冲区移动到前端缓冲区队列中,然后用预备缓冲区来填充当前缓冲区
    { //将当前缓冲区放到前端缓冲区队列中后就要唤醒后端处理线程
        buffers_.push_back(std::move(currentBuffer_));
        if (nextBuffer_)//如果预备缓冲区还未使用,就用来填充当前缓冲区
        {
            currentBuffer_ = std::move(nextBuffer_);
        } 
        else//如果预备缓冲区无法使用,就重新分配一个新的缓冲区(如果日志写的速度很快,但是IO速度很慢,那么前端日志缓冲区就会积       //累,但是后端还没有来得及处理,此时预备缓冲区也还没有归还,就会产生这种情况
        {
            currentBuffer_.reset(new Buffer); // 很少发生
        }
        currentBuffer_->append(logline, len);//向新的当前缓冲区中写入日志消息
        cond_.notify(); //唤醒后台线程
    }

前端实现的具体过程可描述为:网络IO线程或业务线程生成一条日志消息时,若当前缓冲currentBuffer_中剩余的空间大于日志消息的字节长度,就会把日志消息拷贝到当前缓冲中;否则,说明当前缓冲区已经写满,就把currentBuffer_移动到buffers_中,此时currentBuffer_=NULL,此后判断预备缓冲nextBuffer_是否为NULL。若currentBuffer_尚未使用,就把预备缓冲nextBuffer_移用为当前缓冲,然后继续向当前缓冲中追加日志消息并唤醒后端线程将缓冲中的日志消息写到磁盘文件中;若currentBuffer_=NULL,说明nextBuffer_已经被使用了,这时就需要重新分配一块缓冲区作为当前缓冲currentBuffer_,然后继续向当前缓冲中追加日志消息并唤醒后端线程将缓冲中的日志消息写到磁盘文件中(这种情况较少,只有在日志消息生成速度太快后端来不及写入日志消息时发生)。

其中,buffers_为双缓冲技术中的buffer A,具体实现时buffers_根据日志消息的生成速度可调节大小,实现比较灵活。

后端的关键实现代码为:

void AsyncLogging::threadFunc() 
{
  LogFile output(basename_, rollSize_, false);   //指定输出的日志文件
  BufferPtr newBuffer1(new Buffer);//用来填充移动后的currentBuffer_
  BufferPtr newBuffer2(new Buffer);//用来填充使用后的nextBuffer_
  
  newBuffer1->bzero(); //缓冲区清零
  newBuffer2->bzero(); //缓冲区清零
  BufferVector buffersToWrite;//后端缓冲区队列,初始大小为16
  buffersToWrite.reserve(16);
  while (running_)
  {
    {
      muduo::MutexLockGuard lock(mutex_);
      if (buffers_.empty())  //  如果前端缓冲区队列为空,就休眠flushInterval_的时间
      {
        cond_.waitForSeconds(flushInterval_);
      }
      buffers_.push_back(std::move(currentBuffer_));
	  currentBuffer_ = std::move(newBuffer1); //当前缓冲区获取新的内存
      buffersToWrite.swap(buffers_); //前端缓冲区队列与后端缓冲区队列交换
      
      if (!nextBuffer_) //如果预备缓冲区为空,那么就使用newBuffer2作为预备缓冲区,保证始终有一个空闲的缓冲区用于预备
      {
        nextBuffer_ = std::move(newBuffer2);
      }
    }
 
    //如果最终后端缓冲区的缓冲区太多就只保留前三个
    if (buffersToWrite.size() > 25) 
    {
      char buf[256];//buf作为缓冲区太多时的错误提示字符串
      snprintf(buf, sizeof buf, "Dropped log messages at %s, %zd larger buffers\n",
               Timestamp::now().toFormattedString().c_str(),
               buffersToWrite.size()-2);
      fputs(buf, stderr);
      output.append(buf, static_cast<int>(strlen(buf)));//将buf写出到日志文件中
      buffersToWrite.erase(buffersToWrite.begin()+2, buffersToWrite.end());//只保留后端缓冲区队列中的前三个缓冲区
    }
 
    //遍历当前后端缓冲区队列中的所有缓冲区
    for (const auto& buffer : buffersToWrite)
    {
      // FIXME: use unbuffered stdio FILE ? or use ::writev ?
      output.append(buffer->data(), buffer->length());//依次写入日志文件
    }
	//此时后端缓冲区中的日志消息已经全部写出,就可以重置缓冲区队列了
    if (buffersToWrite.size() > 2)
    {
      // drop non-bzero-ed buffers, avoid trashing
      buffersToWrite.resize(2);
    }
 
    if (!newBuffer1)//如果newBuffer1为空 (刚才用来替代当前缓冲了)
    {
      newBuffer1 = std::move(buffersToWrite.back()); //把后端缓冲区的最后一个作为newBuffer1
      buffersToWrite.pop_back(); //最后一个元素的拥有权已经转移到了newBuffer1中,因此删除最后一个元素
      newBuffer1->reset(); //重置newBuffer1为空闲状态
    }
 
    if (!newBuffer2)//如果newBuffer2为空
    {
      newBuffer2 = std::move(buffersToWrite.back());
      buffersToWrite.pop_back();
      newBuffer2->reset();
    }
 
    buffersToWrite.clear();//清空后端缓冲区队列
    output.flush();//清空文件缓冲区
  }
  output.flush();
}

后端实现的具体过程为:后端线程会准备两块临时缓冲buffer,然后执行一个循环。循环中首先判断buffers_是否为空,若为空就休眠flushInterval_秒再唤醒后端线程,休眠过程中,若前端中buffers_不为空,后端线程也会被唤醒。后端线程唤醒后,将前端的当前缓冲currentBuffer_中的数据追加到buffers_,并立即将空闲的newBuffer1移为当前缓冲。紧接着,交换buffers_与buffersToWrite的资源,并用newBuffer2替换nextBuffer_(保证前端始终有一个可调配的预备buffer)。以上步骤涉及到前端与后端的数据交互,为保证数据同步,需要加锁,使其处于临界区。

临界区的数据交互完成之后,就可以将buffersToWrite中的日志数据写入磁盘文件。写日志消息时,若buffersToWrite过大,只保留前三个缓冲区的日志数据,从而避免日志堆积。

日志数据写入完成以后,重置buffersToWrite的大小为2,并重新用于填充newBuffer1和newBuffer2,从而实现了现有资源的有效利用,避免了重新开辟内存资源,有利于提高服务器性能。

最后,清空后端缓冲队列buffersToWrite,并flush缓冲区,从而促使缓冲区的日志数据全部写入到磁盘文件中。

其中,buffersToWrite为双缓冲技术中的buffer B。

总结

muduo异步日志库的实现,有很多细节都体现了高性能服务器开发过程中如何提高服务器性能,读者可以从中多多学习。比如:
一、互斥锁所持有临界区很小,临界区内只完成前端、后端日志数据的交互,然后前端继续向前端缓冲区写日志消息,后端也立即向文件中写数据。
二、后端日志数据写完之后,会利用buffersToWrite中现有的内存空间来重新填充newBuffer1和newBuffer2,而不是重新开辟一块内存,这实际上也有利于节省内存开销,尽量减小日志消息给服务器性能造成的影响。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

muduo异步日志总结 的相关文章

  • Nodejs+Express中页面控制器及脚本自动加载设计

    Express自身带强大的路由功能 这让我们可以详细拆分项目的需求 设计出优美的restful风格对外API 为了方便实现上述功能 我加入了页面控制器及脚本自动加载设计 比如针对会员模块 我们在app js中指定了模块的路由文件 app u
  • 回调函数

    单线程的时候同步的话 很容易阻塞在那边 用户体验极差 例如 异步是可以多线程的 因为UI主线程一旦阻塞整个界面就卡死了 一旦异步 两个线程下一个可以后台处理数据 一个可以做UI显示 js是单线程的 如果所有的操作 ajax 获取文件等I O
  • libev学习系列之三:libev编译安装

    libev学习系列之三 libev编译安装 版本说明 版本 作者 日期 备注 0 1 ZY 2019 5 31 初稿 目录 文章目录 libev学习系列之三 libev编译安装 版本说明 目录 源码结构 正常编译 交叉编译 源码结构 4 2
  • 基于Linux用C语言实现TCP/UDP图片和文件传输(socket)

    目录 一 TCP实现 1 服务端 2 客户端 二 UDP实现 1 服务端 2 客户端 一 TCP实现 传输控制协议 TCP Transmission Control Protocol 是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设
  • 网络编程14——epoll反应堆模型⭐,epoll反应堆实现源码(并没掌握▼

    epoll ET模式 非阻塞 void ptr epoll反应堆模式 与原来监听模式对比 给lfd和cfd指定回调函数的区别 epoll反应堆实现源码 这代码有点难 eventset函数 设置回调函数 lfd gt acceptconn c
  • 三次握手、四次挥手的理解

    下面是个人对三次握手和四次挥手的理解 1 三次握手 第一次握手 客户端向服务器发起连接请求 此时要确认客户端能不能发 第二次握手 服务器收到客户端的连接请求后 会给一个应答 以及请求连接的数据包 意味着服务器收到了客户端连接的请求 以此证明
  • Ubuntu 下同局域网主机访问Tomcat 服务器

    转自 https blog csdn net zm yang article details 70483439 搭建Tomcat环境 自己写些小应用 需要用到服务器 便在Ubuntu环境下搭建了个Tomcat服务器 搭建方法很简单 去官网下
  • C# 序列化与反序列化几种格式的转换

    这里介绍了几种方式之间的序列化与反序列化之间的转换 首先介绍的如何序列化 将object对象序列化常见的两种方式即string和xml对象 第一种将object转换为string对象 这种比较简单没有什么可谈的 public string
  • TFTP协议下载实验

    include
  • C# 网络编程之获取本机名、ip地址、域名、物理位置

    在C 网络编程中 主机域名与ip之间能相互转换 同时DNS中有Dns类 IPHostEntry类 IPAddress类 DnsPermission类实现DNS的一些简单功能 下面主要讲述一个C 的Windows应用程序 实现以下功能 1 获
  • Linux I/O多路复用——poll模型实现服务端Socket通信

    目录 poll函数 参数说明 events相关 与select的不同 程序流程 程序实例 poll函数 poll模型在实现服务端时思路是和select类似的 可以说poll是select的加强版 poll函数原型如下 int poll st
  • 进程间通信之共享内存分析

    零拷贝技术 https strikefreedom top linux io and zero copy 一 内存映射和共享内存的区别 1 1 内存映射之mmap函数 将一个文件或者其它对象映射到进程的地址空间 实现文件磁盘地址和进程虚拟地
  • web服务搭建

    Python 吹爆Python 1行代码搭建Web服务器30行代码实现服务器的文件上传下载 需求 手机每日下载图片 然后需经过本人编写的Python脚本处理一遍 再返回到手机上 个人电脑不能保证时刻开机在线 自己也不可能一直在电脑旁边 故欲
  • 2_C/S模型编程示例1

    本文主要参考C语言中文网和linux网络编程 网络基础 socket编程 高并发服务器 1 网络程序设计模式 1 1 CS模式 客户机 client 服务器 server 模式 需要在通讯两端各自部署客户机和服务器来完成数据通信 1 本地可
  • Go_一文入门网络编程:常见协议、通信过程、Socket、CS/BS、TCP/UDP

    网络编程三要素 ip地址 端口 协议 在网络通信协议下 不同计算机上运行的程序 可以进行数据传输 IP地址 IP地址是一种在互联网协议中用于识别和定位设备的32位或128位数字地址 它是一个设备在网络上的唯一标识符 用于在互联网上定位和识别
  • 网络编程——软件架构、osi七层、TCP/UDP协议

    文章目录 一 网络编程是什么 二 软件架构 1 c s架构 2 b s架构 三 OSI七层 1 物理层 2 链路层 3 网络层 4 传输层 5 应用层 四 TCP UDP协议 1 TCP 2 UDP协议 一 网络编程是什么 一个完整计算机系
  • Node.js开发入门—HTTP文件服务器

    HelloWorld示例只有演示意义 这次我们来搞一个实际的例子 文件服务器 我们使用Node js创建一个HTTP协议的文件服务器 你可以使用浏览器或其它下载工具到文件服务器上下载文件 用Node js实现的HTTP文件服务器 比我在Qt
  • Windows7旗舰版和10 创建原始套接字失败,代码10013

    笔记本重装系统后 以前能运行的程序中的Ping程序不能运行了 查看代码 创建套接字失败 RawSock socket AF INET SOCK RAW IPPROTO ICMP RawSock INVALID SOCKET 用DWORD d
  • UNIX网络编程-recv、send、read、write之间的联系与区别

    原文链接 http www cnblogs com mhscn p 3911284 html include
  • 使用epoll时需要将socket设为非阻塞吗?

    本文是回答一位知友的提问 在APUE中介绍select和poll中说 一个描述阻塞与否并不影响select是否阻塞 也就是说 如果希望读一个非阻塞描述符 并且以超时值5s调用select 则select最多阻塞5s 我看到有些程序使用epo

随机推荐

  • caffe 查看caffemodel中的参数与数据

    在用caffe训练完一个模型之后 我们想更加直观的查看这个模型该怎么做呢 caffe框架训练出来的caffemodel是一个类似于黑盒的东西 我们无法直接看到它的本质 需要借助caffe所定义的接口来协助我们 详细的文档在caffe官网上都
  • gcc链接脚本和启动文件详解

    C代码生成可执行程序分为 预编译 编译 汇编 链接四个阶段 预处理器把源程序聚合在一起 并把宏定义转换为源语言 编译器根据预处理的源程序生成汇编程序 汇编器处理汇编程序 生成可重定位的机器代码 连接器将可重定位的目标代码和库文件连接到一起
  • 基于51单片机直流电机PID调速PWM输出LCD1602液晶显示设计

    视频演示地址 https www bilibili com video BV1LK4y1R7ju 该设计是由AT89C51单片机为主控芯片显示为1602液晶构成直流电机调速 开机默认不转按下启动后电机开始运行 PID控制PWM进行调速 按键
  • Arduino使用ESP8266模块联网

    ESP8266模块准备 1 透传程序烧写 2 Arduino与ESP8266接线 Arduino模块程序 测试 总结 上一篇文章已经介绍了 利用 ArduinoIDE开发ESP8266模块 这篇文章介绍一下arduino怎么通过ESP826
  • unity鼠标事件

    鼠标事件 鼠标事件 都是当鼠标和gui或者碰撞体 Collider 交互时候触发 需要说明的是drag其实就是鼠标down后up之前持续每帧都会发送此消息 OnMouseDown 当鼠标上的按钮被按下时触发的事件 OnMouseDrag 当
  • LLVM IR格式的基本介绍

    LLVM IR以两种格式存储在磁盘上 1 位码 bc文件 2 汇编文本 ll文件 以sum c源代码为例 int sum int a int b return a b 使用Clang生成位码 命令如下 clang sum c emit ll
  • 单片机数码管从00到99C语言_51单片机数码管实现1到99显示

    在 51 单片机上实现用数码管显示 1 到 99 的数字 并且时间间隔为 1 秒 全部代码如下 include define uchar unsigned char define uint unsigned int sbit dula P2
  • C语言之tentative definition

    参考链接 What Are Tentative Symbols
  • redisson究极爽文-手把手带你实现redisson的发布订阅,消息队列,延迟队列(死信队列),(模仿)分布式线程池

    参考资料 分布式中间件实战 java版 书籍 多线程视频教程 视频 项目启动环境 导入依赖
  • Android Watchdog分析

    初始化 Watchdog作为一个独立的线程在SystemServer进程中被初始化 private void startBootstrapServices NonNull TimingsTraceAndSlog t Start the wa
  • Ant Design Vue通过iconfont构建自定义图标库

    虽然Ant Design Vue已经为我们内置了很多icon图标 我们很方便就能使用 但有时候有些图表我们还是系统能实现定义 本文主要介绍根据iconfont图标库创建自定义的icon图表 并在Ant Design Vue中使用 首先在ic
  • 3、 数组和字符串的应用 编程实现以下功能:将一个3行5列的二维数组a行和列元素互换,存到另一个二维数组b中。

    3 数组和字符串的应用 编程实现以下功能 将一个3行5列的二维数组a行和列元素互换 存到另一个二维数组b中 include
  • 在 Linux 中使用日志来排错

    人们创建日志的主要原因是排错 通常你会诊断为什么问题发生在你的 Linux 系统或应用程序中 错误信息或一系列的事件可以给你提供找出根本原因的线索 说明问题是如何发生的 并指出如何解决它 这里有几个使用日志来解决的样例 登录失败原因 如果你
  • Hibernate对原生sql处理及结果集报错:Expected type: java.lang.Integer, actual value: java.math.BigInteger

    基于Hibernate执行sql查询方法 映射实体对象报错Expected type java lang Integer actual value java math BigInteger 实体类ChannelTree package co
  • 面试题-6

    1 查找根目录下所有的隐藏目录 root chengyinwu find type d name 2 查找根目录下以rpm结尾的所有文件 root yinwucheng find type f name rpm 3 查找 data bak目
  • CTFshow单身杯 部分wp

    前言 不会吧不会吧不会有人520521不约会打比赛吧 文章目录 1 单身杯热身题目 2 misc签到 3 没大没小的串串 4 任性老板 5 蛤壳雪茄 1 6 蛤壳雪茄 2 7 The Dancing Men 8 伪装成RSA的MUSC 9
  • notion搭建博客方法一:notion简单操作说明

    简介 notion搭建博客的方法有很多种 今天介绍最基础的 原汁原味的方法 步骤 注册一个notion账号 使用邮箱就可以很快注册 免费账号除了附件大小 共享用户数等有部分限制其他基本都不影响使用 新建一个Page 输入 然后table I
  • Java课题笔记~ IoC 控制反转

    二 IoC 控制反转 控制反转 IoC Inversion of Control 是一个概念 是一种思想 指将传统上由程序代码直接操控的对象调用权交给容器 通过容器来实现对象的 装配和管理 控制反转就是对对象控制权的转移 从程序代码本身反转
  • springboot 连接redis

    安装文章https blog csdn net yeluo vinager article details 103680059 问题 F soft Redis x64 3 2 100 gt redis server exe service
  • muduo异步日志总结

    muduo中的日志是指诊断日志 即通常用于故障诊断和追踪的日志 便于服务器发生故障时的线索追踪 是网络库中很重要的一个部分 在总结异步日志之前 首先应该清楚什么是异步日志 与同步日志又有什么区别 同步日志与异步日志 同步日志 网络IO线程或