webserver--多缓冲区实现日志系统

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: webserver--多缓冲区实现日志系统

⽇志系统

服务器的⽇志系统是⼀个多⽣产者,单消费者的任务场景:多⽣产者负责把⽇志写⼊缓冲区,单消费者负责把缓冲 区中数据写⼊⽂件。如果只⽤⼀个缓冲区,不光要同步各个⽣产者, 还要同步⽣产者和消费者。⽽且最重要的是需要保证⽣产者与消费者的并发,也就是前端不断写⽇志到缓冲区的同时,后端可以把缓冲区写⼊⽂件。


所以我们这里采用双缓冲区,这样就可以高效的避免因为读写原因导致的阻塞。


双缓冲技术的基本思路:准备两块 buffer , A 和 B, 前端往 A 写数据,后端从 B ⾥⾯往硬盘写数据,当 A 写满 后,交换 A 和 B ,如此反复。使⽤两个 buffer 的好处是在新建⽇志消息的时候不必等待磁盘⽂件操作,也避 免每条新⽇志消息都触发后端⽇志线程。换句话说,前端不是将⼀条条⽇志消息分别送给后端,⽽是将多条⽇ 志消息拼接成⼀个⼤的 buffer 传送给后端,相当于批处理,减少了线程唤醒的开销。

先看看大纲理解一下每个函数的作用。

各个函数的作用:

Log::Log() - 构造函数


初始化Log类的实例。设置日志级别、异步写入标志、写入线程、队列、今日日期等成员变量的初始状态。

Log::~Log() - 析构函数


清理Log类的实例。如果存在异步写入线程,则等待线程结束,关闭队列,加入主线程,并关闭文件指针。如果文件指针存在,则同步写入并关闭日志文件。

Log::GetLevel() - 获取当前日志级别


通过互斥锁保护的成员变量level_,返回当前设置的日志级别。

Log::SetLevel(int level) - 设置日志级别


通过互斥锁保护的成员变量level_,设置新的日志级别。

Log::init(int level, const char* path, const char* suffix, int maxQueueSize) - 初始化日志系统


设置日志级别、日志文件路径、文件后缀和最大队列大小。根据队列大小决定是否开启异步写入模式,并初始化相关的成员变量。创建日志文件并打开用于写入。

Log::write(int level, const char *format, ...) - 写入日志


根据指定的日志级别和格式化字符串写入日志。首先获取当前时间,然后根据日期和行数决定是否需要创建新文件。格式化日志内容并写入缓冲区,如果开启异步写入,则将日志内容推送到队列中,否则直接写入文件。

Log::AppendLogLevelTitle_(int level) - 添加日志级别标题


根据传入的日志级别,向缓冲区追加相应的日志级别标题。

Log::flush() - 刷新日志缓冲区


如果开启异步写入,则调用队列的flush()方法,否则调用fflush()刷新文件指针。

Log::AsyncWrite_() - 异步写入日志


从队列中取出日志内容并写入文件。这是异步线程执行的函数,用于将日志缓冲区的内容异步写入磁盘。

Log::Instance() - 获取日志实例


静态函数,返回Log类的一个实例。这里使用了单例模式,确保整个程序中只存在一个日志实例。

Log::FlushLogThread() - 异步日志写入线程函数


这是异步线程的入口点,它会调用AsyncWrite_()函数来处理异步日志写入任务

具体详细注释代码(太不容易了,点个赞呗,可以关注一下公众号和视频号)

#include "log.h"
 
using namespace std;
 
// Log类的构造函数
Log::Log() {
    lineCount_ = 0; // 初始化日志行数为0
    isAsync_ = false; // 初始化异步写入标志为false
    writeThread_ = nullptr; // 初始化写入线程指针为nullptr
    deque_ = nullptr; // 初始化日志队列指针为nullptr
    toDay_ = 0; // 初始化今日日期标志为0
    fp_ = nullptr; // 初始化文件指针为nullptr
}
 
