文章目录

  • 一、日志系统简介
  • 二、功能需求
  • 三、性能需求
  • 四、高效的异步日志
    • 1、异步日志的概念
    • 2、双缓冲异步日志解析
    • 3、AsyncLogging源码
    • 4、代码运行图示
  • 五、双缓冲异步日志的相关问题

一、日志系统简介

  日志通常用于故障诊断和追踪(trace),也可用于性能分析。日志通常是分布式系统中事故调查时的唯一线索, 用来追寻蛛丝马迹, 查出原凶。

【日志需要记录的内容】:

  • 收到每条内部消息的ID(还可以包括关键字段、长度、hash等);

  • 收到的每条外部消息的全文;

  • 发出每条消息的全文, 每条消息都有全局唯一的id;

  • 关键内部状态的变更, 等等。

【一个日志文件可分为前端(frontend)和后端(backend)两部分】:

  • 前端提供应用程序使用的接口(API), 并生成日志消息(log message);

  • 后端则负责把日志消息写到目的地(destination)

  • 典型的多生产者-单消费者问题, 对生产者(前端)而言, 要尽量做到低延迟、低CPU开销、无阻塞;

  • 对消费者(后端)而言, 要做到足够大的吞吐量, 并占用较少资源;


二、功能需求

【日志消息格式有几个要点】:

  • 尽量每条日志占一行,这样很容易用awk、sed、grep等命令行工具快速联机分析日志;

  • 时间戳精确到微妙;

  • 始终使用GMT时区(Z);

  • 打印线程id,便于分析多线程程序的时序,也可以检测死锁;

  • 打印日志级别;

  • 打印源文件名和行号。


三、性能需求

【日志库的高效性体现在几个方面】:

  • 每秒写上千万条日志的时候没有明显的性能损失;

  • 能应对一个进程生产大量日志数据的场景, 例如1GB/min;

  • 不阻塞正常的执行流程;

  • 在多程序程序中, 不造成争用(contention)

  • 磁盘带宽约是110MB/S, 日志库应该能瞬时写满这个带宽(不必持续太久);

  • 假如每条日志消息的平均长度是110字节, 这就意味着1秒要写100万条日志。

【muduo日志库实现了几点优化措施】:

  • 时间戳字符串中的日期和时间部分是缓存的, 一秒内的多条日志只需要重新格式化微妙部分;

  • 日志消息的前4个字段是定长的, 因此可以避免在运行期求字符串长度(不会反复调用strlen),因为编译器认识memcpy()函数, 对于定长的内存复制, 会在编译期把它的inline展开为高效的目标代码;

  • 线程id是预先格式化为字符串, 在输出日志消息时只需要简单拷贝几个字节;

  • 每行日志消息的源文件名部分采用了编译期计算来获得basename, 避免运行期strrchr()开销。


四、高效的异步日志

1、异步日志的概念

  多线程程序对日志库提出了新的需求:线程安全, 即多个程序可以并发写日志, 两个线程的日志消息不会出现交织。

  用一个背景线程收集日志消息, 并写入日志文件, 其他业务线程只管往这个日志线程发送日志消息, 这称为异步日志(非阻塞日志)。

2、双缓冲异步日志解析

  muduo日志库是用双缓冲技术。基本思路是准备两块buffer:A和B, 前端负责往buffer A填数据(日志消息), 后端负责将buffer B的数据写入文件当buffer A写满之后, 交换A和B, 让后端将buffer A的数据写入文件, 而前端则往buffer B填入新的日志消息, 如此往复。

  使用两个buffer的好处是在新建日志消息的时候不必等待磁盘文件操作,也避免每条新日志消息都触发后端日志线程。换句话说,前端不是将一条条日志消息分别送给后端,而是将多条日志消息拼接成一个大的buffer传送给后端,相当于批处理,减少了线程唤醒的开销。

3、AsyncLogging源码

