默认FileHandler使Web请求变慢,因其每条日志同步落盘并阻塞线程,高并发下磁盘I/O成瓶颈;ConcurrentRotatingFileHandler通过独立线程+队列实现异步写入,兼容标准接口且无需改业务代码。

为什么默认的FileHandler会让Web请求变慢
因为每条日志都同步落盘,FileHandler.emit() 会阻塞当前线程。在高并发 Web 场景下(比如每秒上百次 API 调用),磁盘 I/O 成为瓶颈,导致响应延迟明显上升,甚至拖垮整个服务。
常见现象包括:logging.info("request handled") 执行耗时从 0.1ms 突增至 5–20ms,time.perf_counter() 对比可复现;压测时 P99 延迟曲线随日志量陡增。
- 根本原因不是日志内容本身,而是每次调用都触发一次
os.write()系统调用 -
RotatingFileHandler在轮转时还会加锁 + rename,进一步放大阻塞 - 即使开了
buffering=1(行缓冲),Python 的TextIOWrapper仍可能触发 flush
concurrent_log_handler 是最稳妥的异步替代方案
它不是“纯异步”,而是用独立线程 + 锁 + 队列实现线程安全的后台写入,兼容所有标准 Handler 接口,无需改业务代码。
安装与基本用法:
立即学习“Python免费学习笔记(深入)”;
pip install concurrent-log-handler
替换原 FileHandler:
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
import logging
from concurrent_log_handler import ConcurrentRotatingFileHandler
<p>logger = logging.getLogger("web")
handler = ConcurrentRotatingFileHandler(
"app.log",
maxBytes=10 <em> 1024 </em> 1024, # 10MB
backupCount=5,
encoding="utf-8"
)
logger.addHandler(handler)
- 它内部使用
threading.Lock保护文件操作,避免多进程写冲突(不像RotatingFileHandler在多进程下易损坏) - 默认启用队列缓冲,
queue_size=1024,满时会阻塞写入线程而非主线程——这点比自己手写 queue+thread 更健壮 - 不依赖
asyncio,和 Flask/FastAPI/Starlette 等同步/异步框架都无兼容问题
自己用 queue + threading 实现异步日志要防三个坑
虽然可控性强,但容易掉进陷阱。如果你坚持自研,务必注意:
- 进程退出时未清空队列 →
log_queue.join()必须在主程序 exit 前显式调用,否则日志丢失 - 未设守护线程 → 后台线程没加
daemon=True,会导致sys.exit()卡住 - 异常未捕获 →
log_worker内部没包try/except,一旦格式化失败或磁盘满,线程静默退出,后续日志全丢
一个最小可用模板:
import logging import queue import threading import atexit <p>_log_queue = queue.Queue() _worker_thread = None</p><p>def _log_worker(): while True: try: record = _log_queue.get(timeout=0.1) if record is None: break logger = logging.getLogger(record.name) logger.handle(record) except queue.Empty: continue except Exception: pass # 不让异常杀死线程 finally: _log_queue.task_done()</p><p>def setup_async_handler(): global _worker_thread _worker_thread = threading.Thread(target=_log_worker, daemon=True) _worker_thread.start() atexit.register(lambda: _log_queue.put(None)) # 退出时发停止信号</p><h1>替换 emit 方法(需在 setup_async_handler() 后执行)</h1><p>_old_emit = logging.FileHandler.emit def _new_emit(self, record): _log_queue.put(record) logging.FileHandler.emit = _new_emit
FastAPI/Starlette 中别用 aiofiles 写日志
异步框架里直接 await aiofiles.open(...).write() 看似合理,但实际效果差,且有隐藏风险:
- Python 的
aiofiles底层仍是线程池 +loop.run_in_executor(),没减少系统调用次数,只是把阻塞挪到线程池里 - 大量小 write 会击穿线程池,默认
max_workers=4,高并发下排队严重 - 无法保证日志顺序:多个协程并发写同一文件,即使加锁也难保时间戳严格有序,对调试不利
真正适合异步框架的是「结构化日志 + 输出到 stdout」,再由 systemd/journald/Nginx 日志聚合器统一处理。例如:
# FastAPI 启动时配置
import logging
import sys
<p>logging.basicConfig(
level=logging.INFO,
format='{"time":"%(asctime)s","level":"%(levelname)s","msg":"%(message)s"}',
handlers=[logging.StreamHandler(sys.stdout)]
)
这样既避开文件 I/O,又保留结构化能力,还方便对接 Loki 或 Datadog。
真正棘手的从来不是“怎么加异步”,而是“哪些日志必须立刻落盘”(比如支付成功、审计事件)——这类日志得单独走同步通道,和普通访问日志隔离。漏掉这个区分,优化就只做了半截。


















