异步日志与系统集成
文章目录
- 异步日志与系统集成
-
- 一、异步日志器设计动机
- 二、缓冲区类实现
- 三、异步线程的实现
- 四、异步日志器具体实现
-
- [同步 vs 异步日志器对比](#同步 vs 异步日志器对比)
- 五、日志器建造者模式
-
- [5.1 日志器建造者基类](#5.1 日志器建造者基类)
- [5.2 局部日志器建造者](#5.2 局部日志器建造者)
- [5.3 日志器管理类](#5.3 日志器管理类)
- [5.4 全局日志器建造者](#5.4 全局日志器建造者)
- 六、日志系统接口优化
- 七、测试
-
- [7.1 一般功能测试](#7.1 一般功能测试)
- [7.2 拓展功能测试](#7.2 拓展功能测试)
- [7.3 性能测试](#7.3 性能测试)
- 八、总结
这是一套 C++ 日志系统学习笔记 ,涵盖同步与异步双模式 日志的设计与实现。核心特点:模块化分层(格式→落地→日志器→建造者接口)、双缓冲区生产者-消费者模型、多种设计模式(单例/工厂/代理/建造者)应用、C++11 多线程与智能指针实践,代码可直接在 Linux 下编译测试。
这是这个项目的第四篇开发笔记,感兴趣的大佬可以看看其他相关的内容
一、异步日志器设计动机
同步日志器在实际使用中存在明显的性能问题。当业务线程直接执行日志写入操作时,如果遇到网络状况不佳、文件系统响应慢或缓冲区满等情况,会导致线程阻塞。这种阻塞在交互式程序中尤其影响用户体验,造成程序卡顿。
异步日志器的核心思想是将日志写入操作与实际日志落地分离。业务线程不再直接执行耗时的日志写入操作,而是将日志消息放入内存缓冲区后立即返回。专门的异步工作线程负责从缓冲区取出日志消息并执行实际的写入操作。这种设计避免了业务线程因日志写入操作阻塞而影响程序响应速度。
异步日志器的缓冲区需要完善的线程安全机制。多个业务线程可能并发写入日志,而异步线程同时读取日志,这形成了典型的多生产者-单消费者模型。必须通过锁机制保证:
- 不同生产者不会同时修改同一缓冲区位置。
- 生产者和消费者不会同时访问同一数据。
互斥锁保护所有缓冲区操作,包括指针移动和数据存取。虽然加锁会带来一定性能开销,但相比直接写入日志的阻塞时间可以忽略。
二、缓冲区类实现
对于日志系统,每一条格式化之后的日志信息的大小是不确定的,因此无法通过日志条数来确定在一个缓冲区中哪一个位置已经存放了日志数据。我们需要使用指针指向已经存放数据的起始位置和未存放数据的起始位置,并在存取数据时移动指针位置。
- 日志缓冲区的设计采用直接存放格式化后的日志消息字符串的方式,而不是存储 LogMessage 对象。这种设计避免了 LogMessage 对象的频繁构造和析构开销。缓冲区中存储的是已经格式化好的日志字符串,每条日志以反斜杠零结尾。
- 使用 vector 进行空间管理而不是 string,因为 string 遇到反斜杠零就会结束处理,而 vector 可以更灵活地管理各种数据。
- 缓冲区需要管理三个关键数据:存放字符串数据的缓冲区本身、指向可写区域起始位置的写入指针,以及指向可读区域起始位置的读取指针。
当读取指针和写入指针指向相同位置时,表示数据已经处理完毕。写入指针会随着数据写入不断向后偏移,而读取指针在处理数据时向后移动直到与写入指针相遇。这种设计支持不定长度的日志消息存储,每条消息写入后写入指针相应偏移,读取时从起始位置开始直到当前写入位置。
双缓冲区思想
在异步日志处理系统中,单条处理日志会导致严重的锁冲突问题。生产者线程和消费者线程之间频繁互斥,导致性能显著下降。
为了解决这个问题,提出了双缓冲区的设计思想。系统需要申请两个缓冲区:任务写入缓冲区(生产缓冲区)和任务处理缓冲区(消费缓冲区)。业务线程将日志写入生产缓冲区,而异步工作线程从消费缓冲区取出数据进行磁盘写入操作。当生产缓冲区数据达到一定量时,两个缓冲区进行交换操作。
swap 接口实现缓冲区内容的快速交换,包括交换底层 vector 管理的存储空间地址(通过 buffer.swap)以及交换读写位置索引(read_idx 和 write_idx)。
cpp
#include "util.hpp"
#include <vector>
#include <iostream>
namespace logsys
{
class LogBuffer
{
#define THRESHOLD_BUFFER_SIZE 10 * 1024 * 1024 // 缓冲区扩容阈值
#define INCREMENT_BUFFER_SIZE 1024 * 1024 // 缓冲区扩容线性增量
public:
LogBuffer() : _buffer(1024 * 1024), _read_idx(0), _write_idx(0) {}
// 将数据写入缓冲区
void Push(const char *data, size_t size)
{
// 判断当前缓冲区的可写长度是否满足要求
if (writeableSize() < size)
{
// 扩容
dilatation(size);
}
// 将数据拷贝到缓冲区
std::copy(data, data + size, &_buffer[_write_idx]);
// 写入指针向后偏移
moveWriter(size);
}
// 返回可写数据长度
size_t writeableSize() const
{
return _buffer.size() - _write_idx;
}
// 返回可读数据长度
size_t readableSize() const
{
return _write_idx - _read_idx;
}
// 可读位置向后移动len长度
bool moveReader(size_t len)
{
if (_read_idx + len > _write_idx)
return false;
_read_idx += len;
return true;
}
// 返回可读位置的起始地址
const char *begin() const
{
return &_buffer[_read_idx];
}
// 重置缓冲区
void reset()
{
_write_idx = 0;
_read_idx = 0;
}
// 对LogBuffer进行交换操作
void swap(LogBuffer &buffer)
{
_buffer.swap(buffer._buffer);
std::swap(_read_idx, buffer._read_idx);
std::swap(_write_idx, buffer._write_idx);
}
// 判断缓冲区是否为空
bool empty()
{
return _read_idx == _write_idx;
}
private:
// 扩容
void dilatation(size_t size)
{
if (size <= writeableSize())
return;
size_t newsize = 0;
while (newsize < size)
{
if (_buffer.size() < THRESHOLD_BUFFER_SIZE)
newsize = _buffer.size() * 2;
else
newsize = _buffer.size() + INCREMENT_BUFFER_SIZE;
}
_buffer.resize(newsize);
}
// 可写位置向后移动len长度
bool moveWriter(size_t len)
{
if (_write_idx + len > _buffer.size())
return false;
_write_idx += len;
return true;
}
private:
std::vector<char> _buffer;
size_t _read_idx;
size_t _write_idx;
};
}
缓冲区核心操作说明
| 操作 | 说明 |
|---|---|
Push(data, size) |
将数据写入缓冲区,空间不足时自动扩容 |
readableSize() |
返回可读数据长度(_write_idx - _read_idx) |
writeableSize() |
返回可写空间长度(_buffer.size() - _write_idx) |
begin() |
返回可读数据的起始地址 |
reset() |
重置读写指针,清空缓冲区 |
swap(other) |
与另一个缓冲区交换底层数据和指针 |
empty() |
判断缓冲区是否为空 |
三、异步线程的实现
异步工作线程的设计基于双缓冲区思想,包含生产缓冲区和消费缓冲区。外界将任务数据添加到生产缓冲区,异步线程处理消费缓冲区的数据。当消费缓冲区数据处理完毕后,若生产缓冲区有数据则交换缓冲区,否则线程进入休眠等待唤醒。
- 异步工作器管理的成员包括双缓冲区(生产缓冲区和消费缓冲区)、互斥锁(保证线程安全)、两个条件变量(生产者和消费者等待队列)、回调函数(处理缓冲区数据的接口)以及停止标志和工作线程。
- 提供的操作包括停止异步工作器和添加数据到缓冲区(线程安全操作)。私有操作包括线程入口函数,该函数负责交换缓冲区并使用回调函数处理消费缓冲区数据。
- 异步工作器设计遵循高内聚低耦合原则,回调函数由使用者传入,告知工作器如何处理数据。异步工作器不关心具体处理方式,只需调用传入的函数处理数据即可。
- 由于
_isRunning变量可能被多个线程访问,存在线程安全问题,因此将其改为原子类型atomic<bool>。 - 实现
push时需要先加锁,使用unique_lock管理互斥锁,然后通过条件变量判断能否添加数据。条件为缓冲区剩余空间大于数据长度时返回 true 可以添加,否则阻塞等待。被唤醒后满足条件则调用_pro_buf.Push(data, size)添加数据,完成后唤醒消费者线程处理数据,因为消费者可能在处理完数据后因生产缓冲区无数据而休眠,添加数据后需要唤醒它。
cpp
#include <mutex>
#include <condition_variable>
#include <thread>
#include <functional>
#include <memory>
#include <atomic>
#include "logBuffer.hpp"
namespace logsys
{
using Function = std::function<void(LogBuffer &)>;
enum LOOPER_TYPE
{
LOOPER_EXPAND, // 当生产缓冲区空间不足时,进行扩容
LOOPER_BLOCKING // 当生产缓冲区空间不足时,生产者阻塞等待
};
class AsyncLooper
{
public:
using Ptr = std::shared_ptr<AsyncLooper>;
AsyncLooper(const Function &callback, LOOPER_TYPE type = LOOPER_TYPE::LOOPER_EXPAND)
: _isRunning(true), _thread(std::thread(&AsyncLooper::threadEnter, this)),
_callback(callback), _looper_type(type)
{
}
~AsyncLooper()
{
setRunning(false);
}
// 设置当前工作状态
void setRunning(bool isRunning)
{
_isRunning = isRunning;
_con_cond.notify_all();
_thread.join();
}
// 获取当前工作状态
bool isRunning() const
{
return _isRunning;
}
// 向生产缓冲区中push数据
void push(const char *data, size_t size)
{
// 加锁
std::unique_lock<std::mutex> lock(_mutex);
if (_looper_type == LOOPER_TYPE::LOOPER_BLOCKING)
{
// 如果运行模式是阻塞,则需要判断当前生产缓冲区的可写长度是否满足要求
_pro_cond.wait(lock, [&] {
return _pro_buf.writeableSize() >= size;
});
}
// 将数据加入生产缓冲区
_pro_buf.Push(data, size);
// 唤醒消费者缓冲区进行处理
_con_cond.notify_one();
}
private:
// 线程入口函数,对消费缓冲区中的数据进行处理
void threadEnter()
{
while (1)
{
{
// 加锁
std::unique_lock<std::mutex> lock(_mutex);
// 判断是否要唤醒工作
_con_cond.wait(lock, [&] {
// 如果是停止状态,或者生产缓冲区中有数据则往后执行
return !_isRunning || !_pro_buf.empty();
});
// 如果是停止状态,并且生产缓冲区中没有数据了,则退出循环
if (!_isRunning && _pro_buf.empty())
{
break;
}
// 交换缓冲区
_con_buf.swap(_pro_buf);
}
if(_looper_type == LOOPER_TYPE::LOOPER_BLOCKING)
{
// 唤醒生产者线程进行生产
_pro_cond.notify_all();
}
// 进行数据处理
_callback(_con_buf);
// 重置缓冲区
_con_buf.reset();
}
}
private:
LOOPER_TYPE _looper_type; // 运行策略
std::atomic<bool> _isRunning; // 是否正在运行
LogBuffer _pro_buf; // 生产缓冲区
LogBuffer _con_buf; // 消费缓冲区
std::mutex _mutex; // 锁
std::condition_variable _pro_cond; // 生产者条件变量
std::condition_variable _con_cond; // 消费者条件变量
std::thread _thread; // 线程
Function _callback; // 回调函数
};
}
异步工作线程的工作流程
线程入口函数 threadEnter 是工作线程的核心,负责处理消费缓冲区中的数据。入口函数采用循环处理,循环条件为 _isRunning 不为 true,否则退出循环程序结束。处理数据时首先判断生产缓冲区是否有数据,有则交换到消费缓冲区,无则等待。
整个工作流程如下:
- 加锁等待:线程尝试获取互斥锁,然后检查两个唤醒条件------程序是否停止、生产缓冲区是否有数据。如果都没有,线程在条件变量上阻塞等待。
- 检查退出条件:如果程序已停止且生产缓冲区没有剩余数据,线程正常退出循环。
- 交换缓冲区:将生产缓冲区的内容快速交换到消费缓冲区,这个操作非常高效(仅交换指针)。
- 唤醒生产者:如果是阻塞模式,唤醒所有可能因缓冲区满而阻塞的生产者线程。
- 处理数据:在锁外调用回调函数处理消费缓冲区中的数据,这样做的好处是处理数据时不占用锁,生产者线程可以继续向生产缓冲区写入数据。
- 重置缓冲区:数据处理完毕后重置消费缓冲区,准备下一次使用。
两种运行策略
| 策略 | 生产缓冲区满时的行为 |
|---|---|
LOOPER_EXPAND |
自动扩容,生产者不会阻塞 |
LOOPER_BLOCKING |
生产者阻塞等待,直到消费缓冲区处理完毕并交换 |
四、异步日志器具体实现
异步日志器类名为 AsyncLogger,继承自 Logger 日志器类。
- 需要重写日志落地方法
log,改为将数据写入缓冲区。 - 类中定义一个私有成员变量
AsyncLooper::Ptr _looper,使用智能指针进行管理。 - 需要实现一个实际落地函数
realLog,该函数作为异步线程的回调函数,负责将缓冲区中的数据实际写入文件。 - 构造函数需要额外初始化异步工作器的处理模式。
- 在异步日志器中重写日志写入操作,使用
AsyncLooper的push方法将数据和长度传入进行消息入队操作。这个操作本身是线程安全的,不需要额外加锁。
cpp
class AsyncLogger : public Logger
{
public:
AsyncLogger(const std::string &name,
LogLevel::VALUE limitLvl,
Formater::Ptr &formater,
std::vector<LogSink::Ptr> &sinkPtrs,
LOOPER_TYPE looper_type = LOOPER_TYPE::LOOPER_EXPAND)
: Logger(name, limitLvl, formater, sinkPtrs),
_looper(std::make_shared<AsyncLooper>(
std::bind(&AsyncLogger::realLog, this, std::placeholders::_1),
looper_type))
{
}
protected:
void log(const char *data, size_t size) override
{
_looper->push(data, size);
}
void realLog(LogBuffer &buffer)
{
for (auto &s : _sinkPtrs)
{
s->log(buffer.begin(), buffer.readableSize());
}
}
private:
AsyncLooper::Ptr _looper;
};
同步 vs 异步日志器对比
| 特性 | 同步日志器 | 异步日志器 |
|---|---|---|
| 落地方式 | 业务线程直接写入 | 放入缓冲区,后台线程写入 |
| 对业务线程的影响 | 可能阻塞 | 几乎不阻塞 |
| 线程安全 | 加锁保护 | 缓冲区操作加锁 |
| 适用场景 | 低并发、快速 I/O | 高并发、慢速 I/O |
| 实现复杂度 | 简单 | 中等 |
五、日志器建造者模式
在最初测试时发现,构造日志器需要创建大量参数和零部件,过程非常繁琐,增加了用户的使用复杂度。用户需要手动构造各种零部件,这无疑太麻烦了。
基于这个问题,提出了使用建造者模式来构造日志器的思想。建造者模式适用于对象构建过于复杂、输入参数过多的情况,它通过先建造各个零部件,再用这些零部件构造复杂对象。
设计建造者类时,首先需要针对具体对象有一个建造者抽象类,然后针对具体对象派生出具体的建造者类,通过指挥者类来指挥对象的建造。但在日志器的建造中,参数顺序没有太大要求,主要是参数过多且每个参数都需要构造,因此可以简化建造者模式,不需要指挥者,直接通过建造者对象来构造对象。
日志建造者模式也可以分为两种,一种是局部建造者,由这个建造者构建的日志器只能在当前作用域使用;另一种是全局建造者,由这个建造者构建的日志器可以在任意包含日志相关头文件的地方使用。
5.1 日志器建造者基类
日志器类型分为同步日志器和异步日志器两种,通过枚举类型 LOGGER_TYPE 来区分。
对外提供的接口包括:
bulidLoggerType:设置日志器类型。bulidLoggerName:设置日志器名称。bulidLoggerLimitLvl:设置日志等级。bulidFormater:设置日志输出格式规则。- 对于落地方向,由于一个日志器可能存在多个落地器,因此提供了模板函数
addSink来创建不同类型的落地器,这些落地器会被添加到 vector 中。 - 最后提供了一个纯虚函数
build用于完成日志器的创建,具体的实现由派生类完成。
cpp
namespace logsys
{
enum LOGGER_TYPE
{
LOGGER_SYNC,
LOGGER_ASYNC
};
class LoggerBuilder
{
public:
LoggerBuilder()
: _logger_type(LOGGER_SYNC), _logger_limitLvl(LogLevel::VALUE::DEBUG),
_async_looper_type(LOOPER_TYPE::LOOPER_BLOCKING)
{
}
void bulidLoggerType(LOGGER_TYPE type) { _logger_type = type; }
void bulidLoggerName(const std::string &name) { _logger_name = name; }
void bulidLoggerLimitLvl(LogLevel::VALUE lvl) { _logger_limitLvl = lvl; }
void bulidFormater(const std::string &fmt) { _formater = std::make_shared<Formater>(fmt); }
void bulidAsyncType(LOOPER_TYPE type) { _async_looper_type = type; }
template <typename SinkType, typename... Args>
void addSink(Args &&...args)
{
_sinkPtrs.push_back(SinkFactory::creat<SinkType>(std::forward<Args>(args)...));
}
virtual Logger::Ptr bulid() = 0;
protected:
LOGGER_TYPE _logger_type;
std::string _logger_name;
LogLevel::VALUE _logger_limitLvl;
std::vector<LogSink::Ptr> _sinkPtrs;
Formater::Ptr _formater;
LOOPER_TYPE _async_looper_type;
};
}
5.2 局部日志器建造者
局部日志器建造者直接创建日志器对象,而不需要处理全局管理的问题。
cpp
namespace logsys
{
class LocalLoggerBuilder : public LoggerBuilder
{
public:
Logger::Ptr bulid() override
{
assert(!_logger_name.empty()); // 必须设置日志器名称
if (_formater.get() == nullptr)
{
_formater = std::make_shared<Formater>();
}
if (_sinkPtrs.empty())
{
_sinkPtrs.push_back(SinkFactory::creat<StdoutSink>());
}
if (_logger_type == LOGGER_TYPE::LOGGER_ASYNC)
{
return std::make_shared<AsyncLogger>(
_logger_name, _logger_limitLvl, _formater, _sinkPtrs, _async_looper_type);
}
return std::make_shared<SyncLogger>(
_logger_name, _logger_limitLvl, _formater, _sinkPtrs);
}
};
}
5.3 日志器管理类
日志器管理器模块的设计目的是为了突破日志器作用域的限制,使日志器可以在项目的任意位置进行输出。当前创建的日志器会受到作用域访问属性的限制,只能在创建它的作用域内使用,传递到其他作用域可能导致释放问题且过程繁琐。
为了解决这个问题,设计了一个日志器管理类,该类采用单例模式,确保在任何时刻和位置获取的都是同一个对象。通过单例对象可以获取到管理的日志器进行日志输出,从而突破作用域限制。
此外,还扩展了功能,在单例管理器创建时默认先创建一个日志器用于标准输出打印,方便用户在不创建任何日志器的情况下也能进行标准输出打印。
类的名称为 LoggerManager,管理的成员包括互斥锁、默认日志器和日志器数组。向外提供的操作接口包括添加日志器、判断是否包含指定名称的日志器、获取指定名称的日志器和获取默认日志器。
cpp
namespace logsys
{
// 日志器管理类
class LoggerManager
{
public:
// 获取单例
static LoggerManager &getInstance()
{
static LoggerManager _instance;
return _instance;
}
// 添加日志器
void addLogger(Logger::Ptr &logger)
{
if (hasLogger(logger->getName()))
{
std::cout << "日志器已存在\n";
return;
}
std::unique_lock<std::mutex> lock(_mutex);
_loggers[logger->getName()] = logger;
}
// 判断指定名称的日志器是否存在
bool hasLogger(const std::string &name)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _loggers.find(name);
return it != _loggers.end();
}
Logger::Ptr getLoggerPtr(const std::string &name = "")
{
if (name == "")
{
return _root_logger; // 返回默认日志器
}
std::unique_lock<std::mutex> lock(_mutex);
auto it = _loggers.find(name);
if (it == _loggers.end())
{
return Logger::Ptr();
}
return it->second;
}
private:
// 设计为单例
LoggerManager()
{
// 这里不能使用全局builder,会导致死循环
std::unique_ptr<LocalLoggerBuilder> builder =
std::make_unique<LocalLoggerBuilder>();
builder->bulidLoggerName("root");
_root_logger = builder->bulid();
addLogger(_root_logger);
}
LoggerManager(LoggerManager &) = delete;
LoggerManager &operator=(const LoggerManager &) = delete;
private:
std::mutex _mutex;
Logger::Ptr _root_logger; // 默认日志器
std::unordered_map<std::string, Logger::Ptr> _loggers; // 被管理的日志器
};
}
5.4 全局日志器建造者
全局建造者构建的日志器不仅创建日志器对象,还会将日志器注册到全局的 LoggerManager 中,使得在项目的任何地方都可以通过日志器名称获取到该日志器进行日志输出。
需要注意的是,在日志器管理类 LoggerManager 的构造函数中不能使用全局建造者,因为这会导致死循环:创建单例对象时需要创建 root 日志器,而创建 root 日志器时又会尝试将未完全构造的单例对象添加到自身中,造成程序卡死。因此 root 日志器必须使用局部建造者创建。
cpp
namespace logsys
{
class GlobalLoggerBuilder : public LoggerBuilder
{
public:
Logger::Ptr bulid() override
{
assert(!_logger_name.empty()); // 必须设置日志器名称
if (_formater.get() == nullptr)
{
_formater = std::make_shared<Formater>();
}
if (_sinkPtrs.empty())
{
_sinkPtrs.push_back(SinkFactory::creat<StdoutSink>());
}
Logger::Ptr logger;
if (_logger_type == LOGGER_TYPE::LOGGER_ASYNC)
{
logger = std::make_shared<AsyncLogger>(
_logger_name, _logger_limitLvl, _formater, _sinkPtrs, _async_looper_type);
}
else
{
logger = std::make_shared<SyncLogger>(
_logger_name, _logger_limitLvl, _formater, _sinkPtrs);
}
LoggerManager::getInstance().addLogger(logger);
return logger;
}
};
}
六、日志系统接口优化
虽然日志系统功能已完成,但使用体验仍需优化。主要问题包括:
- 用户需要手动输入文件名和行号,增加了使用难度。
- 获取日志器需要写冗长的单例对象调用代码。
计划通过 logsys.h 头文件提供更友好的接口:
- 提供获取指定日志器的全局函数,隐藏单例实现细节。
- 使用宏函数代理日志器接口,自动填充文件名和行号。
- 提供直接进行日志打印的宏函数,默认使用预设日志器,进一步简化操作。
cpp
#include "logger.hpp"
namespace logsys
{
// 获取指定名称的日志器
Logger::Ptr getLogger(const std::string &name)
{
return LoggerManager::getInstance().getLoggerPtr(name);
}
// 获取默认日志器
Logger::Ptr getRootLogger()
{
return LoggerManager::getInstance().getLoggerPtr();
}
// 日志器接口代理(自动填充行号和文件名)
#define debug(fmt, ...) debug(__LINE__, __FILE__, fmt, ##__VA_ARGS__)
#define info(fmt, ...) info(__LINE__, __FILE__, fmt, ##__VA_ARGS__)
#define warning(fmt, ...) warning(__LINE__, __FILE__, fmt, ##__VA_ARGS__)
#define error(fmt, ...) error(__LINE__, __FILE__, fmt, ##__VA_ARGS__)
#define fatal(fmt, ...) fatal(__LINE__, __FILE__, fmt, ##__VA_ARGS__)
// 默认日志器接口代理(直接使用默认日志器)
#define DEBUG(fmt, ...) getRootLogger()->debug(fmt, ##__VA_ARGS__)
#define INFO(fmt, ...) getRootLogger()->info(fmt, ##__VA_ARGS__)
#define WARNING(fmt, ...) getRootLogger()->warning(fmt, ##__VA_ARGS__)
#define ERROR(fmt, ...) getRootLogger()->error(fmt, ##__VA_ARGS__)
#define FATAL(fmt, ...) getRootLogger()->fatal(fmt, ##__VA_ARGS__)
}
这些宏的使用非常直观:
- 小写宏 (如
debug、info):用于具体的日志器实例,前面需要先获取日志器对象。宏会自动填充当前的行号和文件名。 - 大写宏 (如
DEBUG、INFO):直接使用默认的 root 日志器进行输出,无需手动获取日志器对象,最为简便。
例如,使用大写宏只需要一行代码即可完成日志输出:
cpp
INFO("服务器启动成功,监听端口:%d", port);
ERROR("数据库连接失败,错误码:%d", errno);
七、测试
7.1 一般功能测试
在初始化日志器时只需要指定日志器的名称即可建造日志器,其他值未设置时使用缺省值。
cpp
#include <iostream>
#include <ctime>
#include <thread>
#include <unistd.h>
#include <fstream>
#include "../logs/logsys.h"
// 用户日志器测试
void test_Logger(const std::string &name)
{
logsys::Logger::Ptr logger = logsys::getLogger(name);
size_t count = 0;
size_t startTime = logsys::util::Date::getTime();
while (count < 100000)
{
logger->warning("测试日志-%lld", count++);
}
std::cout << "主线程执行时间:"
<< logsys::util::Date::getTime() - startTime << " s" << std::endl;
}
// 默认日志器测试
void test_rootLogger()
{
size_t count = 0;
size_t startTime = logsys::util::Date::getTime();
while (count < 100000)
{
WARNING("测试日志-%lld", count++);
}
std::cout << "主线程执行时间:"
<< logsys::util::Date::getTime() - startTime << " s" << std::endl;
}
int main()
{
std::unique_ptr<logsys::GlobalLoggerBuilder> bulider =
std::make_unique<logsys::GlobalLoggerBuilder>();
bulider->bulidLoggerName("test_logger");
bulider->bulidLoggerType(logsys::LOGGER_TYPE::LOGGER_ASYNC);
bulider->bulidLoggerLimitLvl(logsys::LogLevel::VALUE::INFO);
bulider->bulidFormater("[%d{%Y-%m-%d %H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");
bulider->bulidAsyncType(logsys::LOOPER_TYPE::LOOPER_EXPAND);
bulider->addSink<logsys::StdoutSink>();
bulider->addSink<logsys::ScrollSinkBySize>("./log/scrolllog", 1024 * 1024);
logsys::Logger::Ptr syncLogger = bulider->bulid();
test_Logger("test_logger");
return 0;
}
7.2 拓展功能测试
在编写日志落地模块时,我们考虑后续可能还有不同的落地方设置,比如之前我们根据文件的大小来设置日志文件滚动的条件,此外可以根据时间划分日志文件的滚动方式。拓展这个功能需要继承日志落地基类,并在子类中重写 log 函数,之后将这个方法在构建日志器时通过 addSink 添加进去,即可使用拓展的落地方向。
下面是一个以时间间隔划分日志文件的扩展示例:
cpp
#include <iostream>
#include <ctime>
#include <thread>
#include <unistd.h>
#include <fstream>
#include "../logs/logsys.h"
// 扩展:以时间划分文件
class ScrollSinkByTime : public logsys::LogSink
{
public:
ScrollSinkByTime(const std::string &basename, size_t gap)
: _baseName(basename), _gap(gap)
{
_curFiletime = logsys::util::Date::getTime();
std::string fileName = buildNewFileName();
logsys::util::File::creatDirectory(
logsys::util::File::getFilePath(fileName));
_ofs.open(fileName, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
~ScrollSinkByTime()
{
if (_ofs.is_open())
{
_ofs.close();
}
}
void log(const char *data, size_t size) override
{
time_t curTime = logsys::util::Date::getTime();
if (curTime / _gap != _curFiletime / _gap)
{
_ofs.close();
std::string fileName = buildNewFileName();
_ofs.open(fileName, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
_curFiletime = curTime;
}
_ofs.write(data, size);
if (_ofs.good() == false)
{
std::cout << "日志输出失败\n";
}
}
private:
const std::string buildNewFileName()
{
struct tm t;
time_t time = logsys::util::Date::getTime();
localtime_r(&time, &t);
std::stringstream ss;
ss << _baseName << t.tm_year + 1900 << "_" << t.tm_mon + 1 << "_"
<< t.tm_mday << "_" << t.tm_hour << t.tm_min << t.tm_sec << ".log";
return ss.str();
}
private:
std::string _baseName;
std::ofstream _ofs;
size_t _gap;
size_t _curFiletime;
};
void test(const std::string &name)
{
logsys::Logger::Ptr logger = logsys::getLogger(name);
size_t count = 0;
size_t startTime = logsys::util::Date::getTime();
while (logsys::util::Date::getTime() < startTime + 10)
{
logger->info("测试日志-%lld", count++);
}
std::cout << "日志输出时间:"
<< logsys::util::Date::getTime() - startTime << " s" << std::endl;
}
int main()
{
std::unique_ptr<logsys::GlobalLoggerBuilder> bulider =
std::make_unique<logsys::GlobalLoggerBuilder>();
bulider->bulidLoggerName("expand_logger");
bulider->bulidLoggerType(logsys::LOGGER_TYPE::LOGGER_ASYNC);
bulider->bulidLoggerLimitLvl(logsys::LogLevel::VALUE::INFO);
bulider->bulidFormater("[%d{%Y-%m-%d %H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");
bulider->bulidAsyncType(logsys::LOOPER_TYPE::LOOPER_EXPAND);
bulider->addSink<ScrollSinkByTime>("./log/timelog", 2);
logsys::Logger::Ptr syncLogger = bulider->bulid();
test("expand_logger");
return 0;
}
7.3 性能测试
性能测试用于对比同步日志器和异步日志器在不同并发条件下的表现。测试指标包括总耗时、每秒输出日志数量和每秒输出数据量。
cpp
#include "../logs/logsys.h"
#include <chrono>
#include <ctime>
#include <ratio>
void bench(const std::string &logger_name, size_t msg_count,
size_t msg_size, size_t thread_count)
{
logsys::Logger::Ptr logger =
logsys::LoggerManager::getInstance().getLoggerPtr(logger_name);
if (logger.get() == nullptr)
{
std::cout << "未找到名为\"" << logger_name << "\"的线程\n";
return;
}
std::string msg(msg_size, 'x');
std::vector<std::thread> threads;
std::vector<double> time;
size_t thread_msg_count = msg_count / thread_count;
for(int i = 0; i < thread_count; ++i)
{
threads.emplace_back([&, i]{
auto start = std::chrono::high_resolution_clock::now();
for(int j = 0; j < thread_msg_count; ++j)
{
logger->fatal("%s", msg);
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> time_span =
std::chrono::duration_cast<std::chrono::duration<double>>(end - start);
time.push_back(time_span.count());
std::cout << "线程" << i << " 输出日志:" << thread_msg_count
<< "条, 耗时:" << time_span.count() << "s\n";
});
}
for(auto& t : threads) t.join();
double total_time = time[0];
for(auto& t : time) total_time = std::max(total_time, t);
double msg_speed = msg_count / total_time;
double data_speed = msg_count * msg_size / total_time / 1024 / 1024;
std::cout << "日志器: " << logger_name
<< " 在" << thread_count << "线程并发写入"
<< msg_count << "条日志, 每条日志大小为" << msg_size << "字节\t";
std::cout << "总耗时:" << total_time << "s, 每秒输出日志数量:"
<< msg_speed << "条/s, 每秒输出数据量:" << data_speed << "MB/s\n\n";
}
int main()
{
std::unique_ptr<logsys::GlobalLoggerBuilder> bulider =
std::make_unique<logsys::GlobalLoggerBuilder>();
// 配置同步日志器
bulider->bulidLoggerName("sync_logger");
bulider->bulidLoggerType(logsys::LOGGER_TYPE::LOGGER_SYNC);
bulider->bulidLoggerLimitLvl(logsys::LogLevel::VALUE::INFO);
bulider->bulidFormater("[%d{%Y-%m-%d %H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");
bulider->addSink<logsys::ScrollSinkBySize>("./log/sync/scrolllog", 1024 * 1024);
bulider->bulid();
// 测试同步日志器
bench("sync_logger", 1000000, 100, 1);
bench("sync_logger", 1000000, 100, 5);
// 配置异步日志器
bulider->bulidLoggerName("async_logger");
bulider->bulidLoggerType(logsys::LOGGER_TYPE::LOGGER_ASYNC);
bulider->addSink<logsys::ScrollSinkBySize>("./log/async/scrolllog", 1024 * 1024);
bulider->bulid();
// 测试异步日志器
bench("async_logger", 1000000, 100, 1);
bench("async_logger", 1000000, 100, 5);
return 0;
}
通过性能测试,可以直观地对比同步日志器和异步日志器在不同并发条件下的性能差异。通常情况下,异步日志器在高并发场景下表现更优,因为业务线程只需将日志放入内存缓冲区即可立即返回,实际的 I/O 操作由后台线程异步完成。而同步日志器在每次写入时都需要等待 I/O 操作完成,在高并发场景下容易成为性能瓶颈。
八、总结
本文实现了异步日志器的完整设计:通过双缓冲区思想解决了多线程环境下的锁竞争问题,利用生产-消费者模型实现了日志的异步落地,大幅提升了高并发场景下的日志写入性能。同时,建造者模式简化了日志器的构造过程,单例的日志器管理器使得日志器可以在项目的任何位置被获取和使用。最后,通过宏代理的方式为用户提供了简洁友好的日志输出接口。
至此,一个完整的 C++ 同步异步日志系统就全部实现了。这个项目涵盖了设计模式、多线程编程、双缓冲区设计、工厂模式、建造者模式、单例模式等多种核心技术,是一个综合性很强的 C++ 实战项目。