日志系统整体设计步骤以及功能函数梳理

首先到底要做一个什么东西?

我们要造一个 C++ 高并发异步日志库,功能如下:

  1. LOG_INFO << "xxx" 这种简单写法
  2. 自动带:时间、级别、文件名、函数名、行号
  3. 支持级别过滤(TRACE/DEBUG/INFO/WARN/ERROR/FATAL)
  4. 异步写文件,不卡主线程
  5. 文件自动滚动(按大小 / 按天)
  6. 线程安全,多线程一起写不乱

你平时写的:

复制代码
cout << "出错了" << endl;

我们要做的:

复制代码
LOG_INFO << "服务器启动成功";
LOG_ERROR << "数据库连接失败";

输出效果:

复制代码
20260407 18:22:33.123456 INFO main.cpp:100 server start success

第一阶段:最基础的零件------所有功能的地基

1. LogCommon.hpp 公用定义

作用:定规矩

  • 定义 6 个日志级别:TRACE < DEBUG < INFO < WARN < ERROR < FATAL
  • 定义级别名字符串映射:INFO → "INFO"
  • 定义缓冲区大小常量
cpp 复制代码
namespace tulun {
// 日志级别(数字越小越详细)
enum class LOG_LEVEL {
    TRACE = 0,
    DEBUG,
    INFO,
    WARN,
    ERROR,
    FATAL,
};

// 数字转字符串:0→TRACE,1→DEBUG...
const char* LLtoStr[] = { "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL" };

// 缓冲区大小常量
const int SMALL_BUFF_SIZE = 128;
}

面试点:日志级别用来过滤,线上只开 INFO+,调试开 DEBUG。

2. Timestamp 时间戳

作用:给每一条日志打精准时间

功能:

  • 获取当前微秒级时间
  • 格式化成好看的字符串:20260407 18:22:33.123456
  • 给文件命名用:20260407-182233

核心函数:

  • now():获取当前时间
  • toString():格式化字符串
cpp 复制代码
// 获取当前微秒时间戳
Timestamp Timestamp::Now() {
    struct timeval tv;
    gettimeofday(&tv, nullptr);
    uint64_t micro = tv.tv_sec * 1000000 + tv.tv_usec;
    return Timestamp(micro);
}

// 转成 2026/04/07-15:30:55.123456Z
string Timestamp::toFormattedString()

为什么微秒?

高并发系统能区分同一毫秒内的多条日志。

3. LogMessage 日志消息体

作用:把一条日志拼成完整格式

它会自动拼接:时间 + 级别 + 文件名 + 行号 + 你的内容

你写:

复制代码
LOG_INFO << "连接成功";

它自动拼成:

复制代码
2026/04/07-15:30:55 INFO main.cpp main() 10 连接成功

组成部分:

  1. 时间戳 Timestamp
  2. 日志级别 INFO/DEBUG...
  3. 文件名(截取最后一段)
  4. 函数名 __func__
  5. 行号 __LINE__
  6. 用户输入的内容

核心函数:

复制代码
// 构造函数:拼头部
LogMessage::LogMessage(level, file, func, line) {
    ss << 时间 << " " << 级别 << " " << 文件名 << " " << 函数 << " " << 行号;
    header_ = ss.str();
}

// 重载 << 运算符,让你能流式写
template<class T>
LogMessage& operator<<(const T& val) {
    text_ += val;
    return *this;
}

// 最终返回完整日志字符串
string LogMessage::toString() {
    return header_ + text_;
}

第二阶段:核心大脑 Logger------日志器

作用:给用户提供最简单的接口,控制全局行为

它干 5件事:

  1. 全局日志级别控制------判断这条日志该不该输出
  2. 提供输出接口(控制台 / 文件 / 异步)------提供 LOG_INFO 等宏给你用
  3. 提供刷新接口------决定输出到哪(控制台 / 文件 / 网络)
  4. 线程安全控制
  5. 析构时输出日志

最关键的设计:利用析构函数输出日志

复制代码
// 1. 用户写 LOG_INFO << xxx
Logger::Logger(level, file, func, line)
    : impl_(level, file, func, line) // 构造 LogMessage
{}

// 2. 临时对象销毁时,自动输出
Logger::~Logger() {
    // 把日志字符串给输出函数
    s_output_(impl_.toString());
    s_flush_();

    // 如果是 FATAL,直接退出进程
    if (level == FATAL) exit(1);
}

日志宏

复制代码
#define LOG_INFO \
if (当前级别 <= INFO) \
Logger(INFO, __FILE__, __func__, __LINE__).stream()

为什么要用宏?

  • 自动带 __FILE__ 文件名
  • 自动带 __func__ 函数名
  • 自动带 __LINE__ 行号
  • 级别不满足时,整条语句不执行,零开销

最重要的 4 个函数:

  1. setLogLevel设置过滤级别,低于该级别直接丢弃。

  2. setOutput你可以指定输出位置:

    • 默认输出到控制台
    • 可以改成输出到文件
    • 可以改成输出到异步系统
  3. output真正执行输出的函数。

  4. 日志宏 LOG_XXX包装了文件、行号、函数名,让你用起来超级简单。

