高效的多线程日志

  • 日志(logging)有两个意思:

    • 诊断日志(diagnostic log), 常用日志库提供日志功能;
    • 交易日志(transaction log), 用于记录状态变更, 通过回放日志可以逐步恢复每一次修改后的状态;
  • 日志通常用于故障诊断和追踪(trace), 也可用于性能分析;
  • 日志通常是分布式系统中事故调查时的唯一线索, 用来追寻蛛丝马迹, 查出原凶;
    • Log Everything All The Time;
  • 关于进程, 日志通常要记录:
    • 收到每条内部消息的ID(还可以包括关键字段、长度、hash等);
    • 收到的每条外部消息的全文;
    • 发出每条消息的全文, 每条消息都有全局唯一的id;
    • 关键内部状态的变更, 等等;
  • 每条日志都有时间戳, 这样就能完整追踪分布式系统中一个事件的来龙去脉;
  • 一个日志文件可分为前端(frontend)和后端(backend)两部分;
    • 前端提供应用程序使用的接口(API), 并生成日志消息(log message);
    • 后端则负责把日志消息写到目的地(destination);
    • 典型的多生产者-单消费者问题, 对生产者(前端)而言, 要尽量做到低延迟、低CPU开销、无阻塞;
    • 对消费者(后端)而言, 要做到足够大的吞吐量, 并占用较少资源;
  • 对C++程序而言, 最好整个程序(包括主程序和程序库)都使用相同的日志库, 日志有一个整体的日志输出, 而且不要各个组件有各自的日志输出;
    • 从这个意义上讲, 日志库是个singleton模式(单例模式);
  • muduo没有用标准库中的iostream, 而是自己写的LogStream class, 主要是出于性能原因;

功能需求

  • 日志消息有多种级别(level): TRACE、DEBUG、INFO、ERROR、FATAL等;
  • 日志消息可能有多个目的地(appender), 如文件、socket、SMTP等;
  • 日志消息的格式可配置(layout), 例如org.apache.log4j.PatternLayout;
  • 可以设置运行时过滤器(filter), 控制不同组件的日志消息的级别和目的地;
  • 质量保证(QA)测试环境的时候输出DEBUG级别的日志, 在生产环境输出INFO级别的日志;
  • 只要调用muduo::Logger::setLogLevel()就能立即生效;
  • 对于分布式系统中的服务进程而言, 日志的目的第(destination)只有一个:本地文件;
    • 日志文件的滚动(rolling)是必需的, 这样可以简化日志归档(archive)的实现;
  • rolling的条件通常有两个: 文件大小(例如每写满1GB就换下一个文件)和时间(例如每天零点新建一个日志文件, 不论前一个文件有没有写满);
    • 一般的日志库都会自动根据文件大小和时间来主动滚动日志文件;
    • 能主动rolling, 自然也就不必支持SIGUSR1了, 毕竟多线程程序处理signal很麻烦;
  • 日志库的压缩与归档(archive)不是日志库应该有的功能, 而应该交给专门的脚本去做;
  • 磁盘空间监控也不是日志库的必备功能;
  • muduo日志库: 其一是定期(默认3秒), 将缓冲区内的日志消息flush到磁盘;
    • 其二是每条内存中的日志消息都带有cookie(或者叫哨兵值/sentry), 其值为某个函数的地址, 这样通过core dump文件中查找cookie就能找到尚未来得及写入磁盘的消息;
  • 日志消息格式有几个要点:
    • 尽量每条日志占一行;
    • 时间戳精确到微妙;
    • 始终使用GMT时区(Z);
    • 答应线程id;
    • 打印日志级别;
    • 打印源文件名和行号;

