高性能日志系统

目录

设计思路

架构设计

设计模式应用

单例模式

工厂模式

建造者模式

代理模式

异步处理设计

异步日志器使用原因

异步日志器设计思路 

 异步日志器实现的核心模块说明

性能优化以及问题解决

测试结果

 双缓冲区机制设计

设计思路及其架构

生产消费模式与双缓冲区结合

架构实现

​编辑

日志格式化输出逻辑

日志输出模块逻辑

日志器模块逻辑

全局日志器接口

优化与问题解决

难点与挑战

高并发

日志丢失与数据一致性

拓展性和灵活性

性能优化与测试

性能优化策略

性能测试结果

总结与反思

主要成果

 项目反思

项目改进

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

}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/870670.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++ wxWidgets图形界面开发用什么IDE最好?

在主流免费的IDE工具中&#xff0c;我们可以想到的支持cmake项目的工具就只有QtCreator&#xff0c;VisualStudio&#xff0c;VSCode这三个。其中QtCreator和VSCode支持WIndows&#xff0c;Mac&#xff0c;WIndows三大主流平台。但是VSCode在Ubuntu等系统下的支持并没有在WIndo…

【vue讲解:ref属性、动态组件、插槽、vue-cli创建项目、vue项目目录介绍、vue项目开发规范、es6导入导出语法】

0 ref属性&#xff08;组件间通信&#xff09; # 1 ref属性放在普通标签上<input type"text" v-model"name" ref"myinput">通过 this.$refs[myinput] 拿到的是 原生dom对象操作dom对象&#xff1a;改值&#xff0c;换属性。。。# 2 ref属…

Leetcode JAVA刷刷站(38)外观数列

一、题目概述 二、思路方向 为了解决这个问题&#xff0c;我们可以编写一个Java函数countAndSay&#xff0c;该函数接受一个整数n作为输入&#xff0c;并返回外观数列的第n个元素。这个函数将基于递归公式来构建数列&#xff0c;其中countAndSay(1) "1"&#xff0c;…

pycharm windows/mac 指定多版本python

一、背景 工作中经常会使用不同版本的包&#xff0c;如同时需要tf2和tf1&#xff0c;比较新的tf2需要更高的python版本才能安装&#xff0c;而像tf1.5 需要低版本的python 才能安装&#xff08;如 python3.6&#xff09;,所以需要同时安装多个版本。 二、安装多版本python py…

网络安全之XSS基础

从 XSS Payload 学习浏览器解码 xss payload 1.<a href"%6a%61%76%61%73%63%72%69%70%74:%61%6c%65%72%74%28%31%29">1</a> 2.<a href"&#x6a;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;:…

SpringBoot+Vue在线商城(电子商城)系统-附源码与配套论文

摘 要 随着互联网技术的发展和普及&#xff0c;电子商务在全球范围内得到了迅猛的发展&#xff0c;已经成为了一种重要的商业模式和生活方式。电子商城是电子商务的重要组成部分&#xff0c;是一个基于互联网的商业模式和交易平台&#xff0c;通过网络进行产品和服务的销售。…

【题目/训练】:双指针

引言 我们已经在这篇博客【算法/学习】双指针-CSDN博客里面讲了双指针、二分等的相关知识。 现在我们来做一些训练吧 经典例题 1. 移动零 思路&#xff1a; 使用 0 当做这个中间点&#xff0c;把不等于 0(注意题目没说不能有负数)的放到中间点的左边&#xff0c;等于 0 的…

Unity项目优化记录

背景&#xff1a;测试反馈项目组游戏存在内存泄露&#xff0c;来找到中台这边协调排查。好家伙&#xff0c;跑了两次看了内存快照&#xff0c;再看资源组织和管理方式&#xff0c;存在的问题确实比较多。 1、修复内存泄露&#xff1a;结算界面由于资源引用丢失导致整个面板不会…

景联文科技高质量文本标注:驱动自然语言处理技术的发展与应用

文本标注是自然语言处理&#xff08;NLP&#xff09;领域的一个重要环节&#xff0c;是指在文本数据上添加额外的信息或标记的过程&#xff0c;目的是为了让计算机能够理解和处理这些文本数据。 通过文本标注&#xff0c;可以为文本中的各个部分提供具体的含义和上下文信息&…

