第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 新日志——丢得理直气壮。更危险的是,没有任何告警。

处理队列满的策略,常见的有这几种:

  1. 阻塞等待:队列满则当前线程阻塞,直到有空间(变相降级为同步)。
  2. 丢弃新日志:最危险,必须配合监控。
  3. 丢弃旧日志:保留最新日志,淘汰最早。
  4. 切换为同步模式:队列满自动降级,等空间释放后再切回异步。

我的建议:异步队列的容量不要超过可用内存的 20%,并设置最大条目数和最大字节数双限。队列满时,最好触发告警 + 降级为同步写,至少不丢关键日志。

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,你还会用同步日志吗?

同步日志 vs 异步日志:核心决策树 日志写入方式 同步日志 业务线程直接I/O 延迟高,强一致 适用于低频/关键日志 异步日志 内存队列+后台线程 低延迟,高吞吐 适用于高并发/可容忍丢失

2.7 如何选择?我给你三个判断标准

标准一:看QPS — 单机QPS低于2000?同步完全够用。高于5000?必须上异步或者混合模式。
标准二:看日志的「命」 — 审计、交易流水这类必须落地的,建议同步或异步+确认机制;调试日志、统计日志可以用异步。
标准三:看团队运维能力 — 异步日志一旦出现队列堆积或磁盘满,需要快速告警和降级。团队没能力做故障自愈,就别贸然全量异步。

我自己做新系统时,通常会先上同步写,等压测瓶颈出现再切部分模块为异步。这样稳一点,不会一开始就引入复杂性。

2.8 一个真实的「同步+异步」混合方案

早年我在某电商做过一套日志中间件,规则如下:

  • 每台机器开两个日志通道:同步通道(写关键业务日志),异步通道(写辅助日志);
  • 异步通道队列深度 8192,满时自动降级为同步写(阻塞),等队列空闲再切回来;
  • 每10秒最多允许异步丢1条日志,否则触发邮件告警。

重点提醒:不要把「异步」当作万能灵药。你选异步,就必须准备好兜底——队列、降级、监控、告警一样都不能少。我以前就吃过亏,上线了异步但没配降级,结果流量波峰一来,队列溢出,日志全丢,查问题都找不到日志。

总结一下,没有的好坏,只有合不合适的场景。同步日志适合「必须落盘」的逻辑,异步日志撑起「极致性能」。真正的高手是两者结合,分场景投递。