首先到底要做一个什么东西?
我们要造一个 C++ 高并发异步日志库,功能如下:
- 用
LOG_INFO << "xxx"这种简单写法 - 自动带:时间、级别、文件名、函数名、行号
- 支持级别过滤(TRACE/DEBUG/INFO/WARN/ERROR/FATAL)
- 异步写文件,不卡主线程
- 文件自动滚动(按大小 / 按天)
- 线程安全,多线程一起写不乱
你平时写的:
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 连接成功
组成部分:
- 时间戳 Timestamp
- 日志级别 INFO/DEBUG...
- 文件名(截取最后一段)
- 函数名
__func__ - 行号
__LINE__ - 用户输入的内容
核心函数:
// 构造函数:拼头部
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件事:
- 全局日志级别控制------判断这条日志该不该输出
- 提供输出接口(控制台 / 文件 / 异步)------提供
LOG_INFO等宏给你用 - 提供刷新接口------决定输出到哪(控制台 / 文件 / 网络)
- 线程安全控制
- 析构时输出日志
最关键的设计:利用析构函数输出日志
// 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 个函数:
-
setLogLevel设置过滤级别,低于该级别直接丢弃。
-
setOutput你可以指定输出位置:
- 默认输出到控制台
- 可以改成输出到文件
- 可以改成输出到异步系统
-
output真正执行输出的函数。
-
日志宏 LOG_XXX包装了文件、行号、函数名,让你用起来超级简单。
第三阶段:同步日志
流程超级简单:
LOG_INFO << "hello"
→ 生成 Logger 临时对象
→ 构造 LogMessage,拼头部
→ operator<< 写入内容
→ 析构函数调用 output()
→ output 直接输出到控制台/文件
**缺点:**写磁盘是慢 IO!高并发下,业务线程会被日志卡住,性能暴跌。
所以我们必须做:异步日志
第四阶段:高性能核心 ------ 异步日志 AsyncLogging
为什么要异步?
- 同步:主线程自己写磁盘 → 慢、阻塞
- 异步:主线程只丢到内存 → 极速、不阻塞后台线程慢慢写磁盘
异步日志设计思想:双缓冲 / 多缓冲技术
你可以理解为:两个缓冲区交换使用
- 前端缓冲区:主线程拼命往里写日志
- 后端缓冲区:后台线程拿去写入文件
- 当前端满了,直接交换两个缓冲区
- 后台线程默默写入,完全不影响主线程
这就是高并发日志库的核心!
类似于:两个桶接力倒水
- 前端桶:主线程疯狂写日志
- 后端桶:后台线程拿去写文件
- 前端满了 → 交换两个桶
- 主线程继续写,后台线程慢慢刷盘
关键成员
currentBuffer_:当前写的缓冲区buffers_:装满的缓冲区队列mutex_:保护缓冲区cond_:通知后台线程thread:后台写线程
核心流程
- 用户 append加锁 → 往 currentBuffer 写 → 满了就放入队列 → 唤醒后端
- 后端线程 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 个核心函数
-
append() 主线程调用,把日志塞缓冲区,超快!
-
**threadFunc()**后台线程死循环:
- 等待缓冲区满
- 交换缓冲区
- 写入文件
-
**start() / stop()**启动 / 停止后台线程
第五阶段 :线程同步工具(CountDownLatch)
CountDownLatch.hpp------等线程启动好
作用:确保异步线程真正启动后,主线程才继续跑
用法:
- 初始化计数 1
- 主线程调用
wait() - 线程启动好调用
countDown() - 主线程继续执行
保证:日志线程没启动前,不写日志
第六阶段:文件写入模块------日志最终落地
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 件事:
- 生成文件名(包含时间、主机名、进程 ID)
- 判断是否要滚动文件
- 线程安全的写入接口
滚动规则
- 按大小滚动:默认 128KB,超过就切
- 按时间滚动:每天 0 点自动切新文件
- 每次写入都会检查:是否满了 / 是否跨天
核心函数
// 新建一个日志文件
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. 用户调用 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 日志库设计的,工业级标准,高并发稳定可靠。