双缓冲日志模式

发布时间:2026/7/1 15:54:18
双缓冲日志模式 一、什么是双缓冲日志模式双缓冲日志模式简单说就是准备两个缓冲区一个负责让业务线程写日志另一个负责让日志线程刷盘。当一个缓冲区写满或满足刷盘条件时两个缓冲区交换角色。它的核心目标是减少业务线程直接写磁盘的开销降低锁竞争提高日志写入吞吐避免每条日志都调用write()让日志写入变成批量写入。二、为什么需要双缓冲如果每次打印日志都直接写文件大概是这样voidlog(conststringmsg){write(fd,msg.c_str(),msg.length());}这看起来简单但问题很多磁盘 I/O 慢write()可能涉及系统调用如果频繁调用性能开销很大。多线程竞争严重多个业务线程同时写日志需要加锁所有线程都在争抢同一个日志文件。业务线程被阻塞日志本来是辅助功能但直接写文件可能拖慢主业务逻辑。所以更好的做法是业务线程只负责把日志写入内存缓冲区真正写文件的事情交给后台日志线程。三、双缓冲的基本结构通常会有两个缓冲区ostringstream streamA;ostringstream streamB;或者类似string bufferA;string bufferB;再维护两个指针ostringstream*currentStream;// 当前业务线程写入的缓冲区ostringstream*writeStream;// 当前日志线程刷盘的缓冲区逻辑大概是currentStreamstreamA;writeStreamstreamB;业务线程不断写*currentStreamlogMessage;日志线程发现需要写文件时交换两个缓冲区swap(currentStream,writeStream);之后业务线程继续往新的currentStream写日志日志线程把旧的writeStream内容写入文件。四、结合其他代码来看你之前的代码里有这一段lock();ostringstream*streamswapStream();string logstrstream-str();stream-str();unlock();这就是典型的双缓冲设计。可以理解为lock();加锁防止交换缓冲区时业务线程还在写。ostringstream*streamswapStream();切换缓冲区。比如原来业务线程正在写streamA日志线程就把它切走然后让业务线程改写streamB。string logstrstream-str();取出旧缓冲区中的日志内容。stream-str();清空旧缓冲区方便下次继续使用。unlock();释放锁。然后日志线程在锁外写文件intretvalwrite(fd,logstr.c_str(),logstr.length());这一点很关键只在切换缓冲区时加锁真正写磁盘时不持有锁。这样做可以避免业务线程被磁盘 I/O 长时间阻塞。五、双缓冲日志的运行过程假设有两个缓冲区Buffer A Buffer B初始状态业务线程写入 - Buffer A 日志线程备用 - Buffer B业务线程不断写日志Buffer A: [log1] [log2] [log3]日志线程准备刷盘时交换 Buffer A 和 Buffer B交换后业务线程写入 - Buffer B 日志线程刷盘 - Buffer A日志线程把Buffer A写入文件write(fd, Buffer A)写完后清空Buffer A empty下一次再交换业务线程写入 - Buffer A 日志线程刷盘 - Buffer B反复循环。六、一个简化版例子下面是一个简化版双缓冲日志模型方便理解classDoubleBufferLogger{private:ostringstream bufferA;ostringstream bufferB;ostringstream*currentBuffer;ostringstream*flushBuffer;mutex mtx;intfd;public:DoubleBufferLogger(intlogFd):currentBuffer(bufferA),flushBuffer(bufferB),fd(logFd){}voidappend(conststringmsg){lock_guardmutexlock(mtx);(*currentBuffer)msg\n;}voidflush(){string logData;{lock_guardmutexlock(mtx);swap(currentBuffer,flushBuffer);logDataflushBuffer-str();flushBuffer-str();flushBuffer-clear();}if(!logData.empty()){write(fd,logData.c_str(),logData.size());}}};重点是这一段{lock_guardmutexlock(mtx);swap(currentBuffer,flushBuffer);logDataflushBuffer-str();flushBuffer-str();flushBuffer-clear();}它只在交换缓冲区和取数据的时候加锁。而真正的磁盘写入write(fd,logData.c_str(),logData.size());是在锁外完成的。七、再举一个生活化例子可以把双缓冲日志想象成餐厅洗碗。有两个盘子筐筐 A 筐 B服务员不断把脏盘子放进筐 A。当筐 A 快满了洗碗工说筐 A 给我洗你们先用筐 B。于是服务员继续往筐 B 放盘子 洗碗工同时清洗筐 A。等筐 B 快满了再交换筐 B 给洗碗工 服务员继续用筐 A。这样服务员不会因为洗碗工正在洗盘子而停下来。对应到日志系统餐厅场景日志系统服务员业务线程脏盘子日志消息盘子筐 A/B日志缓冲区 A/B洗碗工日志线程洗盘子写入日志文件交换盘子筐switchStream()八、再举一个网络发送例子双缓冲不仅能用于日志也常用于网络发送。比如游戏服务器要给客户端发送数据。如果每产生一条消息就立刻发送send(socketFd,msg.c_str(),msg.size(),0);性能会很差。可以设计两个发送缓冲区sendBufferA sendBufferB业务线程把待发送消息放到sendBufferAsendBufferA.append(packet);网络线程准备发送时交换缓冲区swap(activeSendBuffer,pendingSendBuffer);然后网络线程批量发送send(socketFd,pendingSendBuffer.data(),pendingSendBuffer.size(),0);这样业务线程不用等待网络 I/O。九、再举一个图形渲染例子双缓冲在图形渲染里也非常常见。例如屏幕显示一帧画面时如果直接在当前屏幕缓冲上画图用户可能看到画面撕裂或闪烁。所以图形系统通常有前台缓冲区 front buffer 后台缓冲区 back buffer程序先在后台缓冲区画完整的一帧back buffer: 正在绘制 front buffer: 正在显示画完之后交换swap(front buffer, back buffer)这样用户看到的是完整画面而不是正在绘制一半的画面。这个思想和双缓冲日志非常类似图形渲染日志系统back buffer当前写入日志的缓冲区front buffer准备输出的缓冲区绘图线程业务线程显示器刷新日志线程写文件buffer swapswitchStream()十、双缓冲日志的优点1. 减少系统调用次数不用每条日志都write()。可以把很多日志合并成一次写入write(fd,logstr.c_str(),logstr.length());这是批量写入。2. 减少锁持有时间你的代码中锁的范围是lock();ostringstream*streamswwapStream();string logstrstream-str();stream-str();unlock();写文件不在锁里面write(fd,logstr.c_str(),logstr.length());这很好。因为磁盘 I/O 可能很慢如果持锁写磁盘其他线程写日志就会被卡住。3. 提高业务线程性能业务线程只负责写内存*currentStreammsg;内存操作很快。真正慢的文件写入交给后台线程处理。4. 更适合高并发日志多个业务线程可以快速把日志放到缓冲区里然后让日志线程统一刷盘。这比所有线程直接抢日志文件要好很多。十一、需要注意的问题双缓冲日志虽然好但也有一些坑。1. 程序崩溃时可能丢日志因为日志先写进内存缓冲区不是马上落盘。如果程序突然崩溃缓冲区里的日志可能还没写入文件。解决办法重要错误日志立即刷盘定时刷盘程序退出前强制 flush对严重日志调用fsync()但性能会下降。2. 日志量太大时缓冲区可能撑爆如果业务线程写得太快而日志线程刷盘太慢缓冲区可能不断变大。解决办法设置缓冲区最大容量超过容量后丢弃低级别日志阻塞业务线程使用多缓冲区队列按日志级别限流。3.ostringstream清理要注意清空内容时常见写法是stream-str();stream-clear();str()是清空内部字符串内容。clear()是清除流状态比如错误状态。更稳妥的写法是两个都调用。4.write()不一定一次写完你代码里这一句intretvalwrite(fd,logstr.c_str(),logstr.length());需要注意write()返回值可能小于logstr.length()。也就是说它可能只写入了一部分。更稳妥的写法应该循环写ssize_twriteAll(intfd,constchar*data,size_t len){size_t total0;while(totallen){ssize_t nwrite(fd,datatotal,len-total);if(n0){if(errnoEINTR){continue;}return-1;}if(n0){break;}totaln;}returntotal;}然后ssize_t retwriteAll(m_ltLogFD,logstr.c_str(),logstr.length());if(ret0){coutLog(__FILE__,__LINE__,LL_ERROR,write to log file failed. errno%d,errno);returnret;}十二、适合写到 blog 的总结你可以在 blog 里这样描述双缓冲日志模式是一种常见的异步日志优化方案。它通过维护两个内存缓冲区让业务线程和日志写入线程分别工作在不同的缓冲区上。当日志线程需要刷盘时只需要短暂加锁并交换两个缓冲区随后在锁外将旧缓冲区中的日志批量写入文件。这样既减少了磁盘 I/O 次数又缩短了锁持有时间从而提高多线程程序中的日志写入性能。也可以再精简成一句双缓冲日志的核心思想是业务线程只写内存日志线程批量刷盘二者通过交换缓冲区降低锁竞争和 I/O 开销。你的那段代码里最关键的双缓冲逻辑就是lock();ostringstream*streamswapStream();string logstrstream-str();stream-str();unlock();其中swapStream()很可能就是将当前写入缓冲区和待刷盘缓冲区进行交换。这就是双缓冲日志模式的核心。