#ifndef MUDUO_BASE_ASYNCLOGGING_H
#define MUDUO_BASE_ASYNCLOGGING_H#include <muduo/base/BlockingQueue.h>
#include <muduo/base/BoundedBlockingQueue.h>
#include <muduo/base/CountDownLatch.h>
#include <muduo/base/Mutex.h>
#include <muduo/base/Thread.h>
#include <muduo/base/LogStream.h>#include <atomic>
#include <vector>namespace muduo
{class AsyncLogging : noncopyable
{public:AsyncLogging(const string& basename,off_t rollSize,int flushInterval = 3);~AsyncLogging(){if (running_){stop();}}void append(const char* logline, int len);void start(){// 在构造函数中latch_的值为1// 线程运行之后将latch_的减为0running_ = true;thread_.start();// 必须等到latch_变为0才能从start函数中返回,这表明初始化已经完成latch_.wait();}void stop() NO_THREAD_SAFETY_ANALYSIS{running_ = false;cond_.notify();thread_.join();}private:void threadFunc();typedef muduo::detail::FixedBuffer<muduo::detail::kLargeBuffer> Buffer;// 用unique_ptr管理buffer,持有对对象的独有权,不能进行复制操作只能进行移动操作(效率更高)typedef std::vector<std::unique_ptr<Buffer>> BufferVector; typedef BufferVector::value_type BufferPtr; // 指向buffer的指针const int flushInterval_; // 定期(flushInterval_秒)将缓冲区的数据写到文件中std::atomic<bool> running_; // 是否正在运行const string basename_; // 日志名字const off_t rollSize_; // 预留的日志大小muduo::Thread thread_; // 执行该异步日志记录器的线程muduo::CountDownLatch latch_; // 倒计时计数器初始化为1,用于指示什么时候日志记录器才能开始正常工作muduo::MutexLock mutex_;muduo::Condition cond_ GUARDED_BY(mutex_);BufferPtr currentBuffer_ GUARDED_BY(mutex_); // 当前的缓冲区BufferPtr nextBuffer_ GUARDED_BY(mutex_); // 下一个缓冲区BufferVector buffers_ GUARDED_BY(mutex_); // 缓冲区队列
};}  // namespace muduo#endif  // MUDUO_BASE_ASYNCLOGGING_H
#include <muduo/base/AsyncLogging.h>
#include <muduo/base/LogFile.h>
#include <muduo/base/Timestamp.h>#include <stdio.h>using namespace muduo;AsyncLogging::AsyncLogging(const string& basename,off_t rollSize,int flushInterval): flushInterval_(flushInterval),running_(false),basename_(basename),rollSize_(rollSize),thread_(std::bind(&AsyncLogging::threadFunc, this), "Logging"), // thread绑定threadFunc回调函数latch_(1),mutex_(),cond_(mutex_),currentBuffer_(new Buffer),nextBuffer_(new Buffer),buffers_()
{currentBuffer_->bzero(); // 缓冲区清零nextBuffer_->bzero();buffers_.reserve(16); // vector预定大小,避免自动增长(效率更高)
}/********************************************************************
Description :
前端在生成一条日志消息时,会调用AsyncLogging::append()。
如果currentBuffer_够用,就把日志内容写入到currentBuffer_中,
如果不够用(就认为其满了),就把currentBuffer_放到已满buffer数组中,
等待消费者线程(即后台线程)来取。则将预备好的另一块缓冲
(nextBuffer_)移用为当前缓冲区(currentBuffer_)。
*********************************************************************/
void AsyncLogging::append(const char* logline, int len)
{muduo::MutexLockGuard lock(mutex_);// 如果当前buffer的长度大于要添加的日志记录的长度,即当前buffer还有空间,就添加到当前日志。if (currentBuffer_->avail() > len){currentBuffer_->append(logline, len);}// 当前buffer已满。else {// 把当前buffer添加到buffer数组中。buffers_.push_back(std::move(currentBuffer_));// 如果另一块缓冲区不为空,则将预备好的另一块缓冲区移用为当前缓冲区。if (nextBuffer_){currentBuffer_ = std::move(nextBuffer_);}// 如果前端写入速度太快了,一下子把两块缓冲都用完了,那么只好分配一块新的buffer,作当前缓冲区。else{currentBuffer_.reset(new Buffer);}// 添加日志记录。currentBuffer_->append(logline, len);// 通知后端开始写入日志数据。cond_.notify();}
}/********************************************************************
Description :
如果buffers_为空,使用条件变量等待条件满足(即前端线程把一个已经满了
的buffer放到了buffers_中或者超时)。将当前缓冲区放到buffers_数组中。
更新当前缓冲区(currentBuffer_)和另一个缓冲区(nextBuffer_)。
将bufferToWrite和buffers_进行swap。这就完成了将写了日志记录的buffer
从前端线程到后端线程的转变。
*********************************************************************/
void AsyncLogging::threadFunc()
{assert(running_ == true);latch_.countDown();LogFile output(basename_, rollSize_, false);BufferPtr newBuffer1(new Buffer);BufferPtr newBuffer2(new Buffer);newBuffer1->bzero();newBuffer2->bzero();BufferVector buffersToWrite; // 写入日志记录文件的BufferVector。buffersToWrite.reserve(16);while (running_){assert(newBuffer1 && newBuffer1->length() == 0);assert(newBuffer2 && newBuffer2->length() == 0);assert(buffersToWrite.empty());{muduo::MutexLockGuard lock(mutex_);// 如果buffers_为空,那么表示没有数据需要写入文件,那么就等待指定的时间。if (buffers_.empty()){cond_.waitForSeconds(flushInterval_);}// 无论cond是因何(一是超时,二是当前缓冲区写满了)而醒来,都要将currentBuffer_放到buffers_中。  // 如果是因为时间到(3秒)而醒,那么currentBuffer_还没满,此时也要将之写入LogFile中。  // 如果已经有一个前端buffer满了,那么在前端线程中就已经把一个前端buffer放到buffers_中  // 了。此时,还是需要把currentBuffer_放到buffers_中(注意,前后放置是不同的buffer,  // 因为在前端线程中,currentBuffer_已经被换成nextBuffer_指向的buffer了)。buffers_.push_back(std::move(currentBuffer_));// 将新的buffer(newBuffer1)移用为当前缓冲区(currentBuffer_)。currentBuffer_ = std::move(newBuffer1);// buffers_和buffersToWrite交换数据,此时buffers_所有的数据存放在buffersToWrite,而buffers_变为空。buffersToWrite.swap(buffers_);// 如果nextBuffer_为空,将新的buffer(newBuffer2)移用为另一个缓冲区(nextBuffer_)。if (!nextBuffer_){nextBuffer_ = std::move(newBuffer2);}}assert(!buffersToWrite.empty());// 如果将要写入文件的buffer列表中buffer的个数大于25,那么将多余数据删除。// 前端陷入死循环,拼命发送日志消息,超过后端的处理能力,这是典型的生产速度超过消费速度,// 会造成数据在内存中的堆积,严重时引发性能问题(可用内存不足)或程序崩溃(分配内存失败)。if (buffersToWrite.size() > 25){char buf[256];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)));// 丢掉多余日志,以腾出内存,仅保留两块缓冲区buffersToWrite.erase(buffersToWrite.begin()+2, buffersToWrite.end());}// 将buffersToWrite的数据写入到日志文件中for (const auto& buffer : buffersToWrite){output.append(buffer->data(), buffer->length());}// 重新调整buffersToWrite的大小if (buffersToWrite.size() > 2){buffersToWrite.resize(2);}// 从buffersToWrite中弹出一个作为newBuffer1 if (!newBuffer1){assert(!buffersToWrite.empty());newBuffer1 = std::move(buffersToWrite.back());buffersToWrite.pop_back();newBuffer1->reset();}// 从buffersToWrite中弹出一个作为newBuffer2if (!newBuffer2){assert(!buffersToWrite.empty());newBuffer2 = std::move(buffersToWrite.back());buffersToWrite.pop_back();newBuffer2->reset();}// 清空buffersToWritebuffersToWrite.clear();output.flush();}output.flush();
}

4、代码运行图示

【第一种情况】:


【第二种情况】:


【第三种情况】:



【第四种情况】:



五、双缓冲异步日志的相关问题

  • 什么时候切换写到另一个日志文件?前一个buffer已经写满了,则交换两个buffer(写满的buffer置空)。

  • 日志串写入过多,日志线程来不及消费,怎么办?直接丢掉多余的日志buffer,腾出内存,防止引起程序故障。

  • 什么时候唤醒日志线程从Buffer中取数据?其一是超时,其二是前端写满了一个或者多个buffer。

双缓冲异步日志(Async Logging)相关推荐

  1. 多线程异步日志系统,高效、强悍的实现方式-双缓冲

    作 者:道哥,10+年嵌入式开发老兵,专注于:C/C++.嵌入式.Linux. 目录 文章目录 单片机中常用的环形缓冲区 多线程异步日志:双缓冲机制 双缓冲机制为什么高效 尽可能的降低 Lock 的时 ...

  2. 今儿新学会一个写日志技能:双缓冲机制

    摘要:通过交换指针的方式实现两个缓冲区的功能互换,十分巧妙,令人称赞. 本文分享自华为云社区<奇妙的双缓冲机制写日志(Java实现)>,作者: 洛叶飘 . 写日志面临的问题 写日志在Web ...

  3. muduo多线程异步日志分析

    最近在看muduo的源码,对于其日志系统的实现颇为感兴趣,找了两三天好好研究了一下,本文记录一些所学到的知识. 基础知识 日志是每个高性能服务器必备的组件,分为两种:诊断日志和交易日志.诊断日志,主要 ...

  4. Log4j2异步日志背后的数字

    作者:京东集团-京东零售-技术与数据中台-共享技术部-流量技术组 王丽. 引言 在程序开发过程中,几乎任何一个应用程序都会通过打印日志来记录跟踪程序运行情况,打印日志信息不仅可以让我们详细的了解程序内 ...

  5. log4j2 异步日志

    Log4j2异步日志 异步日志 log4j2最大的特点就是异步日志,其性能的提升主要也是从异步日志中受益,我们来看看如何使用log4j2的异步日志. 同步日志 异步日志 Log4j2提供了两种实现日志 ...

  6. java log4j 异步_Log4j2异步日志之异步格式化

    在优化系统响应时间的时候,除了优化业务逻辑/代码逻辑之外,把日志改成异步也是一种不错的方案 Log4j2在异步日志的性能上已经无人能挡了,其异步效率高的主要原因是使用disruptor来做异步队列 但 ...

  7. 一次鞭辟入里的 Log4j2 异步日志输出阻塞问题的定位

    一次鞭辟入里的 Log4j2 日志输出阻塞问题的定位 问题现象 线上某个应用的某个实例突然出现某些次请求服务响应极慢的情况,有几次请求超过 60s 才返回,并且通过日志发现,服务线程并没有做什么很重的 ...

  8. 一个轻巧高效的多线程c++stream风格异步日志(二)

    一个轻巧高效的多线程c++stream风格异步日志(二) 文章目录 一个轻巧高效的多线程c++stream风格异步日志(二) 前言 LogFile类 AsyncLogging类 AsyncLoggin ...

  9. JAVA多线程双缓冲笔记_关于多线程学习的笔记

    task: implement Observer and Observable, read shared_ptr base 库 1. base/StringPiece.h Viewpoint 1. 判 ...

最新文章

  1. 个人信息泄露致电信诈骗猖獗 专家:治理亟须完善立法
  2. 第十二课:小信号分析 (二)
  3. MySQL 误操作恢复表
  4. [MySQL] 几句MySQL时间筛选SQL语句[进入查看]
  5. 05-连接数据库方法
  6. arcgis jsapi接口入门系列(6):样式
  7. for in for of区别_Python 第5课:for…in循环黄金搭档之列表
  8. OO第四单元——UML及其解析器——总结 暨 OO课程大总结
  9. f4 stm32 神经网络_STM32神经网络开发工具箱将AI技术引入边缘和节点嵌入式设备...
  10. Windows 下 Composer 与 Laravel 4 的安装
  11. SpringAOP-基于@AspectJ的简单入门
  12. 基于OSSIM平台下华为交换机日志收集插件的开发
  13. 10.11 cocoapods安装
  14. GoPose人工智能运动分析软件
  15. 2022年全新UI聚合支付系统四方源码更新完美版
  16. 单片机 TDA8023 读 ic 卡 smrat card sync_card
  17. win2016开启ntp_Windows Server 2016-Windows 时间服务概览
  18. 如何用PDF编辑器将PDF文件拆分
  19. canvas实现pc端画板
  20. web大二实训作业:校园运动会网站设计——运动会图片轮播图片遮罩特效(4页)体育 HTML+CSS+JavaScript HTML5期末大作业...

热门文章

  1. 3D渲染技术分享:3D游戏开发渲染调试高级技巧
  2. php 五角星,php使用GD库画五角星
  3. VS2012序列号,激活码,【旗舰版】
  4. Diskgenius恢复硬盘误删文件及数据
  5. 根据GPS经纬度判断当前所属的市区
  6. 单元格里设置自动换行及调整行间距
  7. C# LINQ的Select与SelectMany函数
  8. 结构静力分析与动力学分析_51CAE_新浪博客
  9. openEuler操作系统的安装
  10. iOS开发系列--地图与定位