// Log类的析构函数
Log::~Log() {
    // 如果存在写入线程并且可以加入,则等待线程结束
    if(writeThread_ && writeThread_->joinable()) {
        // 循环直到队列清空
        while(!deque_->empty()) {
            deque_->flush();
        };
        // 关闭队列
        deque_->Close();
        // 等待写入线程结束
        writeThread_->join();
    }
    // 如果文件指针存在
    if(fp_) {
        // 锁定互斥锁
        lock_guard<mutex> locker(mtx_);
        // 刷新缓冲区并关闭文件
        flush();
        fclose(fp_);
    }
}
 
// 获取当前日志级别
int Log::GetLevel() {
    // 锁定互斥锁
    lock_guard<mutex> locker(mtx_);
    // 返回日志级别
    return level_;
}
 
// 设置日志级别
void Log::SetLevel(int level) {
    // 锁定互斥锁
    lock_guard<mutex> locker(mtx_);
    // 设置新的日志级别
    level_ = level;
}
 
// 初始化日志系统
void Log::init(int level, const char* path, const char* suffix,
    int maxQueueSize) {
    // 设置日志级别
    isOpen_ = true;
    level_ = level;
    // 如果队列大小大于0,则开启异步写入
    if(maxQueueSize > 0) {
        isAsync_ = true;
        // 如果队列不存在,则创建新队列
        if(!deque_) {
            unique_ptr<BlockDeque<string>> newDeque(new BlockDeque<string>);
            deque_ = move(newDeque);
            // 创建新的写入线程
            std::unique_ptr<std::thread> NewThread(new thread(FlushLogThread));
            writeThread_ = move(NewThread);
        }
    } else {
        // 否则关闭异步写入
        isAsync_ = false;
    }
    // 重置日志行数
    lineCount_ = 0;
 
    // 获取当前时间
    time_t timer = time(nullptr);
    struct tm *sysTime = localtime(&timer);
    struct tm t = *sysTime;
    // 设置日志文件路径和后缀
    path_ = path;
    suffix_ = suffix;
    // 构造日志文件名
    char fileName[LOG_NAME_LEN] = {0};
    snprintf(fileName, LOG_NAME_LEN - 1, "%s/%04d_%02d_%02d%s", 
            path_, t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, suffix_);
    // 设置今日日期
    toDay_ = t.tm_mday;
 
    // 锁定互斥锁
    lock_guard<mutex> locker(mtx_);
    // 清空缓冲区
    buff_.RetrieveAll();
    // 如果文件指针存在,则关闭旧文件
    if(fp_) { 
        flush();
        fclose(fp_); 
    }
    // 打开新文件
    fp_ = fopen(fileName, "a");
    // 如果文件打开失败,则创建目录后再次尝试
    if(fp_ == nullptr) {
        mkdir(path_, 0777);
        fp_ = fopen(fileName, "a");
    }
    // 断言文件指针不为空
    assert(fp_ != nullptr);
}
 
