[C++]——同步异步日志系统(6)
同步异步日志系统
- 一、日志器模块设计
- 1.1 同步日志器模块设计
- 1.1.1 局部日志器建造者模式设计
- 1.1.2 同步日志器基本功能测试
- 1.2 异步日志器模块设计
- 1.2.1 单缓冲区设计
- 1.2.2 异步工作线程的设计(双缓冲区思想)
- 1.2.3 异步日志器设计
- 1.2.4 异步日志器建造者模式设计
- 1.2.5 异步日志器基本功能测试
一、日志器模块设计
功能:对前面所有功能进行整合,向外提供接口完成不同等级日志的输出。
管理的成员:
1.格式化模块对象
2.落地模块对象
3.默认的日志输出限制等级(大于等于限制输出等级的日志才能输出)
4.互斥锁(保证日志输出过程的线程安全,不会出现交叉日志)
5.日志名称(日志器的唯一标识,方便查找)
提供的操作:
debug等级日志的输出操作(分别封装日志消息LogMsg——各个接口日志等级不同)
info等级日志的输出操作
warn等级日志的输出操作
error等级日志的输出操作
fatal等级日志的输出操作
实现:
1.实现Logger基类(派生出同步日志器和异步日志器)
2.因为两种日志器的落地方式不同,需要将落地操作给抽象出来,不同的日志器调用不同的落地操作进行日志落地
3.模块关联过程中使用基类指针对子类日志器对象进行日志管理和操作
当前日志系统支持同步日志&异步日志,它们的不同点在于日志的落地方式上不同:
同步日志器:直接对日志消息进行输出
异步日志器:先将日志消息放到缓冲区,然后异步线程进行输出
因此:日志器类在设计的时候,先要设计一个Logger的基类,在Logger基类的基础上,继承出同步日志器(SyncLogger)和异步日志器(AsyncLoggrr)。
1.1 同步日志器模块设计
- 同步日志器的设计和具体框架
/*日志器模块 1.先设计一个日志器的基类 2.根据基类派生出不同的日志器 */ #include "util.hpp" #include "level.hpp" #include "sink.hpp" #include "format.hpp" #include #include #include namespace logslearn { // 设计日志器基类 class Logger { // 公有 public: // 基类指针,用来控制继承子类的对象 using ptr = std::shared_ptr; // 操作方法 // 构造日志消息对象并进行格式化,得到格式化后的日志消息字符串--然后进行落地输出,5个等级 void debug(const std::string &file, size_t line, const std::string &fmt, ...); // 日志的输出操作 void info(const std::string &file, size_t line, const std::string &fmt, ...); // 日志的输出操作 void warn(const std::string &file, size_t line, const std::string &fmt, ...); // 日志的输出操作 void error(const std::string &file, size_t line, const std::string &fmt, ...); // 日志的输出操作 void fatal(const std::string &file, size_t line, const std::string &fmt, ...); // 日志的输出操作 protected: // 日志落地,抽象接口完成实际的落地输出——不同的日志器会有不同的实际落地方式 virtual void log(const char *data, size_t len) = 0; protected: std::mutex _mutex; // 互斥锁 std::string _logger_name; // 日志器的名字 std::atomic _limit_level; // 限制日志等级,atomic原子操作的意思是该操作执行过程中不能被中断,该操作要么不执行,要么全部执行,不存在执行一部分的情况。 Formatter::ptr _formatter; // 控制格式化模块的对象 std::vector _sliks; // 这是一个数组,数组里存放日志落地方式的对象 }; // 派生出同步日志器 class SyncLogger : public Logger { protected: // 重写虚函数 void log(const char *data, size_t len) { } }; }
- 同步日志器的具体实现
#ifndef __M_LOGGER_H__ #define __M_LOGGER_H__ /*日志器模块 1.先设计一个日志器的基类 2.根据基类派生出不同的日志器 */ #define _GNU_SOURCE #include "util.hpp" #include "level.hpp" #include "sink.hpp" #include "format.hpp" #include #include #include #include namespace logslearn { // 设计日志器基类 class Logger { // 公有 public: // 基类指针,用来控制继承子类的对象 using ptr = std::shared_ptr; // 构造函数 Logger(const std::string &logger_name, loglevel::value level, Formatter::ptr &formatter, std::vector &sinks) : _logger_name(logger_name), _limit_level(level), _formatter(formatter), _sliks(sinks.begin(), sinks.end()) {} // 操作方法 //获取日志器名称 const std::string& name(){ return _logger_name; } // 构造日志消息对象并进行格式化,得到格式化后的日志消息字符串--然后进行落地输出,5个等级 void debug(const std::string &file, size_t line, const std::string &fmt, ...) { // 日志的输出操作 // 1.判断当前的日志是否达到输出等级 if (loglevel::value::DEBUG error(__FILE__, __LINE__, "%s", "测试日志"); logger->fatal(__FILE__, __LINE__, "%s", "测试日志"); size_t cursize = 0; size_t count = 0; while (cursize fatal(__FILE__, __LINE__, "测试日志-%d", count++); cursize += 20; } return 0; }
测试结果符合要求(1024102410/20)
1.1.1 局部日志器建造者模式设计
- 局部日志器建造者模式的设计和框架分析。(同步日志器模块)
enum class LoggerType { LOGGER_SYNC, // 同步日志器 LOGGER_ASYNC // 异步日志器 }; // 使用建造者模式来构造日志器,而不要用户直接去构造日志器,简化了用户的使用复杂度 // 1.抽象一个日志器建造者类 // 1.设置日志器类型 // 2.不同类型的日志器的创建放到同一个日志器建造者类中完成。 class LoggerBuilder { public: void buildLoggerType(LoggerType type); void buildLoggerName(std::string &name); void buildLoggerLevel(loglevel::value level); // 构造一个格式化器 void buildLoggerFormatter(const std::string &pattern); // 一个日志器可以有多个不同的落地方式 template void buildSink(Args &&...arg); // 完成我们的日志器构建 virtual void build() = 0; private: // 需要管理的数据,也就是建造的零部件 LoggerType logger_type; // 日志器类型 std::string _logger_name; // 日志器的名字 loglevel::value _limit_level; // 限制日志等级,atomic原子操作的意思是该操作执行过程中不能被中断,该操作要么不执行,要么全部执行,不存在执行一部分的情况。 Formatter::ptr _formatter; // 控制格式化模块的对象 std::vector _sliks; }; // 2.派生出具体的建造者类——局部日志器的建造者 & 全局日志器建造者(后边添加了全局单例管理之后,将日志器添加全局管理) // 局部日志器的建造者 class LocalLoggerBuilder : public LoggerBuilder { public: void build() override; };
- 实现具体操作的各个接口。
enum class LoggerType { LOGGER_SYNC, // 同步日志器 LOGGER_ASYNC // 异步日志器 }; // 使用建造者模式来构造日志器,而不要用户直接去构造日志器,简化了用户的使用复杂度 // 1.抽象一个日志器建造者类 // 1.设置日志器类型 // 2.不同类型的日志器的创建放到同一个日志器建造者类中完成。 class LoggerBuilder { public: // 构造函数 LoggerBuilder() : logger_type(LoggerType::LOGGER_SYNC), _limit_level(logslearn::loglevel::value::DEBUG) {} void buildLoggerType(LoggerType type) { logger_type = type; } void buildLoggerName(const std::string &name) { _logger_name = name; } void buildLoggerLevel(loglevel::value level) { _limit_level = level; } // 构造一个格式化器 void buildLoggerFormatter(const std::string &pattern) { _formatter = std::make_shared(pattern); } // 一个日志器可以有多个不同的落地方式 template void buildSink(Args &&...arg) { LogSink::ptr psink = SinkFactory::create(std::forward(arg)...); // 完美转发 _sliks.push_back(psink); } // 完成我们的日志器构建 virtual Logger::ptr build() = 0; protected: // 需要管理的数据,也就是建造的零部件 LoggerType logger_type; // 日志器类型 std::string _logger_name; // 日志器的名字 loglevel::value _limit_level; // 限制日志等级,atomic原子操作的意思是该操作执行过程中不能被中断,该操作要么不执行,要么全部执行,不存在执行一部分的情况。 Formatter::ptr _formatter; // 控制格式化模块的对象 std::vector _sliks; }; // 2.派生出具体的建造者类——局部日志器的建造者 & 全局日志器建造者(后边添加了全局单例管理之后,将日志器添加全局管理) // 局部日志器的建造者 class LocalLoggerBuilder : public LoggerBuilder { public: Logger::ptr build() override { // 必须要有日志器名称 assert(_logger_name.empty()==false); // 必须要有formatter//必须要有格式化器,没有就要创建 if (_formatter.get() == nullptr) { _formatter = std::make_shared(); } // 如果没有落地方式就给它添加一个标准输出的默认落地方式 if (_sliks.empty()) { buildSink(); } // 如果类型为LOGGER_ASYNC,那么日志器为异步日志器 if (logger_type == LoggerType::LOGGER_ASYNC) { // 目前不做处理,后面在写 } // 返回同步日志器的对象 return std::make_shared(_logger_name, _limit_level, _formatter, _sliks); // r日志器名字,等级,格式化,落地方式 } };
1.1.2 同步日志器基本功能测试
我们这个建造者模式没有指挥者。因为我们构造对象的零部件没有顺序的要求,只要构造就可以了,所有只有建造者。
// 测试代码 #include "util.hpp" #include "level.hpp" #include "message.hpp" #include "format.hpp" #include "sink.hpp" #include "logger.hpp" #include int main() { //同步日志器建造者模式的测试 //先要构造一个建造者出来 std::unique_ptr builder(new logslearn::LocalLoggerBuilder()); //建造者构建零部件 builder->buildLoggerType(logslearn::LoggerType::LOGGER_SYNC); builder->buildLoggerName("sync_logger"); builder->buildLoggerLevel(logslearn::loglevel::value::WARN); builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"); builder->buildSink(); // 标准输出落地 builder->buildSink("./logfile/test.log"); // 文件落地方式 builder->buildSink("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式 //零部件构建好后,用建造者建筑对象 logslearn::Logger::ptr logger=builder->build(); //测试日志打印 logger->debug(__FILE__, __LINE__, "%s", "测试日志"); logger->info(__FILE__, __LINE__, "%s", "测试日志"); logger->warn(__FILE__, __LINE__, "%s", "测试日志"); logger->error(__FILE__, __LINE__, "%s", "测试日志"); logger->fatal(__FILE__, __LINE__, "%s", "测试日志"); size_t cursize = 0; size_t count = 0; while (cursize fatal(__FILE__, __LINE__, "测试日志-%d", count++); cursize += 20; } return 0; }
总结:同步日志器直接将日志消息进行格式化写入文件。
1.2 异步日志器模块设计
思想:为了避免写日志的过程中阻塞,导致影响业务线程的执行效率。异步的思想就是不让业务线程进行日志的实际落地,而是将日志消息放到缓冲区(一块指定的内存)中接下来有一个专门的异步线程,去针对缓冲区中的数据进行处理(实际的落地操作)
实现:
1.实现一个线程安全的缓冲区
2.创建一个异步工作线程,专门用来负责缓冲区中日志信息的落地操作。
缓冲区详情设计:
1.使用队列缓存日志消息,逐条处理
要求:不能涉及空间的频繁申请与释放,否则会降低效率。
结果:设计一个环形队列(提前将空间申请好,然后对空间循环利用)
存在问题:这个缓冲区的操作会涉及到多线程,因此缓冲区的操作必须保证线程安全。
线程安全实现:对于缓冲区的读写加锁
因此写日志操作,在实际开发中,不好分配太多资源,工作线程只需要一个日志器就行
涉及到的锁冲突:生产者与生产者之间的互斥&生产者与消费者的互斥。
问题:锁冲突较为严重,所有线程之间都存在互斥关系
解决方案:双缓冲区
避免了空间频繁的申请与释放,释放与申请的频率,减少了生产者和消费者之间的锁冲突,提高了效率。
1.2.1 单缓冲区设计
设计一个缓冲区:直接存放格式化后的日志消息字符串
好处:
1.减少了LogMsg对象频繁的构造的消耗
2.可以针对缓冲区中的日志消息,一次性进行I0操作,减少I0次数,提高效率
缓冲区类的设计:
1.管理一个存放字符串数据的缓冲区(使用vecotor进行空间管理)
2.当前的写入数据位置的指针(指向可写区域的起始位置,避免数据的写入覆盖)
3.当前的读取数据位置的指针(指向可读数据区域的起始位置,当读取指针与写入指针指向相同位置表示数据取完了)
提供的操作:
1.向缓冲区写入数据
2.获取可读数据起始地址的接口
3.获取可读数据长度的接口
4.移动读写位置的接口
5.初始化缓冲区的操作(将读写位置初始化——将一个缓冲区所有数据处理完毕之后)
6.提供交换缓冲区的接口(交换空间地址,并不交换空间数据)
- 第一步,先设计类,把类的框架搭建出来
/*实现异步日志缓冲区*/ #include "util.hpp" #include namespace logslearn { // 定义宏,表示缓冲区的大小 #define DEFAULT_BUFFER_SIZE (1 * 1024 * 1024) #define THRESHOLD_BUFFER_SIZE (8 * 1024 * 1024) #define INCREMENT_BUFFER_SIZE (1 * 1024 * 1024) // 异步缓冲区 class Buffer { public: // 构造函数 Buffer() {} // 1.向缓冲区写入数据 void push(const char *data, size_t len); // 2.返回可读数据起始地址的接口 const char *begin(); // 3.返回可读数据的长度的接口;返回可写数据的长度的接口 size_t readAbleSize(); size_t writeAbleSize(); // 4.移动读写指针进行向后偏移的接口 void moveWriter(size_t len); void moveReader(size_t len); // 5.重置读写位置,初始化缓冲区的操作 void reset(); // 6.交换缓冲区的接口 void swap( Buffer &buffer); // 判断缓冲区是否为空 bool empty(); private: // 1.存放字符串数据的缓冲区 std::vector _buffer; // 2.当前可写数据的指针--本质是下标 size_t _reader_idx; // 3.当前可读数据的指针 size_t _writer_idx; }; }
- 实现类的各个功能
#ifndef __M_BUFFER_H__ #define __M_BUFFER_H__ /*实现异步日志缓冲区*/ #include "util.hpp" #include #include #include namespace logslearn { // 定义宏,表示缓冲区的大小 #define DEFAULT_BUFFER_SIZE (1 * 1024 * 1024) #define THRESHOLD_BUFFER_SIZE (8 * 1024 * 1024) #define INCREMENT_BUFFER_SIZE (1 * 1024 * 1024) // 异步缓冲区 class Buffer { public: // 构造函数 Buffer() : _buffer(DEFAULT_BUFFER_SIZE), _reader_idx(0), _writer_idx(0) {} // 1.向缓冲区写入数据,容量不够就扩容(两种方式,极限测试的时候使用扩容,实际使用过程中固定空间大小,空间不够阻塞) void push(const char *data, size_t len) { // 缓冲区剩余空间不够的情况下:扩容。 // // 1.固定大小,直接返回 // if (len > writeAbleSize()) // return; // 2.动态空间,用于极限测试--扩容 ensureEnoughSize(len); // 2.将数据拷贝到缓冲区 std::copy(data, data + len, &_buffer[_writer_idx]); // 3.将当前写入位置向后偏移 moveWriter(len); } // 2.返回可读数据起始地址的接口 const char *begin() { return &_buffer[_reader_idx]; } // 3.返回可读数据的长度的接口;返回可写数据的长度的接口 size_t readAbleSize() { // 返回可读数据的长度 // 因为当前实现的缓冲区设计思想是双缓冲区,处理完就交换,所以不存在空间循环使用 return (_writer_idx - _reader_idx); } size_t writeAbleSize() { // 对于扩容的思路来说,不存在可写空间大小,因为总是可写的。 // 因此这个接口只提供给固定大小缓冲区。 return (_buffer.size() - _writer_idx); } // 4.移动读写指针进行向后偏移的接口 void moveWriter(size_t len) { // 移动可写指针 assert((_writer_idx + len) // 移动可读指针 assert(len // 读写位置都为零 _writer_idx = 0; // 缓冲区所有空间都是空闲得 _reader_idx = 0; //_reader_idx与_writer_idx相等就表示没有数据可以读 } // 6.交换缓冲区的接口 void swap(Buffer &buffer) { // 交换缓冲区地址 _buffer.swap(buffer._buffer); // 读写位置的地址也要交换 std::swap(_reader_idx, buffer._reader_idx); std::swap(_writer_idx, buffer._writer_idx); } // 判断缓冲区是否为空 bool empty() { return (_reader_idx == _writer_idx); // 位置一样就是空 } private: // 对空间进行扩容 void ensureEnoughSize(size_t len) { // 不需要扩容 if (len // 小于阈值则翻倍增长 new_size = _buffer.size() * 2; } else { new_size = _buffer.size() + INCREMENT_BUFFER_SIZE+len; // 否则线性增长 } // 重新调整空间大小 _buffer.resize(new_size); } private: // 1.存放字符串数据的缓冲区 std::vector // 异步日志器缓冲区测试 // 读取文件数据,一点一点写入缓冲区,最终将缓冲区数据写入文件,判断生成的新文件与源文件是否一致 std::ifstream ifs("./logfile/test.log", std::ios::binary); // 打开一个文件 if (ifs.is_open() == false) { return -1; } // 文件打开失败返回-1 // 让读写位置跳转到末尾 ifs.seekg(0, std::ios::end); // 获取当前读写位置相对于起始位置的偏移量 size_t fsize = ifs.tellg(); // 重新让指针跳转到起始位置 ifs.seekg(0, std::ios::beg); std::string body; body.resize(fsize); ifs.read(&body[0], fsize); if (ifs.good() == false) { std::cout buffer.push(&body[i], 1); } std::cout // std::cout // readAbleSize()可读数据的长度,可读数据在减少,i在++,双休套进的过程 ofs.write(buffer.begin(), 1); if (ofs.good() == false) { std::cout // 异步工作器类 using Functor = std::function public: using ptr = std::shared_ptr // 异步工作器类 using Functor = std::function ASYNC_SAFE, // 安全状态,表示缓冲区满了就阻塞,避免了资源耗尽的风险 ASUNC_UNSAFE // 非安全状态,不考虑资源耗尽的情况,可以无限扩容,常用与测试 }; class AsyncLooper { public: using ptr = std::shared_ptr} ~AsyncLooper() { stop(); } void stop() { _stop = true; // 将退出标志设置为true _cond_con.notify_all(); // 唤醒所有的工作线程,工作线程只有一个 _thread.join();//等待工作线程退出 } void push(const char *data, size_t len) { // 1.无限扩容-非安全(极限压力测试的情况下使用)2.固定大小 // 加个互斥锁 std::unique_lock return _pro_buf.writeAbleSize() = len; }); // 能够走下来代表满足了条件,可以向缓冲区添加数据 _pro_buf.push(data, len); // 唤醒一个消费者对缓冲区中的数据进行处理 _cond_con.notify_one(); } private: // 线程入口函数--对消费缓冲区中的数据进行处理,处理完毕后,初始化缓冲区,交换缓冲区 void threadEntry() { while (1) { // 要为互斥锁设置一个生命周期,将缓冲区交换完毕后就解锁(并不对数据的处理过程加锁保护) { // 1.判断生产缓冲区里有没有数据,有则交换,无则阻塞 std::unique_lock return !_pro_buf.empty() || _stop; }); //_stop是真表示程序退出,把剩余的数据进行交换 // 等待完毕,消费者与生产者进行地址交换 _con_buf.swap(_pro_buf); // 2.唤醒生产者 if (_looper_type == AsyncType::ASYNC_SAFE) _cond_pro.notify_all(); } // 3.被唤醒后,对消费缓冲区进行数据处理 _callBack(_con_buf); // 4.初始化消费缓冲区 _con_buf.reset(); } } private: Functor _callBack; // 具体对缓冲区数据进行处理的回调函数,由异步工作器使用者传入。 private: // bool _stop;//工作器停止标准,如果工作器停止了,会存在线程安全问题 AsyncType _looper_type; // 默认是安全模式 std::atomic public: AsyncLogger( const std::string &logger_name, loglevel::value level, Formatter::ptr &formatter, std::vector} //将数据写入缓冲区 void log(const char*data,size_t len); //设计一个实际落地函数(将缓冲区里的数据进行落地) void realLog(Buffer &buf); private: AsyncLooper::ptr _looper; }; public: AsyncLogger( const std::string &logger_name, loglevel::value level, Formatter::ptr &formatter, std::vector} //将数据写入缓冲区 void log(const char*data,size_t len){ //消息入队操作,不需要加锁,已经是线程安全得了 _looper-push(data,len); } //设计一个实际落地函数(将缓冲区里的数据进行落地) void realLog(Buffer &buf){ if(_sliks.empty())return ; for(auto &sink:_sliks){ sink-log(buf.begin(),buf.readAbleSize()); } } private: AsyncLooper::ptr _looper; }; LOGGER_SYNC, // 同步日志器 LOGGER_ASYNC // 异步日志器 }; // 使用建造者模式来构造日志器,而不要用户直接去构造日志器,简化了用户的使用复杂度 // 1.抽象一个日志器建造者类 // 1.设置日志器类型 // 2.不同类型的日志器的创建放到同一个日志器建造者类中完成。 class LoggerBuilder { public: // 构造函数 LoggerBuilder() : logger_type(LoggerType::LOGGER_SYNC), _limit_level(logslearn::loglevel::value::DEBUG), _looper_type(AsyncType::ASYNC_SAFE) {} void buildEnabeUnSafeAsync() { _looper_type = AsyncType::ASUNC_UNSAFE; // 非安全模式 } void buildLoggerType(LoggerType type) { logger_type = type; } void buildLoggerName(const std::string &name) { _logger_name = name; } void buildLoggerLevel(loglevel::value level) { _limit_level = level; } // 构造一个格式化器 void buildLoggerFormatter(const std::string &pattern) { _formatter = std::make_shared LogSink::ptr psink = SinkFactory::create public: Logger::ptr build() override { // 必须要有日志器名称 assert(_logger_name.empty() == false); // 必须要有formatter//必须要有格式化器,没有就要创建 if (_formatter.get() == nullptr) { _formatter = std::make_shared buildSink // 返回异步日志器对象 return std::make_shared //异步日志器的测试 //异步日志器和异步工作器进行联调 // //先要构造一个建造者出来 std::unique_ptrbuildSink("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式 //零部件构建好后,用建造者建筑对象 logslearn::Logger::ptr logger=builder->build(); //测试日志打印 logger->debug(__FILE__, __LINE__, "%s", "测试日志"); logger->info(__FILE__, __LINE__, "%s", "测试日志"); logger->warn(__FILE__, __LINE__, "%s", "测试日志"); logger->error(__FILE__, __LINE__, "%s", "测试日志"); logger->fatal(__FILE__, __LINE__, "%s", "测试日志"); size_t count = 0; while (count fatal(__FILE__, __LINE__, "测试日志-%d", count++); } return 0; }