第三阶段:同步日志

流程超级简单:

复制代码
LOG_INFO << "hello"
→ 生成 Logger 临时对象
→ 构造 LogMessage,拼头部
→ operator<< 写入内容
→ 析构函数调用 output()
→ output 直接输出到控制台/文件

**缺点:**写磁盘是慢 IO!高并发下,业务线程会被日志卡住,性能暴跌。

所以我们必须做:异步日志

第四阶段:高性能核心 ------ 异步日志 AsyncLogging

为什么要异步?

  • 同步:主线程自己写磁盘 → 慢、阻塞
  • 异步:主线程只丢到内存 → 极速、不阻塞后台线程慢慢写磁盘

异步日志设计思想:双缓冲 / 多缓冲技术

你可以理解为:两个缓冲区交换使用

  1. 前端缓冲区:主线程拼命往里写日志
  2. 后端缓冲区:后台线程拿去写入文件
  3. 当前端满了,直接交换两个缓冲区
  4. 后台线程默默写入,完全不影响主线程

这就是高并发日志库的核心!

类似于:两个桶接力倒水

  1. 前端桶:主线程疯狂写日志
  2. 后端桶:后台线程拿去写文件
  3. 前端满了 → 交换两个桶
  4. 主线程继续写,后台线程慢慢刷盘

关键成员

  • currentBuffer_:当前写的缓冲区
  • buffers_:装满的缓冲区队列
  • mutex_:保护缓冲区
  • cond_:通知后台线程
  • thread:后台写线程

核心流程

  1. 用户 append加锁 → 往 currentBuffer 写 → 满了就放入队列 → 唤醒后端
  2. 后端线程 loop 等待队列非空 → 把缓冲区全部拿走 → 解锁 → 批量写文件

核心函数

复制代码
// 主线程调用:写入日志
void AsyncLogging::append(const char* msg, size_t len) {
    lock_guard(mutex);
    if (当前缓冲区不够放) {
        buffers.push_back(move(currentBuffer));
        currentBuffer.reset();
    }
    currentBuffer.append(msg, len);
    cond.notify_one();
}

// 后台线程函数
void AsyncLogging::workThreadFunc() {
    vector<string> buffersToWrite;
    while (running) {
        {
            unique_lock(mutex);
            // 等待 200ms 或有数据
            cond.wait_for(lock, 200ms);

            // 把当前缓冲也加入队列
            buffers.push_back(move(currentBuffer));
            // 交换!后端拿走所有缓冲,前端清空
            buffersToWrite.swap(buffers);
        }

        // 无锁批量写文件(性能关键点!)
        for (auto& buf : buffersToWrite) {
            output_.append(buf);
        }
    }
}

为什么这么设计快?

  • 锁时间极短:只在交换缓冲区时加锁
  • 批量写:减少 IO 次数
  • 主线程几乎无等待

异步日志 3 个核心函数

  1. append() 主线程调用,把日志塞缓冲区,超快!

  2. **threadFunc()**后台线程死循环:

    • 等待缓冲区满
    • 交换缓冲区
    • 写入文件
  3. **start() / stop()**启动 / 停止后台线程

第五阶段 :线程同步工具(CountDownLatch)

CountDownLatch.hpp------等线程启动好

作用:确保异步线程真正启动后,主线程才继续跑

用法:

  1. 初始化计数 1
  2. 主线程调用 wait()
  3. 线程启动好调用 countDown()
  4. 主线程继续执行

保证:日志线程没启动前,不写日志

第六阶段:文件写入模块------日志最终落地

1. AppendFile 底层文件操作

作用:封装 fwrite,自带缓冲区,减少 IO 它是真正把字节写到磁盘的类。

成员:

  • FILE* fp_:文件句柄
  • buffer_:自带 1MB 缓冲区
  • writenBytes_:统计已写字节数(用来滚动文件)
cpp 复制代码
// 追加写入(自带缓冲,超快)
void AppendFile::append(const char* msg, size_t len) {
    fwrite_unlocked(msg, 1, len, fp_);
    writenBytes_ += len;
}

// 刷新到磁盘
void AppendFile::flush() {
    fflush(fp_);
}

2. LogFile 文件管理器

作用:管理日志文件 → 满了就切,到第二天就切

最爱的功能:永不爆炸的日志文件

它管 3 件事:

  1. 生成文件名(包含时间、主机名、进程 ID)
  2. 判断是否要滚动文件
  3. 线程安全的写入接口

滚动规则

  1. 按大小滚动:默认 128KB,超过就切
  2. 按时间滚动:每天 0 点自动切新文件
  3. 每次写入都会检查:是否满了 / 是否跨天

核心函数

复制代码
// 新建一个日志文件
bool LogFile::rollFile()

// 线程安全的追加
void LogFile::append(...) {
    lock_guard(mutex);
    append_unlocked(...);
}

// 不加锁的追加(内部用)
void LogFile::append_unlocked(...) {
    file_->append(...);
    // 检查是否需要滚动/刷新
    if (写太多) rollFile();
    if (太久没刷) flush();
}

文件名示例: mylog.20260407_153055.unknownhost.1234.log

