第五章 高效的多线程日志
-
日志有两种意思:
-
诊断日志
-
交易日志
-
-
本章讲的是前一种日志,文本的供人阅读的日志,通常用于故障诊断和追踪,也可用于性能分析。
-
日志通常要记录:
-
收到的每条消息的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不为空,立刻进入下个循环
-
来源:CSDN
作者:Sanzona
链接:https://blog.csdn.net/henuyh/article/details/104617567