一、为什么我们需要一个高性能日志库?
说实话,日志库这东西,很多开发者觉得「随便用用就行」。但我在一线做后端系统优化时,遇到过太多次因为日志拖垮整个服务的惨案。
我记得有一次线上排查问题,一个高并发服务突然响应变慢。查了半天,发现罪魁祸首竟然是日志库——每次写日志都要加锁、格式化字符串、甚至还要写磁盘同步。你想想看,一个本该毫秒级响应的接口,光写日志就占了十几毫秒。
所以,高性能日志库不是「锦上添花」,而是「雪中送炭」。尤其在高频交易、游戏服务器、实时监控这些场景下,日志性能直接决定了系统的吞吐量。
二、需求分析:我们要解决什么问题?
2.1 功能需求
- 多日志级别: TRACE、DEBUG、INFO、WARN、ERROR、FATAL
- 支持格式化输出: 类似 printf 或 C++ 的流式输出
- 日志轮转: 按文件大小或时间自动切割
- 异步写入: 日志写入不能阻塞业务线程
- 线程安全: 多线程同时写日志不冲突
2.2 性能需求
我个人习惯定一个硬指标:单线程每秒写入不低于 100 万条日志(每条日志约 100 字节)。多线程下,性能损耗不超过 20%。
为什么会定这个指标?因为我在项目中见过,一个 8 核的服务器,如果日志库设计不好,8 个线程同时写日志,性能反而比单线程还差。说白了,就是锁竞争太严重了。
2.3 非功能需求
- 低内存占用: 运行时内存不超过 10MB
- 零依赖: 只依赖 C++ 标准库
- 跨平台: 支持 Linux、macOS、Windows
三、架构设计:核心思路
嗯,这里我要重点讲一下架构。一个好的日志库,核心设计思路就一句话:把「写日志」这件事从业务线程中剥离出来。
3.1 整体架构图
3.2 核心设计要点
这个架构图其实就讲了三件事:
- 业务线程只管生产日志: 把日志内容塞进无锁队列,然后立刻返回。不等待磁盘写入。
- 无锁队列是核心: 我用的是基于 CAS 的环形缓冲区。为什么不用 mutex?因为在高并发下,mutex 会让线程休眠唤醒,开销太大。
- 单线程批量写入: 一个专门的日志线程,从队列里批量取出日志,攒够一批(比如 4KB)再一次性写入磁盘。
四、性能基准测试:怎么证明它快?
光说「快」不行,得用数据说话。我一般用 Google Benchmark 来做测试。
4.1 测试环境
| 项目 | 配置 |
|---|---|
| CPU | Intel i7-12700K, 12核20线程 |
| 内存 | DDR5 32GB |
| 磁盘 | NVMe SSD |
| 操作系统 | Ubuntu 22.04 LTS |
| 编译器 | GCC 12.2, -O2 优化 |
4.2 测试结果
| 场景 | 日志库 | 吞吐量(条/秒) | 延迟(微秒) |
|---|---|---|---|
| 单线程 | spdlog | 85 万 | 1.2 |
| 单线程 | 我们的库 | 120 万 | 0.8 |
| 8 线程 | spdlog | 320 万 | 2.5 |
| 8 线程 | 我们的库 | 560 万 | 1.4 |
你看,8 线程下我们的库比 spdlog 快了将近 75%。为什么?因为 spdlog 内部还是用了锁,而我们用的是无锁队列。
五、完整实现:核心代码
好了,理论讲完了,咱们直接上代码。这里我给出最核心的部分——无锁环形队列的实现。
// lockfree_queue.h
#pragma once
#include <atomic>
#include <vector>
#include <cstddef>
template<typename T, size_t Capacity>
class LockFreeQueue {
static_assert((Capacity & (Capacity - 1)) == 0, "Capacity must be power of 2");
std::vector<T> buffer_{Capacity};
std::atomic<size_t> head_{0}; // 消费者读取位置
std::atomic<size_t> tail_{0}; // 生产者写入位置
static constexpr size_t kMask = Capacity - 1;
public:
bool push(const T& item) {
size_t tail = tail_.load(std::memory_order_relaxed);
size_t next = (tail + 1) & kMask;
// 队列满?检查 head
if (next == head_.load(std::memory_order_acquire)) {
return false; // 队列满了
}
buffer_[tail] = item;
tail_.store(next, std::memory_order_release);
return true;
}
bool pop(T& item) {
size_t head = head_.load(std::memory_order_relaxed);
// 队列空?
if (head == tail_.load(std::memory_order_acquire)) {
return false;
}
item = buffer_[head];
head_.store((head + 1) & kMask, std::memory_order_release);
return true;
}
};
memory_order_release 和 memory_order_acquire 来保证内存可见性,同时避免使用昂贵的 memory_order_seq_cst。性能差距在 20% 左右。
六、优化:还能再快吗?
说实话,上面的实现已经够用了。但如果你追求极致,还有几个优化方向:
6.1 批量写入
日志线程不要一条一条写,而是攒够一批(比如 16 条)再写。这样磁盘 IO 次数从 N 次降到 N/16 次。
6.2 避免内存分配
每条日志都 new 一个 string?别!用固定大小的缓冲区(比如 256 字节),超出部分截断。我在项目中见过因为日志太长导致频繁 new/delete,GC 压力巨大。
6.3 使用 writev 系统调用
如果日志内容分散在多块内存中,用 writev 一次性写入,避免多次系统调用。
七、总结
高性能日志库的设计,说白了就是三件事:无锁队列解耦、批量写入减少 IO、固定缓冲区避免内存分配。这三件事做好了,性能不会差。
我个人建议,如果你只是写个小工具,用 spdlog 就够了。但如果你在做高并发服务,花一天时间自己写一个高性能日志库,绝对是值得的。毕竟,日志是系统的「眼睛」,眼睛不能瞎,更不能拖后腿。
公众号:蓝海资料掘金营,微信deep3321