性能需求

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

    • 每秒写上千万条日志的时候没有明显的性能损失;
    • 能应对一个进程生产大量日志数据的场景, 例如1GB/min;
    • 不阻塞正常的执行流程;
    • 在多程序程序中, 不造成争用(contention);
    • 磁盘带宽约是110MB/S, 日志库应该能瞬时写满这个带宽(不必持续太久);
    • 假如每条日志消息的平均长度是110字节, 这就意味着1秒要写100万条日志;
  • muduo日志库实现了几点优化措施:
    • 时间戳字符串中的日期和时间部分是缓存的, 一秒内的多条日志只需要重新格式化微妙部分;
    • 日志消息的前4个字段是定长的, 因此可以避免在运行期求字符串长度(不会反复调用strlen);
      • 因为编译器认识memcpy()函数, 对于定长的内存复制, 会在编译期把它的inline展开为高效的目标代码;
    • 线程id是预先格式化为字符串, 在输出日志消息时只需要简单拷贝几个字节;
    • 每行日志消息的源文件名部分采用了编译期计算来获得basename, 避免运行期strrchr()开销;

多线程异步日志

  • 多线程程序对日志库提出了新的需求:线程安全, 即多个程序可以并发写日志, 两个线程的日志消息不会出现交织;
  • 多线程的每个进程最好只写一个日志文件;
    • 用一个背景线程收集日志消息, 并写入日志文件, 其他业务线程只管往这个日志线程发送日志消息, 这称为异步日志;
    • 非阻塞日志;
  • 我们需要一个队列将日志前端的数据传送到后端(日志线程);
    • muduo日志库采用的是双缓冲(double buffering)技术;
    • 基本思路是准备两块buffer:A和B, 前端负责往buffer A填数据(日志消息), 后端负责将buffer B的数据写入文件;
    • 当buffer A写满之后, 交换A和B, 让后端将buffer A的数据写入文件, 而前端则往buffer B填入新的日志消息, 如此往复;
    • 为了及时将日志消息写入文件, 即便buffer A未满, 日志库也会每3秒执行一次上述交换写入操作;
  • 对于异步日志来说, 这是典型的生产速度高于消费速度问题, 会造成数据在内存中的堆积, 严重时引发性能问题(可用内存不足)或程序崩溃(分配内存失败);
    • 直接丢弃掉多余的日志buffer, 以腾出内存, 这样可以防止程序库本身引起程序故障, 是一种自我保护措施;
    • 也可以加入网络报警功能, 通知人工介入, 以尽快修复故障;
  • 性能不能凭感觉说了算, 一定要有典型场景的测试数据做为支撑;
  • muduo库的异步日志实现用了一个全局锁;
    • java的ConcurrentHashMap那样用多个桶子(bucket), 前端写日志的时候再按线程id哈希到不同的bucket中, 以减少contention(后端实现比较复杂);
    • Linux默认会把core dump写到当前目录, 而且文件名是固定的core;

Mudo C++网络库第五章学习笔记的更多相关文章

  1. Mudo C++网络库第十一章学习笔记

    反思C++面向对象与虚函数 C++语言学习可以看<C++ Primer>这本书; 在C++中进行面向对象编程会遇到其他语言中不存在的问题, 其本质原因是C++ class是值语义, 而非对 ...

  2. Mudo C++网络库第六章学习笔记

    muduo网络库简介 高级语言(Java, Python等)的Sockects库并没有对Sockects API提供更高层的封装, 直接用它编写程序很容易掉到陷阱中: 网络库的价值还在于能方便地处理并 ...

  3. Mudo C++网络库第四章学习笔记

    C++多线程系统编程精要 学习多线程编程面临的最大思维方式的转变有两点: 当前线程可能被切换出去, 或者说被抢占(preempt)了; 多线程程序中事件的发生顺序不再有全局统一的先后关系; 当线程被切 ...

  4. Mudo C++网络库第三章学习笔记

    多线程服务器的适用场合与常用编程模型 进程间通信与线程同步; 以最简单规范的方式开发功能正确.线程安全的多线程程序; 多线程服务器是指运行在linux操作系统上的独占式网络应用程序; 不考虑分布式存储 ...

  5. Mudo C++网络库第七章学习笔记

    muduo编程示例 muduo库是设计来开发内网的网络程序, 它没有做任何安全方面的加强措施, 如果在公网上可能会受到攻击; muduo库把主动关闭连接这件事分成两步来做: 如果主动关闭连接, 会先关 ...

  6. 《Linux内核设计与实现》课本第五章学习笔记——20135203齐岳

    <Linux内核设计与实现>课本第五章学习笔记 By20135203齐岳 与内核通信 用户空间进程和硬件设备之间通过系统调用来交互,其主要作用有三个. 为用户空间提供了硬件的抽象接口. 保 ...

  7. 《Linux内核设计与实现》第五章学习笔记

    <Linux内核设计与实现>第五章学习笔记 姓名:王玮怡  学号:20135116 一.与内核通信     在Linux中,系统调用是用户空间访问内核的唯一手段:除异常和陷入外,它们是内核 ...

  8. Spring实战第五章学习笔记————构建Spring Web应用程序

    Spring实战第五章学习笔记----构建Spring Web应用程序 Spring MVC基于模型-视图-控制器(Model-View-Controller)模式实现,它能够构建像Spring框架那 ...

  9. Linux 第五章 学习笔记

    ---恢复内容开始--- 第五章 系统调用 一.与内核通信 1.系统调用在用户控件进程和硬件设备之间添加了一个中间层. 为用户空间提供了一种硬件的抽象接口 系统调用保证了系统的稳定和安全 每个进程都运 ...

