muduo学习笔记 - 第五章 高效的多线程日志

扶醉桌前 提交于 2020-03-03 00:54:16

第五章 高效的多线程日志

  • 日志有两种意思:

    • 诊断日志

    • 交易日志

  • 本章讲的是前一种日志,文本的供人阅读的日志,通常用于故障诊断和追踪,也可用于性能分析。

  • 日志通常要记录:

    • 收到的每条消息的id(关键字段,长度,hash等)

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

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

    • 关键部分状态的变更,等等

5.1 功能需求

  • 日志库大体分为前端和后端两个部分

    • 前端负责提供应用程序使用的接口API,并生成日志消息
    • 后端负责把日志消息写到目的地
  • C++日志库的前端大体有两种API风格

    • printf的格式化输出风格
    • stream<<风格

    stream风格的好处是当输出日志级高于语句的日志级别时,打印日志操作时个空操作,运行时开销接近零

  • 分布式系统中的服务进程而言,日志的目的地只有一个:本地文件。往网络写日志消息时不靠谱的,因为诊断日志功能之一正是诊断网络故障,如果日志消息也是通过网络发到另一台机器就一损俱损…

  • 本地文件作为destination,日志文件的滚动时必须的,可以简化日志的归档实现

    • 文件大小(例如写满10GB就换下一个文件)
    • 时间(例如每天零点新建一个日志文件,不论上一个文件是否写满)

    日志文件压缩和归档,不应该是日志库应有的功能,应该交给专门的脚本去做

  • 日志重复利用空间的功能,只会帮倒忙

  • 往文件写日志的常见问题是,如果程序崩溃,最后几条日志信息就会丢失,因为日志库不能每条消息都flush硬盘,更不能每条消息都open/close文件,这样开销太大。

    • 定时(默认3秒)将缓冲区的日志消息flush到硬盘
    • 每条内存中的日志消息都带有cookie,其值为函数地址,通过core dump文件找到cookie就能找到尚未写到硬盘的消息

5.2 性能需求

  • 日志库的高性能体现在:
    • 每秒写几千上万条日志的时候没有明显的性能损失
    • 能应对一个进程产生大量日志数据的场景,1GB/min
    • 不阻塞正常的执行流程
    • 在多线程中不造成争用
  • muduo日志库的实现的优化措施:
    • 时间戳字符串中的日期和时间两部分是缓冲的,一秒之内的多条日志只需重新格式化微妙部分
    • 日志消息前4个字节时定长的,避免在运行期间求strlen,编译器认识memcpy()函数,对于定长的内存复制,会在编译期把它inline展开为高效的目标代码
    • 线程id时预先格式化为字符串,输出只需要拷贝几个字节
    • 每条日志消息的源文件名部分采用了编译期计算来获得basename,避免运行期strrchr()开销

5.3 多线程异步日志

多线程程序的每个进程最好写一个日志文件,这样分析日志最容易,不必再多个文件中跳来跳去

  • muduo日志库采用的时双缓冲技术

    准备两块buffer:A和B,前端负责往buffer A写入数据,后端负责将buffer A的数据写到文件中,之后前端往buffer B中填入新的数据,如此反复。

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

    为了及时将日志消息写道文件,即使buffer A没有满,日志库也要没3秒执行一次swap(buffer A, buffer B)

  • muduo采用前端后端,各两个buffer(curr buffer,next buffer)和一个buffer vector保存full buffer四种可能存在的情况

    • 前端写日志频率不高,后端3秒超时写入文件

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ny3JEoOc-1583147157747)(./img/5.3-buf1.png)]

      在第2.9s,curr使用了80%,第3秒后端线程醒过来,将curr送到buffers,把new1移动到curr,随后交换buffers和buffersToWrite,当文件写完之后,把new1重新填上,等待下次cond.waitForSeconds()返回

    • 3秒超时之前已经写满burr buffer,唤醒后端开始写入文件

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pY35lWXl-1583147157765)(./img/5.3-buf2.png)]

      在第1.5s,curr使用了80%,在1.8scurr写满送到buffers,将next替换到curr buff,唤醒后端线程。后端将curr加到buffers中,在把new1移动到curr,交换buffers和buffersToWrite,new2替换next。完成之后重新填充new1和new2

    • 前端短时间密集写入日志消息,用完了两个缓冲需要新分配缓冲

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1WTHFN2J-1583147157770)(./img/5.3-buf3.png)]

      在第1.8s,A已经写满,B接近满,已经notify()后端线程,但是后端线程由于一些原因没有立即响应,到1.9s线程B写满,前端线程新创建缓冲E。在1.8s+后端线程获得控制权,将C,D移动给前端,把当前的curr放到buffers,将A、B、E写到文件。使用A、B填充new1/2释放缓冲E

    • 文件写入速度较慢,导致前端耗尽了两个缓冲,并分配了新缓冲

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vz3gpLwR-1583147157772)(./img/5.3-buf4.png)]

      1.8s前和场景2相同,后端线程写入过慢,导致前端已经写满两个缓冲并且分配了一个新的缓冲,这期间notify已经丢失。当后端完成之后,发现buffers不为空,立刻进入下个循环

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!