首先,这篇文章准备分析一下kudu里的日志类是怎么实现的,简单来说,日志的刷盘是一个多生产者单消费者模型,首先写日志的一个或多个线程会调用write会将message互斥写入内存的缓存里(应用级),然后通知工作线程也就是RunThread(),将应用缓存里的消息写入内核缓存,甚至落盘。kudu的临界区,也就是日志的buffer采用了双缓存交替的结构。试想如果只有一块buffer,工作线程落盘的时候,write无法写入需要等待日志落盘结束,程序阻塞。而双缓存就没有这个问题,buf1在写入到磁盘的时候,buf2用于接受新的message,落盘完成时,清空buf1,用作接受新的message,而buf2此时积累了message需要落盘,在任意时刻,write总是能够往内存写入message。
异步日志的实现在 async_logger.h/async.logger.cc中
async_logger.h:
#pragma once
#include "kudu/gutil/macros.h"
#include <memory>
#include <string>
#include <thread>
#include <vector>
#include <glog/logging.h>
#include "kudu/util/locks.h"
namespace kudu {
//asynclogger是对glog的封装 用于实现异步写日志
//启动一个新的线程用于顺序把信息写到日志,采用了双缓冲区的设计.writer日志线程往当前buffer
//添加日志 并且唤醒logger线程.logger被唤醒后,换入一块新的缓冲区,并且把积累的信息写入内核缓存甚至是磁盘。
// 双缓冲区的设计用于提高性能,因为记录的信息需要刷盘,两个缓冲区可以把写入以及刷盘两个过程 流水线化。
// 封装的类提供的语义弱于原始的glog.默认glog马上把(Waring级别以上的信息)缓存刷盘 而此处只是刷入一个分离的线程
// ,如果线程崩溃 信息也不见了.但是此处是为了更好的性能考虑 减少刷盘的调用次数,仅当有FATAL级别的信息时,马上刷盘.
// 注意:日志限制了缓冲区的总大小,所以如果底层日志阻塞太久,最终导致那些产生信息的线程也会被阻塞.
class AsyncLogger : public google::base::Logger {
public:
AsyncLogger(google::base::Logger* wrapped,
int max_buffer_bytes);
~AsyncLogger();
void Start();
//启动一个日志工作线程 负责将应用缓存数据写入内核缓存以及落盘 当本线程调用write或者flush时 会与日志工作线程进行交互
void Stop();
//停止异步日志 停止后 写日志以及刷盘的操作都不能再调用了
void Write(bool force_flush,
time_t timestamp,
const char* message,
int message_len) override;
//写日志 4个参数 force_flush表示 是则立即刷盘 否先写入内存缓存区 时间戳 信息字符串 信息长度
//较为高层地写入日志消息 如果force_flush为false 则仅写入应用缓存 若为true 则需要等日志落盘
voi