// 写入日志
void Log::write(int level, const char *format, ...) {
    // 获取当前时间
    struct timeval now = {0, 0};
    gettimeofday(&now, nullptr);
    time_t tSec = now.tv_sec;
    struct tm *sysTime = localtime(&tSec);
    struct tm t = *sysTime;
    va_list vaList;
 
    // 如果日期变化或达到最大日志行数,则创建新文件
    if (toDay_ != t.tm_mday || (lineCount_ && (lineCount_  %  MAX_LINES == 0))) {
        // 锁定互斥锁
        unique_lock<mutex> locker(mtx_);
        locker.unlock();
        
        // 构造新文件名
        char newFile[LOG_NAME_LEN];
        char tail[36] = {0};
        snprintf(tail, 36, "%04d_%02d_%02d", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday);
 
        // 如果日期变化
        if (toDay_ != t.tm_mday) {
            snprintf(newFile, LOG_NAME_LEN - 72, "%s/%s%s", path_, tail, suffix_);
            // 更新今日日期
            toDay_ = t.tm_mday;
            // 重置日志行数
            lineCount_ = 0;
        } else {
            // 否则更新文件名,包含行数信息
            snprintf(newFile, LOG_NAME_LEN - 72, "%s/%s-%d%s", path_, tail, (lineCount_  / MAX_LINES), suffix_);
        }
        
        // 重新加锁
        locker.lock();
        // 刷新缓冲区
        flush();
        // 关闭旧文件
        fclose(fp_);
        // 打开新文件
        fp_ = fopen(newFile, "a");
        // 断言文件指针不为空
        assert(fp_ != nullptr);
    }
 
    // 锁定互斥锁
    unique_lock<mutex> locker(mtx_);
    // 增加日志行数
    lineCount_++;
    // 格式化日志时间
    int n = snprintf(buff_.BeginWrite(), 128, "%d-%02d-%02d %02d:%02d:%02d.%06ld ",
                    t.tm_year + 1900, t.tm_mon + 1, t.tm_mday,
                    t.tm_hour, t.tm_min, t.tm_sec, now.tv_usec);
    // 写入时间到缓冲区
    buff_.HasWritten(n);
    // 添加日志级别标题
    AppendLogLevelTitle_(level);
 
    // 格式化日志内容
    va_start(vaList, format);
    int m = vsnprintf(buff_.BeginWrite(), buff_.WritableBytes(), format, vaList);
    va_end(vaList);
 
    // 写入日志内容到缓冲区
    buff_.HasWritten(m);
    // 添加换行符
    buff_.Append("\n\0", 2);
 
    // 如果开启异步写入且队列未满,则将日志内容推送到队列
    if(isAsync_ && deque_ && !deque_->full()) {
        deque_->push_back(buff_.RetrieveAllToStr());
    } else {
        // 否则直接写入文件
        fputs(buff_.Peek(), fp_);
    }
    // 清空缓冲区
    buff_.RetrieveAll();
}
 
// 添加日志级别标题
void Log::AppendLogLevelTitle_(int level) {
    // 根据日志级别添加相应的标题
    switch(level) {
    case 0:
        buff_.Append("[debug]: ", 9);
        break;
    case 1:
        buff_.Append("[info] : ", 9);
        break;
    case 2:
        buff_.Append("[warn] : ", 9);
        break;
    case 3:
        buff_.Append("[error]: ", 9);
        break;
    default:
        buff_.Append("[info] : ", 9);
        break;
    }
}
 
// 刷新缓冲区,将内容写入文件
void Log::flush() {
    // 如果开启异步写入,则调用队列的flush方法
    if(isAsync_) { 
        deque_->flush(); 
    }
    // 刷新文件指针
    fflush(fp_);
}
 
// 异步写入日志
void Log::AsyncWrite_() {
    // 从队列中取出日志内容并写入文件
    string str = "";
    while(deque_->pop(str)) {
        // 锁定互斥锁
        lock_guard<mutex> locker(mtx_);
        // 将日志内容写入文件
        fputs(str.c_str(), fp_);
    }
}
 
// 获取日志实例
Log* Log::Instance() {
    // 创建并返回Log类的单例实例
    static Log inst;
    return &inst;
}
 
