高性能日志系统

设计思路

架构设计

核心模块作用

  • 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);
    }
};

}
}

  • 12
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值