基于vue全家桶的pc端仿淘宝系统_kebgy基于vue全家桶的pc端仿淘宝系统_kebgy--论文

TOC springboot478基于vue全家桶的pc端仿淘宝系统_kebgy基于vue全家桶的pc端仿淘宝系统_kebgy--论文 绪 论 1.1开发背景 改革开放以来&#xff0c;中国社会经济体系复苏&#xff0c;人们生活水平稳步提升&#xff0c;中国社会已全面步入小康社会。同时也在逐渐转型&#xf…

这个是git使用的合集

如果遇到了关于git和github的bug就会写这里 2024/8/16 github一直没有打卡和上传代码是因为感觉除了做项目的情况&#xff0c;普通的学习和普通的笔记没必要记在github里&#xff1b;如果是笔记类的东西为什么不记在csdn上呢&#xff1f;如果是算法题算法网站上回有记录啊&am…

Cacti SQL注入漏洞分析(CVE-2023-51448)

Cacti 为全球用户提供强大且可扩展的运营监控和故障管理框架。它还是一个完整的网络绘图解决方案&#xff0c;旨在利用RRDTool的数据存储和绘图功能。Cacti 包括一个完全分布式和容错的数据收集框架、用于设备、图表和树的高级基于模板的自动化功能、多种数据采集方法、通过插件…

Vue2 和 Vue3中EventBus使用差异

目录 前言一、EventBus 和 mitt 的对比二、Vue 2 中的 EventBus 使用实例2.1 创建 EventBus2.2 在组件中使用 EventBus2.2.1 组件 A - 发送事件2.2.2 组件 B - 监听事件 2.3 注意事项 三、Vue 3 中的 mitt 使用实例3.1 安装 mitt3.2 创建 mitt 实例3.3 在组件中使用 mitt3.3.1 …

DHU OJ 二维数组

思路及代码 #include<iostream> using namespace std; int main(){ //input 多组 //input M,N int 1< <20 //input M 行 N 列 数据 //initialize listint M, N;while (cin >> M >> N){int list[M][N];for (int i 0; i < M-1; i){for (int j 0; j…

Python编写Word文档

目录 0. 安装依赖 1. 创建word文档 2. 添加标题、居中、字体16大小 3. 添加标题一 4. 添加一段话并设置字体颜色 封装函数 5. 换页 6. 插入表格 0. 安装依赖 python-docx1.1.2 1. 创建word文档 from docx import Documentdoc Document() 2. 添加标题、居中、字体1…

计算机网络面试题汇总

文章目录 计算机网络基础计算机网络体系结构(网络分层模型)OSI 七层模型是什么?每一层的作用是什么?TCP/IP 四层模型是什么?每一层的作用是什么?五层体系结构以及对应的协议为什么网络要分层,分层的好处?常见网络协议有哪些,每一层常见协议有哪些?应用层有哪些常见的协…

24/8/18算法笔记 目标导向强化学习

目标导向强化学习&#xff08;Goal-Oriented Reinforcement Learning&#xff0c;简称GORL&#xff09;是强化学习的一个分支&#xff0c;它关注于智能体如何通过与环境的交互来实现特定的目标或任务。与传统的强化学习不同&#xff0c;目标导向强化学习更加关注目标的设定和达…

一元二次方程系数

前言&#xff1a;刚刚开始写的时候也想到了先求出两个的解&#xff0c;但是没想到最后正负数系数怎么处理 且我才知道求解gcd是可以负数和正数的 #include<bits/stdc.h> using namespace std;#define int long long int t; int a,b,c;void solve(){cin >> a >&…

spfa()算法(求最短路)

spfa算法是对bellman_ford算法的优化&#xff0c;大部分求最短路问题都可以用spaf算法来求。 注意&#xff1a; &#xff08;1&#xff09;如若图中有负权回路&#xff0c;不能用spfa算法&#xff0c;要用bellman_ford算法&#xff1b;若只有负权边&#xff0c;则可以用 spf…

得到任务式 大模型应用开发学习方案

根据您提供的文档内容以及您制定的大模型应用开发学习方案&#xff0c;我们可以进一步细化任务式学习的计划方案。以下是具体的任务式学习方案&#xff1a; 任务设计 初级任务 大模型概述&#xff1a;阅读相关资料&#xff0c;总结大模型的概念、发展历程和应用领域。深度学…