kudu源码分析之async_logger

来源:互联网 发布:淘宝无人机 编辑:程序博客网 时间:2024/05/16 03:02


       首先,这篇文章准备分析一下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 则需要等日志落盘    void Flush() override;  //日志落盘    uint32 LogSize() override;  //获取日志文件大小 粗略 因为部分日志还在缓存  // Return a count of how many times an application thread was  // blocked due to the buffers being full and the writer thread  // not keeping up.  int app_threads_blocked_count_for_tests() const {    MutexLock l(lock_);//因buf满导致调用write的线程被阻塞的次数    return app_threads_blocked_count_for_tests_;  }   private:    struct Msg {    time_t ts;    std::string message;    Msg(time_t ts, std::string message)        : ts(ts),          message(std::move(message)) {    }  };  //Msg结构体较为简单 时间+信息.值得注意的是 构造函数中用到了std::move 移动语义  //如果不使用移动语义 而是直接使用message(message) 会发送以下操作:  //函数内先构造一个临时变量string tmp,  //然后是tmp的拷贝构造 把参数message里char指针指向的内容复制到tmp,  //再由tmp深拷贝给message,然后析构临时tmp,再析构参数里的message  //上述的原因仅仅因为没有经过move的message是一个左值变量 而move之后是一个右值  //右值就相当于所有权转让 令成员变量message的char指针等于参数message的char指针 并且把参数message的指针置为空  //构造以及析构的调用次数 降低了 速度变快 这样的改进在string比较长的时候 或者msg数量多的时候 还是会有比较大的性能差异     struct Buffer {        std::vector<Msg> messages;//缓存区维护了Msg的一个vector    int size = 0;    //缓存区所有消息总字节量初始化为0    bool flush = false;    //buffer是否需要刷盘     Buffer() {}    //空构造函数    void clear() {      messages.clear();      size = 0;      flush = false;    }    //落盘完毕后调用clear重置应用缓存 清空vector 总量归零 flush标志重新为flag     void add(Msg msg, bool flush) {      size += sizeof(msg) + msg.message.size();      messages.emplace_back(std::move(msg));      this->flush |= flush;    }    //往active的那个应用缓冲区中的添加消息 更新总量    //值得注意的是 此处vector没有用push_back    //用的是push_back的右值参数版本emplace_back(本人也是第一次知道有这API)    //如果这条消息(比如是FATAL级别的消息)需要立即flush 那么该缓存区标记为flush为true    //那么日志工作线程被write唤醒后 就会检查该标志位 从而将日志真正刷入磁盘    bool needs_flush_or_write() const {      return flush || !messages.empty();    }    //日志工作线程每次被唤醒后都要通过该函数检查是否需要写入内核缓存或者落盘   private:    DISALLOW_COPY_AND_ASSIGN(Buffer);      //不允许复制以及赋值      //这个宏展开实际上就是把拷贝构造以及operator=设为私有      //原因是这两个函数填写麻烦 容易出错 又怕如果不填写这两个函数 造成浅拷贝  };  bool BufferFull(const Buffer& buf) const;    //检查是否缓存区满  void RunThread();    //日志工作线程运行的函数体  const int max_buffer_bytes_;    //Buffer里最大的字节数  google::base::Logger* const wrapped_;    //Logger类的指针 子类的write只是写入内核缓存 子类的flush也只是唤醒     //所以最终还是要通过指针调用父类的方法 将缓存区日志落盘  std::thread thread_;    //线程对象 C++11特性  // 阻塞次数 uint64_t flush_count_ = 0; //flush次数   mutable Mutex lock_;//互斥变量用于保护buffer以及状态state_ //因为工作线程以及调用writer的线程都需要访问buffer以及状态   ConditionVariable wake_flusher_cond_;//条件变量1 app线程(也就是调用writer的线程)发出信号 用于唤醒日志工作线程 唤醒条件为新数据 或者state变化    ConditionVariable free_buffer_cond_;//条件变量2 日志工作线程发出信号 因为日志工作换入新的缓存用于写 之前阻塞的writer可以继续    ConditionVariable flush_complete_cond_;//条件变量3 flusher发出信号 完成对当前缓存的清空 需要强制flush的writer收到信号才能返回  std::unique_ptr<Buffer> active_buf_;//智能指针 指向可用于写入的buf//为何要使用智能指针呢 因为可能有多个    std::unique_ptr<Buffer> flushing_buf_;//智能指针 指向当前正在刷盘的buf  // Trigger for the logger thread to stop.  enum State {    INITTED,    RUNNING,    STOPPED  };  State state_ = INITTED;  //状态枚举变量  DISALLOW_COPY_AND_ASSIGN(AsyncLogger);} // namespace kudu


看完头文件 看下实现 在async_logger.cc里。


