双缓冲本身不提升io吞吐量,真正起效的是避免write()阻塞、批量合并写入及减少fsync()次数;单独增加缓冲区而不控制刷盘节奏反而可能降低吞吐。

双缓冲本身不提升 IO 吞吐量,真正起作用的是「避免 write() 阻塞主线程 + 批量合并 + 减少 fsync() 次数」。单独堆两个 buffer 而不控制刷盘节奏,吞吐反而可能下降。
为什么 std::atomic 直接交换缓冲区指针容易丢日志
问题不在交换动作本身,而在交换时机缺乏语义保护:生产者可能刚写入一半(比如只写了 12 字节的 log_entry 头),就触发了缓冲区翻转,消费者读到未对齐/不完整结构体,解析崩溃或内容错乱。
- 必须配合三状态原子机:
READY_TO_WRITE→SWITCHING→READY_TO_FLUSH,不能用std::atomic_flag这种双态标识 - 生产者在写入前检查状态是否为
READY_TO_WRITE;若为SWITCHING,需自旋等待或 fallback 到thread_local char[4096]临时缓冲 - 消费者确认满/超时后,先原子置为
SWITCHING,再交换指针,最后置READY_TO_FLUSH—— 中间任何一步失败都需回滚或告警 - 缓冲区 struct 必须按硬件对齐(如
alignas(8)),且每个 log entry 尾部需显式std::atomic_thread_fence(std::memory_order_release)
write() 批量合并比单条 writev() 快 3–5 倍的关键条件
不是调了 writev() 就自动快,关键在于内存布局和系统调用密度。
- 所有待刷日志必须连续存放在一块物理内存中(例如 memcpy 到一个
std::string或预分配char buf[64*1024]),不能是队列里分散的LogEntry*指针数组 - 单次
write()数据量建议 ≥ 4KB 且 ≤ 64KB:小于 4KB 容易被 page cache 吞掉(O_DIRECT又难配),大于 64KB 可能触发内核同步路径 - 必须禁用
O_SYNC和O_DSYNC—— 它们会让每次write()等磁盘确认,双缓冲彻底失效 - 落盘线程每轮最多取 64 条日志,拼成一整块再
write();间隔用std::this_thread::sleep_for(1ms),而非忙等
fsync() 触发策略不当会让吞吐归零
每条日志后跟 fsync() 是最常见错误,实测延迟从微秒级跳到毫秒级,吞吐跌 90%+。
- 只在三种情况下调用:
buffer 满、空闲超 100ms、程序显式 flush()—— 其他时间全靠内核 page cache 缓冲 - 用
fdatasync()替代fsync(),跳过元数据更新(如 mtime),快 20%~40% - 打开文件时加
O_APPEND | O_WRONLY | O_CREAT,但绝不能加O_SYNC;如需强持久化,改用io_uring的IORING_OP_FSYNC单独提交 - 崩溃恢复依赖
fsync()成功返回,因此调用后必须检查返回值;-1且errno == EINTR可重试,EBADF或EINVAL需立即停刷并报错
最容易被忽略的点:缓冲区大小不是越大越好。实测 64KB 是 x86 L1 cache 友好边界,超过后 memcpy 开销上升,且单次 write() 更易被调度器打断;低于 8KB 则切换太频繁,原子状态机开销占比飙升。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











