第2章 同步与异步:同步日志 vs 异步日志
这一章,我们来聊一个老生常谈却又常踩坑的话题——日志该同步写还是异步写?
说白了,同步日志就是每产生一条日志,当前线程亲自把它写到磁盘;异步日志则是把日志扔给后台线程去处理,当前线程立刻返回。我见过不少团队一上来就全盘异步,结果该丢日志时照丢不误;也有人死守同步,高并发下把自己系统拖垮。到底该怎么选?我们掰开来看。
2.1 什么是同步日志?
同步日志的逻辑很简单:业务线程调用日志 API,API 内部直接执行 I/O 写磁盘,写完了才返回。我一直觉得这种模式就像吃饭必须等盘子洗好才能上菜——干净,但慢。
// 伪代码示例:同步日志
void log_sync(const std::string& msg) {
std::lock_guard<std::mutex> lock(mutex_);
file_ << msg << std::endl; // 直接写磁盘
}
特点
- 强一致性:日志没写完,业务线程不会往下走。
- 延迟高:一次磁盘 I/O 通常消耗 1~10ms,业务线程被迫等。
- 吞吐受限:单机串行写,qps 到 3000 左右就开始排队。
我在项目中遇到过一家支付公司,他们用同步日志记录每笔交易。线上压测到 2000 TPS 时,日志 I/O 直接占用了 70% 的线程时间,接口响应从 2ms 飙升到 200ms。这就是典型的「日志拖垮业务」。
2.2 什么是异步日志?
异步日志的思路是:把日志塞进一个内存队列,后台线程池专职工出来消费写入磁盘。当前线程秒级返回,业务延迟不受影响。
// 伪代码示例:异步日志(典型生产者-消费者模型)
void log_async(const std::string& msg) {
queue_.push(msg); // 入队,基本无锁或轻量锁
// 后台线程负责:while(true) { auto m = queue_.pop(); file_ << m; }
}
特点
- 低延迟:入队耗时通常 < 1μs(微秒级)。
- 高吞吐:队列削峰填谷,适合突发流量。
- 有丢失风险:若队列满或进程崩溃,未落盘的日志会丢。
嗯,这里要注意:异步不是万能的。我记得有个团队把日志队列设得特别大,结果内存撑爆了 OOM,连业务都停了。异步日志的核心就是「队列满了怎么办」——这是必须提前想好的。
2.3 对比:同步vs异步
| 维度 | 同步日志 | 异步日志 |
|---|---|---|
| 写入时机 | 实时,调用线程亲自写 | 延迟写入,后台线程批量写 |
| 业务延迟影响 | 严重(磁盘I/O等待) | 极轻微(仅入队时间) |
| 最大吞吐量 | 约 2000~5000 条/秒(单机) | 5万~100万+ 条/秒(取决于队列和磁盘) |
| 数据可靠性 | 高,写入成功才返回 | 较低,队列或缓存可能丢失 |
| 适用场景 | 低频、对日志一致性要求极高 | 高并发、可容忍少量日志丢失 |
从表格能看出来,两个极端。但实际项目很少非黑即白,更多是「混合模式」——比如流量低时同步写,峰值时切异步。我习惯把「可丢失」的情况单独拎出来,让业务代码主动标记:这条日志我要你保证落地,否则事务回滚。
2.4 避坑指南:最容易被忽视的「队列满」处理
我曾经接手过一个日志模块,线上经常丢日志。查到最后发现:队列满时,异步线程直接 discard 新日志——丢得理直气壮。更危险的是,没有任何告警。
处理队列满的策略,常见的有这几种:
- 阻塞等待:队列满则当前线程阻塞,直到有空间(变相降级为同步)。
- 丢弃新日志:最危险,必须配合监控。
- 丢弃旧日志:保留最新日志,淘汰最早。
- 切换为同步模式:队列满自动降级,等空间释放后再切回异步。
2.5 框架选择对比:Logback vs Log4j2 vs 自己造的轮子
| 特性 | Logback | Log4j2 | 手工实现(如dp) |
|---|---|---|---|
| 同步Appender | FileAppender | FileAppender | 自定义FileWriter |
| 异步Appender | AsyncAppender | AsyncAppender | Thread + BlockingQueue |
| 队列满策略 | 阻塞或丢弃(可配) | 阻塞、丢弃或降级 | 随便自己写 |
| 批量写入 | 不支持 | 支持(Buffer+Flush) | 自行实现 |
| 丢日志概率 | JVM崩溃时丢 | JVM崩溃时丢 | 取决于实现 |
我用 Logback 比较多,但它异步模式有个坑:AsyncAppender 默认队列大小是 256。在高并发下很容易满,然后静默丢弃。我记得有一次线上排错,发现最重要的一批错误日志全没了,就是因为队列满了。后来我改成了 8192,并加了阻塞模式。
2.6 核心知识体系
下面这张图梳理了同步日志和异步日志的定位、优缺点及选型决策。你想想看——假如你的系统 QPS 是 5000,你还会用同步日志吗?
2.7 如何选择?我给你三个判断标准
标准二:看日志的「命」 — 审计、交易流水这类必须落地的,建议同步或异步+确认机制;调试日志、统计日志可以用异步。
标准三:看团队运维能力 — 异步日志一旦出现队列堆积或磁盘满,需要快速告警和降级。团队没能力做故障自愈,就别贸然全量异步。
我自己做新系统时,通常会先上同步写,等压测瓶颈出现再切部分模块为异步。这样稳一点,不会一开始就引入复杂性。
2.8 一个真实的「同步+异步」混合方案
早年我在某电商做过一套日志中间件,规则如下:
- 每台机器开两个日志通道:同步通道(写关键业务日志),异步通道(写辅助日志);
- 异步通道队列深度 8192,满时自动降级为同步写(阻塞),等队列空闲再切回来;
- 每10秒最多允许异步丢1条日志,否则触发邮件告警。
总结一下,没有的好坏,只有合不合适的场景。同步日志适合「必须落盘」的逻辑,异步日志撑起「极致性能」。真正的高手是两者结合,分场景投递。