// 异步日志写入线程函数
void Log::FlushLogThread() {
    // 调用实例的异步写入函数
    Log::Instance()->AsyncWrite_();
}
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
3月前
|
运维 安全 Linux
【揭秘】如何轻松掌控Linux系统命脉?——一场探索日志文件奥秘的旅程,带你洞悉系统背后的故事!
【8月更文挑战第21天】日志文件对Linux系统至关重要,记录着包括应用行为、组件状态和安全事件在内的系统活动,如同系统的“黑匣子”。掌握日志查看技巧是系统管理的基础技能,有助于快速诊断问题。常用命令包括`cat`、`tail`和`grep`等,可用于查看如`/var/log/messages`和`/var/log/auth.log`等系统日志文件,以及特定应用的日志。`journalctl`则用于查看systemd服务日志。此外,`logrotate`工具可管理日志文件的滚动和归档,确保系统高效运行。
46 4
|
3月前
|
存储 数据采集 数据处理
【Flume拓扑揭秘】掌握Flume的四大常用结构,构建强大的日志收集系统!
【8月更文挑战第24天】Apache Flume是一个强大的工具,专为大规模日志数据的收集、聚合及传输设计。其核心架构包括源(Source)、通道(Channel)与接收器(Sink)。Flume支持多样化的拓扑结构以适应不同需求,包括单层、扇入(Fan-in)、扇出(Fan-out)及复杂多层拓扑。单层拓扑简单直观,适用于单一数据流场景;扇入结构集中处理多源头数据;扇出结构则实现数据多目的地分发;复杂多层拓扑提供高度灵活性,适合多层次数据处理。通过灵活配置,Flume能够高效构建各种规模的数据收集系统。
69 0
|
5天前
|
存储 Linux Docker
centos系统清理docker日志文件
通过以上方法,可以有效清理和管理CentOS系统中的Docker日志文件,防止日志文件占用过多磁盘空间。选择合适的方法取决于具体的应用场景和需求,可以结合手动清理、logrotate和调整日志驱动等多种方式,确保系统的高效运行。
8 2
|
17天前
|
XML JSON 监控
告别简陋:Java日志系统的最佳实践
【10月更文挑战第19天】 在Java开发中,`System.out.println()` 是最基本的输出方法,但它在实际项目中往往被认为是不专业和不足够的。本文将探讨为什么在现代Java应用中应该避免使用 `System.out.println()`,并介绍几种更先进的日志解决方案。
42 1
|
24天前
|
监控 网络协议 安全
Linux系统日志管理
Linux系统日志管理
38 3
|
30天前
|
监控 应用服务中间件 网络安全
#637481#基于django和neo4j的日志分析系统
#637481#基于django和neo4j的日志分析系统
32 4
|
3月前
|
存储 消息中间件 人工智能
AI大模型独角兽 MiniMax 基于阿里云数据库 SelectDB 版内核 Apache Doris 升级日志系统,PB 数据秒级查询响应
早期 MiniMax 基于 Grafana Loki 构建了日志系统,在资源消耗、写入性能及系统稳定性上都面临巨大的挑战。为此 MiniMax 开始寻找全新的日志系统方案,并基于阿里云数据库 SelectDB 版内核 Apache Doris 升级了日志系统,新系统已接入 MiniMax 内部所有业务线日志数据,数据规模为 PB 级, 整体可用性达到 99.9% 以上,10 亿级日志数据的检索速度可实现秒级响应。
AI大模型独角兽 MiniMax 基于阿里云数据库 SelectDB 版内核 Apache Doris 升级日志系统,PB 数据秒级查询响应
|
1月前
|
监控 Linux 测试技术
Linux系统命令与网络,磁盘和日志监控总结
Linux系统命令与网络,磁盘和日志监控总结
52 0
|
1月前
|
监控 Linux 测试技术
Linux系统命令与网络,磁盘和日志监控三
Linux系统命令与网络,磁盘和日志监控三
37 0
|
3月前
|
缓存 NoSQL Linux
【Azure Redis 缓存】Windows和Linux系统本地安装Redis, 加载dump.rdb中数据以及通过AOF日志文件追加数据
【Azure Redis 缓存】Windows和Linux系统本地安装Redis, 加载dump.rdb中数据以及通过AOF日志文件追加数据
125 1
【Azure Redis 缓存】Windows和Linux系统本地安装Redis, 加载dump.rdb中数据以及通过AOF日志文件追加数据
下一篇
无影云桌面