设计思路
架构设计
核心模块作用
- Logger: 核心模块,负责调用其他模块以实现日志的记录、格式化和输出
- Formatter: 负责日志的格式化,按照用户定义的模板生成最终的日志内容
- Sink: 定义日志输出目的地,输出到显示器、指定文件、滚动文件
- Looper: 管理异步任务调度,实现日志的异步写入
- Queue: 提供线程安全的日志消息队列,用于在多线程环境下传递日志
- Buffer: 管理日志采用双缓冲区设计,减少内存分配和释放的开销
设计模式应用
项目职工
单例模式
日志器作为日志系统的核心,主要有同步日志器和异步日志器,负责记录所有的日志信息。为了确保系统中所有模块都可以访问同一个日志器实例,并避免多次实例化带来的性能开销,所以选择单例模式。通过单例模式,保证系统中只有一个日志器实例,从而保证了日志记录的一致性。
//代码简略说明(下同)
namespace maglog {
classLogger {
public:
// 获取唯一实例的方法static Logger& GetInstance() {
static Logger instance; // 静态局部变量,保证实例的唯一性
return instance;
}
// 其他成员函数...private:
// 构造函数私有化,禁止外部创建实例Logger() {}
// 禁止拷贝和赋值Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
};
}
单例模式对项目的好处
- 唯一性: 确保了整个系统中只有一个日志器实例,避免了多实例可能导致的日志混乱
- 全局访问点: 提供了一个全局的访问点,方便系统中各个模块使用同一个日志器实例
- 资源节约: 通过单例模式,避免了重复实例化带来的内存和资源浪费
工厂模式
日志系统需要支持多种的日志输出方式,例如支持的屏幕输出、指定文件输出、滚动输出等。每种输出都需要创建对应的sink实例,为了让日志系统能够灵活的创建不同类型的sink,实现将日志输出到不同位置。所以使用工厂模式,可以在运行的时候根据需要创建不同的Sink实例,而不需要修改Logger代码。
namespace maglog {
classSink {
public:
virtualvoidWrite(const std::string& message)= 0;
virtual ~Sink() = default;
};
classFileSink : public Sink
{
public:
explicitFileSink(const std::string& filename) : file_(filename, std::ios::out | std::ios::app) {}
voidWrite(const std::string& message)override{
file_ << message << std::endl;
}
private:
std::ofstream file_;
};
// 工厂方法,根据传入参数创建不同的Sink实例
std::unique_ptr<Sink> CreateSink(const std::string& type, const std::string& target)
{
if (type == "file") {
return std::make_unique<FileSink>(target);
} elseif (type == "network") {
return std::make_unique<NetworkSink>(target, 8080);
}
return std::make_unique<ConsoleSink>();
}
}
工厂模式优点
- 灵活性: 通过工厂模式,系统可以根据不同的需求创建相应的
Sink
实例,增强了日志输出的灵活性- 开闭原则: 新的
Sink
类型可以通过扩展工厂方法来添加,而无需修改现有的Logger
代码,符合开闭原则- 降低耦合: 将
Sink
的创建逻辑与Logger
解耦,简化了Logger
的实现,使其更加专注于日志记录的核心功能
建造者模式
同步日志器和异步日志器的实现则是借助建造者模式进行实现。主要通过设计Logger基类,创建同步日志器和异步日志器,然后分别创建两个日志器建造者专门负责日志器的设置和建造,最后通过日志器管理模块,对创建的日志器进行统一的管理。
高性能日志系统 日志器模块-CSDN博客(具体实现和分析参考本篇文章)
代理模式
目的是对日志的输出行为进行控制,例如需要对日志进行过滤、延迟输出等,通过代理模式实现了不修改Sink实现的情况下,灵活的增加了对日志输出行为的控制。
namespace maglog {
classSink {
public:
virtualvoidWrite(const std::string& message)= 0;
virtual ~Sink() = default;
};
// 日志输出的代理类classSinkProxy : public Sink {
public:
SinkProxy(std::unique_ptr<Sink> real_sink) : real_sink_(std::move(real_sink)) {}
voidWrite(const std::string& message)override{
// 在实际写入前进行额外操作(如日志过滤、缓存等)
if (ShouldWrite(message)) {
real_sink_->Write(message);
}
}
private:
boolShouldWrite(const std::string& message){
// 过滤逻辑(示例:只写入INFO级别日志)return message.find("INFO") != std::string::npos;
}
std::unique_ptr<Sink> real_sink_;
};
}
同时使用代理模式实现对顶层调用的三重封装,让调用日志更加的方便,不必要关闭底层的具体代码实现。
#define debug(fmt, ...) debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define info(fmt, ...) info(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define warn(fmt, ...) warn(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define error(fmt, ...) error(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define fatal(fmt, ...) fatal(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_DEBUG(logger, fmt, ...) (logger)->debug(fmt, ##__VA_ARGS__)
#define LOG_INFO(logger, fmt, ...) (logger)->info(fmt, ##__VA_ARGS__)
#define LOG_WARN(logger, fmt, ...) (logger)->warn(fmt, ##__VA_ARGS__)
#define LOG_ERROR(logger, fmt, ...) (logger)->error(fmt, ##__VA_ARGS__)
#define LOG_FATAL(logger, fmt, ...) (logger)->fatal(fmt, ##__VA_ARGS__)
#define LOGD(fmt, ...) LOG_DEBUG(maglog::rootLogger(), fmt, ##__VA_ARGS__)
#define LOGI(fmt, ...) LOG_INFO(maglog::rootLogger(), fmt, ##__VA_ARGS__)
#define LOGW(fmt, ...) LOG_WARN(maglog::rootLogger(), fmt, ##__VA_ARGS__)
#define LOGE(fmt, ...) LOG_ERROR(maglog::rootLogger(), fmt, ##__VA_ARGS__)
#define LOGF(fmt, ...) LOG_FATAL(maglog::rootLogger(), fmt, ##__VA_ARGS__
代理模式的优点
- 增强功能: 代理模式允许我们在不改变现有
Sink
代码的情况下,添加额外的日志处理功能,如过滤、缓存等- 灵活控制: 可以在代理中动态调整日志输出行为,如根据条件选择性输出日志,提升系统的灵活性
- 分离职责: 通过代理模式,将日志输出的核心逻辑与增强功能分离,使代码更加清晰和易于维护
异步处理设计
异步日志器使用原因
- 主线程阻塞: 日志写入通常涉及I/O操作,如写入文件或发送网络请求。如果这些操作在主线程中执行,可能会导致线程长时间阻塞,降低系统整体性能
- I/O瓶颈: 当大量日志写入请求同时发生时,I/O操作容易成为系统的瓶颈,进一步拖慢主线程的处理速度
- 并发竞争: 多个线程同时尝试写入日志时,可能会导致锁竞争和资源争用,影响系统的并发性
日志器主要就是将日志写入操作与主线程进行解耦,通过任务调度和队列实现机制,从而实现日志的异步处理,这样主线程就不会阻塞,大大提高了系统的并发能力和响应速度。
异步日志器设计思路
核心思想就是将日志最后写入和记录的操作分离开,也就是让另一个线程去做。主线程只负责将日志消息放入一个线程安全的队列中,然后立即返回去继续执行其他任务。日志写入操作则是由一个或者多个后台线程专门运行,这些线程就是负责从任务队列中取出消息,然后将日志消息写入到指定位置即可。
设计目标
- 减少主线程阻塞: 通过异步处理,主线程在记录日志时几乎不会阻塞
- 提高系统吞吐量: 通过批量处理和异步I/O操作,最大限度地提高日志系统的吞吐量
- 确保日志顺序性: 在高并发场景下,确保日志按照生成的顺序写入
异步日志器实现的核心模块说明
Logger模块:接收日志消息,然后将日志消息放入到队列中
namespace maglog {
classLogger {
public:
// 设置异步模式void SetAsyncMode(bool async) {
async_mode_ = async;
if (async_mode_) {
looper_ = std::make_unique<Looper>();
looper_->Start(); // 启动后台线程
}
}
// 记录INFO级别的日志void LogInfo(const std::string& message) {
WriteLog(message, "INFO");
}
private:
bool async_mode_ = false;
std::unique_ptr<Looper> looper_;
voidWriteLog(const std::string& message, const std::string& level){
std::string formatted_message = "[" + level + "] " + message;
if (async_mode_) {
looper_->EnqueueLog(formatted_message); // 异步处理
} else {
sink_->Write(formatted_message); // 同步处理
}
}
};
}
Looper模块:其中的线程安全队列负责存储日志消息,并负责管理后台线程,后台线程则主要就是从队列中提取日志消息,同时通过Sink模块将日志写入到目标位置
namespace maglog {
classLooper {
public:
// 启动后台线程void Start() {
worker_thread_ = std::thread(&Looper::Run, this);
}
// 将日志消息放入队列void EnqueueLog(const std::string& log) {
std::lock_guard<std::mutex> lock(queue_mutex_);
log_queue_.push(log);
queue_cv_.notify_one(); // 通知后台线程处理日志
}
private:
std::queue<std::string> log_queue_;
std::mutex queue_mutex_;
std::condition_variable queue_cv_;
std::thread worker_thread_;
// 后台线程主循环void Run() {
while (true) {
std::unique_lock<std::mutex> lock(queue_mutex_);
queue_cv_.wait(lock, [this] { return !log_queue_.empty(); });
std::string log = log_queue_.front();
log_queue_.pop();
lock.unlock();
// 处理日志,将日志写入目标位置
sink_->Write(log);
}
}
};
}
Queue模块:一个线程安全的消息队列,负责在多线程环境下传递日志信息,通过互斥锁和条件变量,确保日志消息在并发环境下可以正确的被处理
namespace maglog {
classQueue {
public:
// 添加日志消息到队列void Enqueue(const std::string& log) {
std::lock_guard<std::mutex> lock(mutex_);
queue_.push(log);
cv_.notify_one(); // 通知等待的线程
}
// 从队列中获取日志消息std::string Dequeue() {
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this] { return !queue_.empty(); });
std::string log = queue_.front();
queue_.pop();
return log;
}
private:
std::queue<std::string> queue_;
std::mutex mutex_;
std::condition_variable cv_;
};
}
性能优化以及问题解决
批量处理
- 为了进一步提高日志系统的吞吐量,Looper模块支持批量处理日志消息。后台线程可以一次性从队列中取出多条日志消息,然后批量写入。通过这种方法,可以减少I/O操作的次数,提高日志系统的运行效率
双缓冲机制
- 高负载情况下,日志写入的速度有可能是跟不上日志的生成速度。为了解决该问题,该日志系统在设计的时候采用了双缓冲区机制。即一个缓冲区主要用于接收新日志,另一个缓冲区则主要用于异步写入。当写入缓冲区写满的时候,自动交换两个缓冲区,从而实现高负载情况下日志系统稳定运行
日志丢失与数据一致性
- 异常情况下,例如在遇到系统崩溃或者网络故障的时候,有可能会造成日志丢失。为解决该问题,日志系统设计日志的时候设计了日志持久化机制。每当后台线程从队列中取出日志的时候,会将日志先写入一个临时文件中,确保即使在意外断电或者系统崩溃的时候,日志数据不会丢失。
测试结果
【具体测试环境参考最后一节的测试文章】
测试结果
- 在单线程模式下,系统每秒可以处理约 759,204 条日志,数据吞吐量达到 72 MB/s。
- 在多线程模式下(5个工作线程),系统每秒处理日志的数量提升至 1,170,953 条,数据吞吐量达到 111 MB/s。
双缓冲区机制设计
设计思路及其架构
双缓冲区设计的目的主要就是减少数据处理过程中的阻塞。在该日志系统中,双缓冲区允许一个缓冲区用于接收新日志,另一个缓冲区则适用于异步写入。当写入缓冲区满的时候,系统会自动切换到另一个缓冲区中,从而使得日志生成和写入可以并行的进行,从而减少主线程阻塞的时间。
设计目标
- 并行处理: 通过双缓冲区,日志生成与日志写入可以并行进行,减少相互之间的等待
- 平滑过渡: 当一个缓冲区满时,可以立即切换到另一个缓冲区,避免主线程因等待日志写入而阻塞
namespace maglog {
classBuffer {
public:
Buffer(size_t size) : buffer_size_(size), current_buffer_(new std::vector<std::string>), write_buffer_(new std::vector<std::string>) {
current_buffer_->reserve(buffer_size_);
write_buffer_->reserve(buffer_size_);
}
// 向当前缓冲区添加日志void AddLog(const std::string& log) {
std::lock_guard<std::mutex> lock(buffer_mutex_);
current_buffer_->push_back(log);
if (current_buffer_->size() >= buffer_size_) {
SwapBuffers();
}
}
// 获取写入缓冲区std::vector<std::string>* GetWriteBuffer() {
std::lock_guard<std::mutex> lock(buffer_mutex_);
return write_buffer_.get();
}
// 清空写入缓冲区void ClearWriteBuffer() {
std::lock_guard<std::mutex> lock(buffer_mutex_);
write_buffer_->clear();
}
private:
size_t buffer_size_;
std::unique_ptr<std::vector<std::string>> current_buffer_;
std::unique_ptr<std::vector<std::string>> write_buffer_;
std::mutex buffer_mutex_;
// 交换当前缓冲区和写入缓冲区void SwapBuffers() {
std::swap(current_buffer_, write_buffer_);
// 可以通知异步线程写入日志
}
};
}
工作原理
- 日志添加: 日志生成时,日志信息首先被添加到
current_buffer_
缓冲区中- 缓冲区切换: 当
current_buffer_
达到预定的大小时,系统自动切换到write_buffer_
,并将current_buffer_
的内容交给后台线程异步写入- 异步写入: 后台线程异步将
write_buffer_
中的日志信息写入目标位置,并在写入完成后清空write_buffer_
生产消费模式与双缓冲区结合
结合的主要目的在于提升性能、提高响应速度、提高稳定性以及日志输出一致性。首先,两种机制结合,系统能够有效处理高并发下的大量日志请求,显著提升系统性能;其次,主线程基本不会因为日志写入阻塞,从而提高系统的整体响应速度;最后,缓冲区机制确保了日志写入的顺序性和稳定性,避免因并发竞争而导致日志丢失和重复写入的情况。
实现流程
- 日志生成: 主线程作为生产者,不断生成日志消息,并通过
Logger
模块将消息添加到Buffer
中- 缓冲区切换: 当
current_buffer_
满时,Buffer
模块切换到write_buffer_
,并通知Looper
模块开始异步写入日志- 异步写入:
Looper
模块从write_buffer_
中提取日志消息,异步写入到目标位置- 清空缓冲区: 写入完成后,清空
write_buffer_
,等待下一次切换
//实现说明(并非项目中的具体实现)
voidLogger::WriteLog(const std::string& message, const std::string& level){
std::string formatted_message = "[" + level + "] " + message;
if (async_mode_) {
buffer_.AddLog(formatted_message); // 使用双缓冲区机制
looper_->EnqueueLog(buffer_.GetWriteBuffer()); // 使用生产者-消费者模型
} else {
sink_->Write(formatted_message);
}
}
架构实现
日志格式化输出逻辑
设计日志消息的输出格式,根据使用者指定的格式对日志消息进行输出
高性能日志系统 日志格式化输出逻辑_格式化日志输出信息-CSDN博客(具体分析参考该文章)
日志输出模块逻辑
借助多态和工厂模式,构建灵活的日志输出,同时可以根据自己需求对其进行拓展,将日志输出到任何自己想要输出的位置。
高性能日志系统 日志输出模块逻辑_日志标准输出是什么-CSDN博客(具体分析参考该文章)
日志器模块逻辑
主要通过建造者模式,构建同步日志器以及异步日志器的实现,同时实现了双缓冲区,提高日志输出的性能。
高性能日志系统 日志器模块-CSDN博客(具体分析参考该文章)
全局日志器接口
将获取和管理日志器的逻辑都封装在LoggerManner中,从而实现快速调用日志器的目的。
高性能日志系统 代理模式构建全局日志器获取接口-CSDN博客(具体分析参考该文章)
优化与问题解决
性能优化
- 异步处理机制:通过Looper模块实现异步处理机制,减少主线程的阻塞时间,从而提升系统的响应速度
- 批量处理:双缓冲区实现日志批量处理
- 线程安全:互斥锁以及条件变量实现日志消息传递的安全性,避免线程的并发竞争
问题
- 高并发:高并发场景下,通过生产消费模式和双缓冲区的结合,平衡日志生成和写入的速度,从而避免了队列溢出以及日志错误等问题
- 日志一致性:双缓冲区机制以及队列的顺序管理保证日志输出的一致性
难点与挑战
【前文分析中说明了部分项目中的难点和挑战,在该处进行汇总】
设计与实现该项目过程中,处理高并发、确保日志数据一致性以及实现系统拓展性是项目设计最关键的问题。通过引用异步处理、生产者消费者模型、双缓冲区机制以及模块设计,解决上述难点。从而构建一个高效、可靠且灵活的日志系统。
高并发
高并发存在的问题分析
- 主线程阻塞: 日志写入涉及I/O操作,可能会导致主线程长时间阻塞,无法及时响应其他请求
- 锁竞争: 多个线程同时写入日志时,容易发生锁竞争,导致系统性能下降
- I/O瓶颈: 大量的日志写入请求可能导致I/O操作频繁,会导致系统性能降低
解决思路总结
- 异步处理机制: 将日志的记录与写入分离,主线程只需将日志消息放入线程安全的队列中,然后立即返回。日志写入由后台线程异步完成,避免了主线程的阻塞
- 生产者-消费者模型: 采用生产者-消费者模型,主线程作为生产者不断生成日志消息并将其放入队列,消费者线程则从队列中提取日志并进行写入操作。借助该模型有效地平衡了日志生成与写入的速度,避免了队列溢出和日志丢失问题
- 双缓冲区机制: 为进一步减少锁竞争和内存分配开销,系统采用了双缓冲区机制。一个缓冲区用于接收新日志,另一个缓冲区用于异步写入。当写入缓冲区满时,系统自动切换到另一个缓冲区,避免主线程等待日志写入完成
日志丢失与数据一致性
问题分析
- 异常情况下的日志丢失: 在系统崩溃或断电的情况下,正在写入的日志可能会丢失
- 数据一致性问题: 在并发环境下,如果日志消息的处理顺序被打乱,可能会导致日志数据不一致,给后续的调试和问题排查带来困难
解决方法
- 持久化机制: 在异步处理日志的同时,系统采用了日志持久化机制。每当后台线程从队列中提取日志消息时,会先将其写入一个临时文件或缓冲区,确保即使在系统崩溃时,日志数据也不会丢失。恢复时,可以通过读取临时文件恢复未写入完成的日志
- 顺序写入: 通过严格的队列管理和双缓冲区切换机制,确保日志消息按生成的顺序被写入,从而避免数据不一致的问题
- 日志冗余机制: 为防止单一日志写入失败导致的数据丢失,系统支持日志冗余写入,即将日志同时写入多个输出到多个地方,即使一个目标写入失败,其他目标的日志数据仍然完整
拓展性和灵活性
问题分析
- 多样化需求: 不同的应用对日志格式和输出方式有不同的要求,系统需要能够灵活配置
- 扩展难度: 在支持更多功能和特性的同时,必须避免对现有系统造成影响,确保系统的稳定性和性能不受损
解决方法
- 模块化设计: 系统采用模块化设计,将日志的记录、格式化、输出等功能分离为独立的模块。通过这种方式,用户可以根据需求自由组合和替换模块,而不会影响系统的核心功能
- 设计模式的应用: 在系统的设计中,我广泛应用了工厂模式、策略模式和代理模式。例如,通过工厂模式,系统可以灵活创建不同的
Sink
实例,而不需要修改Logger
的核心代码;通过策略模式,用户可以动态选择或更改日志的格式化方式- 动态配置支持: 系统支持通过配置文件或环境变量动态调整日志级别、输出目标和格式化方式,用户可以在运行时进行配置自己想要的日志系统,不需要重新编译代码
性能优化与测试
项目测试过程中,使用htop、vmstat等工具,同时借助日志分析,分析同步日志以及异步日志写入的耗时情况,验证并推测可能出现的瓶颈,然后找到具体的问题,针对性的对其优化。
性能优化策略
上文中穿插对部分性能优化最终落地实现的分析,该处主要就是分析优化策略和问题的解决思路,不再说明具体实现。
优化总结
- 异步处理机制: 将日志记录与日志写入分离,减少主线程的阻塞时间
- 生产者-消费者模型: 通过多线程协调生产和消费日志,提高并发处理能力
- 双缓冲区机制: 通过双缓冲区减少内存分配和释放的开销,提高日志写入效率
- 批量处理: 减少I/O操作的频率,提升系统的整体吞吐量
- 锁优化与无锁编程: 通过减少锁的使用或采用无锁数据结构,进一步降低线程间的竞争
异步日志处理机制引进
- 传统日志系统中,都是主线程直接负责日志的写入操作,这样会导致主线程长时间阻塞,影响系统的整体响应速度
- 优化:通过异步机制,日志生成和写入分离,主线程将日志消息放入到安全队列中就返回,后台线程取出该日志任务进行写入操作
生产消费模型
- 目的就是为了解决多个线程同时生成日志的时候,如何协调线程之间的运行,避免队列溢出和资源竞用
- 优化:使用消费生产模型, 主线程作为生产者将日志消息放入队列,消费者线程从队列中提取日志并执行写入操作。这样可以平衡生产与消费的速度,防止队列溢出
双缓冲区机制
- 高负载场景下,日志的写入速度不一定可以跟得上日志生成速度,导致主线程被阻塞
- 优化:双缓冲区机制,一个接收新日志,另一个异步写入日志,然后当写入为空的时候,交换两个缓冲区,这样使得即使写入速度较慢,也不会阻塞主线程的日志生成操作
批量处理
- 日志系统中,频繁的I/O操作会导致系统性能下降
- 优化:通过批量处理,将多个日志消息合并后一次性写入。这种方法减少了I/O操作的次数,从而提高了系统的吞吐量
锁优化
- 线程间的锁竞争是并发编程中的一大问题,频繁的锁操作可能导致性能下降
- 优化:在日志系统的设计中,通过减少锁的使用或采用无锁数据结构来降低线程间的竞争。可以使用原子操作或无锁队列来代替传统的锁机制,从而进一步提升系统的并发性能(下面代码中说明使用atomic进行无锁计数器的实现)
#include<atomic>classLogger {
public:
voidIncrementLogCount(){
log_count_.fetch_add(1, std::memory_order_relaxed);
}
intGetLogCount()const{
return log_count_.load(std::memory_order_relaxed);
}
private:
std::atomic<int> log_count_{0};
};
- 异步处理: 解释为什么异步日志处理比同步处理更高效,以及如何实现异步处理。
- 内存管理: 讨论缓冲区的设计与内存分配策略,如何减少内存碎片化和分配开销。
性能测试结果
主要测试了日志系统在高并发环境下运行的性能,同时进行了容错性测试,建立对应场景,防止日志系统崩溃等。
测试结果
- 响应:耗时1.20599秒,高负载情况下实现百万并发
- 吞吐量:每秒处理829365日志,处理数据量79M,实现高并发情况下的数据处理性能
- 多线程并发:5个线程并行处理日志,充分利用CPU提高运行性能
高性能日志系统 性能测试-CSDN博客(详细测试参考该文章)
总结与反思
主要成果
- 高并发:异步处理、生产消费模型、双缓冲区,实现主线程不会因写入则色,提高系统吞吐量
- 性能优化:批量处理、锁优化,提高处理速度,同时智能指针管理内存,减少频繁释放内存,提高日志系统的稳定性
- 模块设计:模块化设计以及多种设计模式结合,从而提高日志的可拓展性
项目反思
提升异步处理的复杂性
日志系统设计初期,更多关注日志系统吞吐量以及并发处理能力。但是在后续的测试中,系统在极端高并发场景下仍然有性能瓶颈问题,主要体现在异步日志写入时的队列管理和资源调度上。
异步日志虽然极大的提高了性能,但是如何在极端高并发情况下,平衡生产和消费的速度,是一个巨大的挑战。高负载的情况下,日志队列可能会积压,导致日志的写入速度是无法跟上生成速度,从而会导致延迟写入和队列溢出。
锁优化带来的局限性
尽管通过锁的时候和优化提升了系统的整体性能,但是多线程下锁的竞争是不可避免的,锁的优化虽然解决了部分并发竞争问题,但是只要使用锁,就会存在性能开销问题
内存管理的平衡
在日志系统的内存管理中,双缓冲区和内存池极大地减少了内存的分配和释放操作,但当系统处理非常大的日志数据时,缓冲区大小的设计可能导致内存使用过高。如何在不同的负载条件下动态调整缓冲区大小,确保系统既能高效运行,又不会过度消耗内存资源,是我在设计中需要进一步考虑的因素
项目改进
异步I/O和事件驱动模型
尝试应用效率更高的epoll模型,以及Reactor机制,实现更大的吞吐量和系统响应速度
高级的无锁数据结构
lock-free queues 等无锁数据结构,减少使用锁造成的性能开销,提高并发能力。
动态调整缓冲区大小
引入动态缓冲区管理机制,然系统根据当前负载自动调整缓冲区,从而让系统可以在低负载的时候减少内存消耗
集成分布式日志系统
将日志系统拓展为一个分布式日志,支持日志的分片存储和跨节点写入
spdlog源码参考
轻量级高性能的日志库,支持同步异步两种日志记录模式,允许灵活拓展日志功能
核心功能总结
- 同步与异步日志: 支持高效的同步和异步日志处理。
- 丰富的格式化选项: 提供灵活的日志格式化功能,用户可以根据需要自定义日志输出格式。
- 多种输出目标: 支持将日志输出到控制台、文件、滚动文件等多种目标
阅读spdlog源码理解与项目改进
Logger模块
- 核心模块,负责日志的写入和输出
- 分析该源码逻辑后,得知其是采用组合方式设计,将sink作为成员变量处理日志的输出,这样可以灵活的支持多种日志输出方式
- 该设计思路在本项目中的日志模块中应用,设计一个灵活的日志记录器,提高扩展性
namespace spdlog {
classLogger {
public:
// 构造函数中注入Sink模块Logger(std::string name, sinks_init_list sinks)
: name_(std::move(name)), sinks_(sinks) {}
// 记录日志template<typename... Args>
void log(level::level_enum lvl, fmt::format_string<Args...> fmt, Args&&... args) {
// 格式化日志消息memory_buf_t formatted;
formatter_->format(lvl, name_, fmt::vformat(fmt, fmt::make_format_args(args...)), formatted);
// 输出日志到所有的Sinkfor (auto& sink : sinks_) {
sink->log(lvl, formatted);
}
}
private:
std::string name_;
std::vector<std::shared_ptr<sinks::sink>> sinks_;
std::unique_ptr<formatter> formatter_;
};
}
Sink模块
- 源码中使用Sink子类,实现向不同方向输出日志内容
- 本项目中也采用类似策略,从而实现日志可以定向输出到指定位置,该项目中实现了滚动输出、文件输出等功能
namespace spdlog {
namespace sinks {
classsink {
public:
virtual ~sink() = default;
virtualvoidlog(const details::log_msg& msg)= 0;
virtualvoidflush()= 0;
};
classstdout_sink : public sink {
public:
voidlog(const details::log_msg& msg)override{
fmt::print("{}\n", msg.formatted.str());
}
voidflush()override{
std::fflush(stdout);
}
};
}
}