#include "kudu/util/async_logger.h"#include <algorithm>#include <string>#include <thread>#include "kudu/util/locks.h"using std::string;namespace kudu {    AsyncLogger::AsyncLogger(google::base::Logger* wrapped,                             int max_buffer_bytes) :            max_buffer_bytes_(max_buffer_bytes),            wrapped_(DCHECK_NOTNULL(wrapped)),            wake_flusher_cond_(&lock_),            free_buffer_cond_(&lock_),            flush_complete_cond_(&lock_),            active_buf_(new Buffer()),            flushing_buf_(new Buffer()) {      DCHECK_GT(max_buffer_bytes_, 0);    }//异步日志的构造函数//需要基础类logger指针 以及最大字节数//三个条件变量与互斥变量lock_绑定//构造两个bufferAsyncLogger::~AsyncLogger() {}//默认析构函数void AsyncLogger::Start() {  CHECK_EQ(state_, INITTED);  state_ = RUNNING;  thread_ = std::thread(&AsyncLogger::RunThread, this);}//查看当前状态是否是initted//如果是 设为RUNNING 启动新线程 入口为RunThreadvoid AsyncLogger::Stop() {  {    MutexLock l(lock_);    CHECK_EQ(state_, RUNNING);    state_ = STOPPED;    wake_flusher_cond_.Signal();  }  thread_.join();  CHECK(active_buf_->messages.empty());  CHECK(flushing_buf_->messages.empty());}//结束时 修改state//唤醒wait日志工作线程线程 工作线程检测到state改变 跳出最外层循环 结束任务 回收线程//检测缓存区的信息是否为空void AsyncLogger::Write(bool force_flush,                        time_t timestamp,                        const char* message,                        int message_len) {  {    MutexLock l(lock_);    DCHECK_EQ(state_, RUNNING);    while (BufferFull(*active_buf_)) {      app_threads_blocked_count_for_tests_++;      free_buffer_cond_.Wait();    }    active_buf_->add(Msg(timestamp, string(message, message_len)),                     force_flush);    wake_flusher_cond_.Signal();  }//由想写日志的线程调用 //获取互斥变量 用于改变条件变量 如果是RUNNING 继续//如果缓存满 阻塞次数++ 等待工作线程换入新的缓存 才能继续写//如果空闲缓存区出现了 wait不再阻塞 activebuf为不是满//(这里的activebuf指针指向原来的flushbuf) 则跳出循环//往activebuf里增加msg 改变flush为true或者维持false//通知工作线程 把应用缓存里的信息写入内核缓存  if (message_len > 0 && message[0] == 'F') {    Flush();  }  //如果是致命消息 需要调用Flush()把内核缓存的消息全部写入磁盘  //Flush()会通知工作函数进行写磁盘的操作 然后阻塞直到返回  //如果不致命 就返回了 消息在缓存里 与某次需要flush的写操作一起落盘}void AsyncLogger::Flush() {  MutexLock l(lock_);  DCHECK_EQ(state_, RUNNING);  uint64_t orig_flush_count = flush_count_;  while (flush_count_ < orig_flush_count + 2 &&         state_ == RUNNING) {    active_buf_->flush = true;    wake_flusher_cond_.Signal();    //发送信号给工作线程    //直到工作线程    flush_complete_cond_.Wait();  }  //这个函数是writer的辅助函数 如果有fatal消息 就会阻塞直到落盘  //通知两次工作线程 保证两个buf都落盘 至于这么做的原因 我也还不清楚uint32 AsyncLogger::LogSize() {  return wrapped_->LogSize();//调用父类}void AsyncLogger::RunThread() {  MutexLock l(lock_);  while (state_ == RUNNING || active_buf_->needs_flush_or_write()) {    //外层循环 如果状态是RUNNING或者activebuf需要flush或者需要写    while (!active_buf_->needs_flush_or_write() && state_ == RUNNING) {      //如果async的writer以及flush没有被调用 则阻塞等待唤醒(不会一直占CPU) 直到跳出循环      wake_flusher_cond_.Wait();    }    //writer或者flush被调用    active_buf_.swap(flushing_buf_);    //要讲应用缓存里的数据刷入内核缓存 甚至落盘 则此时应用缓存不能添加新信息    //将active指针指向之前flushing过的空缓存    //将flushing指针指向当前需要flush的active指针    if (BufferFull(*flushing_buf_)) {      free_buffer_cond_.Broadcast();    }    //如果当前是满的 说明之前可能有write操作被阻塞    //通知阻塞写 往新的buf里面继续写        l.Unlock();//    for (const auto& msg : flushing_buf_->messages) {      wrapped_->Write(false, msg.ts, msg.message.data(), msg.message.size());      //这里的write是glog的write方法 先把消息写入内核缓存 依然有数据丢失危险    }    if (flushing_buf_->flush) {      wrapped_->Flush();      //这里的flush是glog的flush方法 从内核缓存清空 刷入文件    }    flushing_buf_->clear();    //清空flushingbuf的缓存 用于下一次无缝替换activebuf    l.Lock();    flush_count_++;    flush_complete_cond_.Broadcast();    //通知 刷盘完毕  }}bool AsyncLogger::BufferFull(const Buffer& buf) const {  // 某一buf超过最大上限的一般 即视为full  return buf.size > (max_buffer_bytes_ / 2);}} // namespace kudu



代码分析到这里就差不多了,第一次写这种技术博客,谈谈感想与心得吧。之前看一些大型开源项目的代码,容易卡壳,然后看着看着就迷失了自己,其实是基础不够牢固,后来花了一些时间看了APUE以及设计模式,补充了一些基础知识比如多线程的同步,条件变量,标准IO,内核缓存之类的知识,对底层熟悉了一些,看这些代码感觉就不那么费劲了,能与之前的知识进行印证,也有成就感了。而且kudu是比较新的项目,用的是C++,比较清爽,还能学习一些智能指针,移动语义 auto的新特性,不像一些老开源项目C与C++混杂在一起,宏又跳不过去,看的着实费力。 另外一个感受是Clion确实用着挺爽的!


0 0
原创粉丝点击