muduo日志库特点
- 日志批量写入
- 批量唤醒写线程
- 写日志用notify+wait_timeout 方式触发日志的写入
- 锁的粒度,双缓冲,双队列
- buffer默认 4M 缓冲区, buffers 是 buffer 队列, push 、 pop 时使用 move 语义 减少内存拷贝
muduo的这些特点使得其作为高性能日志库被广泛使用。
muduo日志库异步机制
muduo为了保证日志的写入速度快,采用异步机制处理日志信息。多个线程将日志信息保存在数据buffer中,等buffer装满了再将buffer放入日志队列,通过锁和条件变量保证日志队列的数据安全,最后由日志落盘线程将日志队列中的数据写入磁盘中。

日志写入固定缓冲区程序
void AsyncLogging::append(const char* logline, int len)
{// if(cnt++ == 50000)abort();MutexLockGuard lock(mutex_); // 多线程加锁if (currentBuffer_->avail() > len) // 判断buffer还有没有空间写入这条日志{currentBuffer_->append(logline, len); // 直接写入}else{buffers_.push_back(std::move(currentBuffer_)); // buffers_是vector,把buffer入队列// printf("push_back append_cnt:%d, size:%d\n", ++append_cnt, buffers_.size());if (nextBuffer_) // 用了双缓存{currentBuffer_ = std::move(nextBuffer_); // 如果不为空则将buffer转移到currentBuffer_}else{// 重新分配buffercurrentBuffer_.reset(new Buffer); // Rarely happens如果后端写入线程没有及时读取数据,那要再分配buffer}currentBuffer_->append(logline, len); // buffer写满了cond_.notify(); // 唤醒写入线程}
}
日志写入磁盘程序
void AsyncLogging::threadFunc()
{assert(running_ == true);latch_.countDown();LogFile output(basename_, rollSize_, false);BufferPtr newBuffer1(new Buffer); // 是给currentBuffer_BufferPtr newBuffer2(new Buffer); // 是给nextBuffer_newBuffer1->bzero();newBuffer2->bzero();BufferVector buffersToWrite; // 保存要写入的日志buffersToWrite.reserve(16);while (running_){assert(newBuffer1 && newBuffer1->length() == 0);assert(newBuffer2 && newBuffer2->length() == 0);assert(buffersToWrite.empty());{ // 锁的作用域MutexLockGuard lock(mutex_);if (buffers_.empty()) // 没有数据可读取,休眠{// printf("waitForSeconds into\n");cond_.waitForSeconds(flushInterval_); // 超时退出或者被唤醒(收到notify)// printf("waitForSeconds leave\n");}buffers_.push_back(std::move(currentBuffer_)); // currentBuffer_被锁住 currentBuffer_被置空// printf("push_back threadFunc:%d, size:%d\n", ++threadFunc_cnt, buffers_.size());currentBuffer_ = std::move(newBuffer1); // currentBuffer_ 需要内存空间buffersToWrite.swap(buffers_); // 用了双队列,把前端日志的队列所有buffer都转移到buffersToWrite队列if (!nextBuffer_) // newBuffer2是给nextBuffer_{nextBuffer_ = std::move(newBuffer2); // 如果为空则使用newBuffer2的缓存空间}}// 从这里是没有锁,数据落盘的时候不要加锁assert(!buffersToWrite.empty());// fixme的操作 4M一个buffer *25 = 100Mif (buffersToWrite.size() > 25) // 这里缓存的数据太多了,比如4M为一个buffer空间,25个buffer就是100M了。{printf("Dropped\n");char buf[256];snprintf(buf, sizeof buf, "Dropped log messages at %s, %zd larger buffers\n",Timestamp::now().toFormattedString().c_str(),buffersToWrite.size()-2); // 只保留2个bufferfputs(buf, stderr);output.append(buf, static_cast<int>(strlen(buf)));buffersToWrite.erase(buffersToWrite.begin()+2, buffersToWrite.end()); // 只保留2个buffer(默认4M)}for (const auto& buffer : buffersToWrite) // 遍历buffer{// FIXME: use unbuffered stdio FILE ? or use ::writev ?output.append(buffer->data(), buffer->length()); // 负责fwrite数据}output.flush(); // 保证数据落到磁盘了if (buffersToWrite.size() > 2){// drop non-bzero-ed buffers, avoid trashingbuffersToWrite.resize(2); // 只保留2个buffer}if (!newBuffer1){assert(!buffersToWrite.empty());newBuffer1 = std::move(buffersToWrite.back()); // 复用buffer对象buffersToWrite.pop_back();newBuffer1->reset(); // 重置}if (!newBuffer2){assert(!buffersToWrite.empty());newBuffer2 = std::move(buffersToWrite.back()); // 复用buffer对象buffersToWrite.pop_back();newBuffer2->reset(); // 重置}buffersToWrite.clear(); }output.flush();
}
日志批量写入
在日志写入固定缓冲区程序可以看到。muduo采用日志批量写入的形式将日志写入磁盘,具体实现方式是将日志写入一个固定大小的buffer中,等到buffer装满了,或者超时了,再将buffer放到队列中供写入线程写入到磁盘中。
如代码中将日志信息写入currentBuffer_
currentBuffer_->append(logline, len); // 日志内容,日志长度
void append(const char* /*restrict*/ buf, size_t len){// FIXME: append partiallyif (implicit_cast<size_t>(avail()) > len){memcpy(cur_, buf, len);cur_ += len;}}
批量唤醒写线程
在日志写入固定缓冲区程序可以看到。当currentBuffer_写满了,或者超时了,会通过条件变量唤醒写线程将数据写入磁盘。
cond_.notify(); // 唤醒写入线程
双缓冲区技术
在日志写入磁盘程序中,新建了一个buffersToWrite的缓冲区用来用来保存日志队列buffers_中的信息。
buffersToWrite.swap(buffers_); // 用了双队列,把前端日志的队列所有buffer都转移到buffersToWrite队列
这样做到好处在于:
- 线程安全;
- 非阻塞。
这样,buffersToWrite写入磁盘时,日志写入固定缓冲区程序仍然可以将数据写入缓冲区,不需要等待前面日志的写入完成提醒。
muduo的使用
#define LOG_TRACE if (Logger::logLevel() <= Logger::TRACE) \Logger(__FILE__, __LINE__, Logger::TRACE, __func__).stream()
#define LOG_DEBUG if (Logger::logLevel() <= Logger::DEBUG) \Logger(__FILE__, __LINE__, Logger::DEBUG, __func__).stream()
#define LOG_INFO if (Logger::logLevel() <= Logger::INFO) \Logger(__FILE__, __LINE__).stream()
#define LOG_WARN Logger(__FILE__, __LINE__, Logger::WARN).stream()
#define LOG_ERROR Logger(__FILE__, __LINE__, Logger::ERROR).stream()
#define LOG_FATAL Logger(__FILE__, __LINE__, Logger::FATAL).stream()
#define LOG_SYSERR Logger(__FILE__, __LINE__, false).stream()
#define LOG_SYSFATAL Logger(__FILE__, __LINE__, true).stream()
inline LogStream& operator<<(LogStream& s, T v)
{s.append(v.str_, v.len_);return s;
}inline LogStream& operator<<(LogStream& s, const Logger::SourceFile& v)
{s.append(v.data_, v.size_);return s;
}
muduo在Logging.cc函数中,通过重载<<和宏定义,使得在写入日志时可以按以下形式操作:
LOG_INFO << "NO." << i << " Root Error Message!";
文章参考与<零声教育>的C/C++linux服务器高级架构系统教程学习















