目录
设计思路
架构设计
设计模式应用
单例模式
工厂模式
建造者模式
代理模式
异步处理设计
异步日志器使用原因
异步日志器设计思路
异步日志器实现的核心模块说明
性能优化以及问题解决
测试结果
双缓冲区机制设计
设计思路及其架构
生产消费模式与双缓冲区结合
架构实现
编辑
日志格式化输出逻辑
日志输出模块逻辑
日志器模块逻辑
全局日志器接口
优化与问题解决
难点与挑战
高并发
日志丢失与数据一致性
拓展性和灵活性
性能优化与测试
性能优化策略
性能测试结果
总结与反思
主要成果
项目反思
项目改进
spdlog源码参考
阅读spdlog源码理解与项目改进
设计思路
架构设计
核心模块作用
- 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);
}
};
}
}