muduo网路库——高效的多线程日志
在服务端编程中,日志是必不可少的,通常用于故障诊断和追踪,也可用于性能分析。在生产环境中应该做到 "Log Everything All The Time"。
一个日志库大体可分为前端(frontend)和后端(backend)两部分。前端是供应用程序使用的接口(API),并生成日志消息;后端则负责把日志消息写到目的地。
在多线程程序中,每个线程有自己的前端,整个程序共用一个后端。对于C++程序而言,最好整个程序都使用相同的日志库,程序有一个整体的日志输出。
C++日志库的前端大体上有两种API风格:
C/Java 的printf(fmt,...) 风格,例如:
log_info("Received %d bytes from %s",len,getClientName().c_str());
C++ 的stream << 风格, 例如:
LOG_INFO << "Received " << len << " bytes from " << getClientName();
muduo日志库是C++ stream风格。
5.1 功能需求
日志的目的地只有一个:本地文件。
往网络写日志消息不靠谱,因为诊断日志的功能之一正是诊断网络故障,比如连接断开,网络拥塞等。如果日志消息也是通过网络发到另一台机器上,如果接受网络日志消息的服务器发生故障或者出现进程死锁,通常会导致发送日志的多个服务进程阻塞或者内存暴涨。另一个坏处是增加网络带宽消耗。
以本地文件为日志的destination,那么日志的滚动(rolling)是必需的,这样可以简化日志归档的实现。
rolling的条件通常有两个:
文件大小(例如每写满1GB就换下一个文件)和 时间(例如每天零点新建一个日志文件,不论前一个文件有没有写满)。
muduo日志库的默认消息格式:
5.2 性能需求
日志库的高效性体现在几方面:
1.每秒写几千上万条日志的时候没有明显的性能损失。
2.能应对一个进程产生大量日志数据的场景,例如 1GB/min。
3.不阻塞正常的执行流程。
4.在多线程程序中,不造成争用。
5.3 多线程异步日志
多线程程序对日志库提出了新的需求:线程安全,即多个线程可以并发写日志,两个线程的日志消息不会出现交织。
解决方法:用一个背景线程负责收集日志消息,并写入日志文件,其他业务线程只管往这个 “日志线程” 发送日志消息,这称为 ”异步日志“ 。
多线程服务程序中,异步日志是必需的,因为如果在网络IO线程或业务线程中直接往磁盘写数据的话,写操作偶尔可能阻塞长达数秒之久。这可能导致请求方超时或者耽误发送心跳消息。因此在正常的 实时业务中应该彻底避免磁盘IO。
需要一个”队列“来将日志前端的数据传送到后端(日志线程)。
muduo日志库采用的是双缓冲技术,基本思路是准备两块buffer:
A和B,前端负责往buffer A填数据(日志消息),后端负责将buffer B的数据写入文件。当buffer A写满之后,交换A和B,让后端将buffer A的数据写入文件,而前端则往buffer B填入新的日志消息,如此往复。用两个buffer的好处是在新建日志消息的时候不必等待磁盘文件操作,也避免每条新日志消息都出发(唤醒)后端日志线程。换言之,前端不是将一条条日志消息分别传送给后端,而是将多条日志消息拼成一个大的buffer传送给后端,相当于批处理,减少了线程唤醒的频率,降低开销。为了及时将日志消息写入文件,即便buffer A未满,日志库也会每3秒执行一次上述交换写入操作。