随机推荐

  1. 12.代理模式(Proxy Pattern)

    直接与间接:   人们对复杂的软件系统常有一种处理手法,即增加一层间接层,从而对系统获得一种更为灵活.满足特定需求的解决方案.                                      ...

  2. Linux记录-JMX监控Tomcat上传到falcon

    1.登录测试服务器xxxxxx xxxxxx su root输入xxxx 2.先修改Tomcat的启动脚本,(linux下为catalina.sh),添加以下内容: CATALINA_OPTS=&qu ...

  3. 使用JMeter进行一次简单的带json数据的post请求测试,json可配置参数

    配置: 1.新建一个线程组: 然后设置线程数.运行时间.重复次数. 2.新建Http请求: 设置服务器域名,路径,方法,编码格式,数据内容. 可以在函数助手中,编辑所需要的变量,比如本例中的随机生成电 ...

  4. HDU - 1540 Tunnel Warfare(线段树区间合并)

    https://cn.vjudge.net/problem/HDU-1540 题意 D代表破坏村庄,R代表修复最后被破坏的那个村庄,Q代表询问包括x在内的最大连续区间是多少. 分析 线段树的区间内,我 ...

  5. PowerDesigner设置一对一关系

    (1)修改Cardinalities 为One-one (2)设置Dominant role A->B(假设表A,表B),保存 (3)到Joins页,设置Parent为None,设置Parent ...

  6. Tooltip导致的无法访问已释放对象

    最近C#项目中遇到了一个无法访问已释放对象问题,经过反复测试,最终发现问题出在控件Tootip上,因为tootip内部有一个定时器,如果在窗口销毁时,鼠标移动到控件上恰好产生了一个tooltip,就会 ...

  7. lua 设置文件运行的环境

    背景 在一个lua文件中书写的代码, 使用的变量, 需要设置其运行环境. 目的: 1. 不破坏全局环境. 2. 限定文件所使用的环境, 作为沙箱功能. 解法 限定运行空间环境的文件: local m ...

  8. 【51nod 1191】消灭兔子

    Description 有N只兔子,每只有一个血量B[i],需要用箭杀死免子.有M种不同类型的箭可以选择,每种箭对兔子的伤害值分别为D[i],价格为P[i](1 <= i <= M).假设 ...

  9. B - 签到题

    计算机系统中使用的UTC时间基于原子钟,这种计时方式同“地球自转一周是24小时”的计时方式有微小的偏差.为了弥补这种偏差,我们偶尔需要增加一个“闰秒”. 最近的一次闰秒增加发生在UTC时间2016年的 ...

  10. 2017CCPC秦皇岛 H题Prime Set&&ZOJ3988

    题意: 定义一种集合,只有两个数,两个数不同且加起来为素数.要从n个数里抽出数字组成该集合(数字也可以是1~n,这个好懵圈啊),要求你选择最多k个该种集合组成一个有最多元素的集合,求出元素的数量. 思 ...