为什么要滚动日志?

答:防止单文件过大无法打开、占用磁盘、方便清理归档。

第七阶段 :整个库完整运行流程

这个日志系统的亮点

  1. 多级日志过滤:线上关闭调试日志
  2. 微秒级时间戳:高并发精准排序
  3. 自动携带上下文:文件、行号、函数
  4. 异步多缓冲:主线程无阻塞,高性能
  5. 日志文件滚动:按大小 / 时间切分
  6. 线程安全:多线程并发写不乱码

异步日志完整链路

复制代码
1. 用户调用 LOG_INFO << "hello"
2. 宏判断级别,满足才执行
3. 构造 Logger 对象
4. 构造 LogMessage,拼接时间/级别/文件/行号
5. operator<< 填入用户内容
6. Logger 析构,调用 output()
7. output 把日志交给 AsyncLogging.append()
8. 主线程写入前端缓冲
9. 缓冲满/超时 → 交换缓冲
10. 后台线程批量写入 LogFile
11. LogFile 调用 AppendFile 写磁盘
12. 自动检查文件大小/时间 → 滚动文件

我现在用一句话给你串起来

我先做了时间戳、级别定义、底层文件写入这些小零件;然后做了日志消息打包、日志入口宏;为了高性能做了异步多缓冲 ;为了方便管理做了日志滚动 ;最后拼成一个主线程无阻塞、多线程安全、自动切文件的工业级日志库。

文件 核心功能
LogCommon.hpp 定义日志级别枚举(TRACE/DEBUG/INFO/WARN/ERROR/FATAL)、日志级别字符串映射、缓冲区大小常量
Timestamp.hpp/.cpp 时间戳封装,支持微秒级时间获取、格式化输出(普通字符串 / 文件命名格式)
LogMessage.hpp/.cpp 日志消息封装,拼接日志头(时间、级别、文件 / 函数 / 行号)和日志内容
Logger.hpp/.cpp 日志器核心类,提供日志宏(LOG_TRACE/LOG_DEBUG 等)、输出 / 刷新函数设置、日志级别控制
AppendFile.hpp/.cpp 底层文件写入封装,支持大缓冲区、非阻塞写入、字节计数
LogFile.hpp/.cpp 日志文件管理,支持文件滚动(按大小 / 时间)、线程安全写入、自动刷新
AsynLogging.hpp/.cpp 异步日志实现,后台线程批量写入日志,减少主线程阻塞
CountDownLatch.hpp/.cpp 倒计时锁,用于异步日志线程初始化同步

第八阶段:面试官最爱问的 10 题

1. 你们日志系统为什么用异步?

同步日志会阻塞业务线程,高并发下性能极差。异步日志将 IO 交给后台线程,主线程只写内存,无阻塞、高吞吐

2. 异步日志怎么实现的?

采用多缓冲技术

  • 前端缓冲区接收日志
  • 后端线程负责写入
  • 缓冲区满或定时则交换缓冲区,批量写入

3. 日志级别有什么用?

用于过滤日志。线上环境只开启 INFO/WARN/ERROR,减少日志量,提升性能。

4. 日志文件滚动是什么?

防止文件无限变大。按大小时间自动创建新文件,方便管理、压缩、清理。

5. 日志系统线程安全吗?

安全。共享缓冲区加锁、文件写入加锁、异步线程单独处理 IO。

6. 你们日志的性能为什么高?

  • 异步非阻塞
  • 批量写入磁盘(减少 IO)
  • 缓冲区减少系统调用
  • 锁粒度小

7. FATAL 日志做了什么?

打印错误信息,然后直接终止进程,用于严重错误。

8. 日志为什么要带文件名和行号?

方便快速定位代码问题。

9. 时间戳为什么用微秒?

高并发场景下,同一毫秒可能产生大量日志,微秒能区分顺序。

10. 这套日志能用于生产环境吗?

完全可以。它是仿照 Muduo 日志库设计的,工业级标准,高并发稳定可靠。

相关推荐
_下雨天.2 小时前
PostgreSQL日常维护
数据库·postgresql
神の愛2 小时前
本地连接MySql数据库报错??
数据库·mysql
黑牛儿2 小时前
MySQL 索引实战详解:为什么B+类型的索引查询更快
数据库·mysql
向上的车轮2 小时前
如何用DeepSeek定制大模型——智能Text-to-SQL专家系统
数据库·sql
西西弟2 小时前
网络编程基础之TCP基本通信
服务器·网络·网络协议·tcp/ip
一个有温度的技术博主2 小时前
Redis主从同步进阶:深入理解增量同步与性能优化
数据库·redis·性能优化
榮華2 小时前
DOTA全图透视辅助下载DOTA全图科技辅助下载DOTA外挂下载魔兽争霸WAR3全图下载
数据库·科技·游戏·游戏引擎·游戏程序·ai编程·腾讯云ai代码助手
Utopia^2 小时前
Flutter 框架跨平台鸿蒙开发 - 时光倒流
服务器·flutter·华为·harmonyos
蓝眸少年CY2 小时前
Hbase - 入门到实战
大数据·数据库·hbase