serilog的async装饰器在高并发下可能变慢,因其仅用单后台线程串行消费blockingcollection队列,磁盘/网络i/o阻塞会导致前端线程等待、内存上涨、gc压力增大甚至丢日志;且不减少序列化开销,轻量sink(如console)反而因调度开销得不偿失。

为什么 Serilog 的 Async 装饰器在高并发下可能变慢
Serilog 本身不自带异步 sink,Async 是一个同步包装器:它把日志事件排队到内存队列(BlockingCollection),由单个后台线程消费并调用下游 sink 的同步 Write() 方法。这意味着——即使启用了 Async,最终写磁盘/网络仍被串行化,且队列堆积会导致内存上涨、GC 压力增大、甚至丢日志(取决于 OverflowStrategy)。
常见现象:BlockingCollection<t>.Add()</t> 在满队列时阻塞调用线程,尤其当磁盘 I/O 持续延迟(如机械盘、远程文件共享),或 sink(如 FileSink)内部加锁频繁时,前端业务线程会被拖慢。
- 默认队列大小是
10000,超限后行为取决于OverflowStrategy(DropNewest或Wait) -
Async不减少日志序列化开销,高频率结构化日志(如每请求打 5+ 条)会显著加重 CPU 和内存压力 - 若下游 sink 是
Console或Debug,因它们本身轻量,Async反而引入额外调度开销,得不偿失
NLog 的 AsyncTargetWrapper 与线程模型差异
NLog 的异步能力更底层:它支持为任意 Target(包括自定义 Target)套上 AsyncTargetWrapper,并可配置 queueLimit、timeToSleepBetweenBatches 和 batchSize。关键区别在于——它默认使用 ThreadPool 线程(非固定单线程)批量消费,且支持真正的异步写入(如 FileTarget 的 enableFileDelete + concurrentWrites 组合下会用 FileStream.WriteAsync())。
但要注意:AsyncTargetWrapper 默认仍是“同步写 + 多线程调度”,只有目标 Target 显式实现 WriteAsync() 才触发真正异步 I/O。比如 NetworkTarget 支持,但老版本 FileTarget 默认不启用。
- 启用真异步需设置
FileTarget.writeToFileName="true"并确保enableFileDelete="false"(否则回退同步) -
queueLimit过小(如1000)在突发流量下易丢日志;过大(如100000)则 GC 压力陡增 - 若同时开启
concurrentWrites="true"和keepFileOpen="true",需注意 Windows 文件句柄泄漏风险
高并发日志性能瓶颈不在“是否异步”,而在 sink 选型和缓冲策略
实测表明,在 5k+ RPS 的 ASP.NET Core 服务中,Serilog + FileSink 启用 Async 后吞吐仅提升约 12%,而换用 SeqSink(HTTP 异步)或 ElasticsearchSink(批量+连接池)后,CPU 占用下降 40% 以上——因为瓶颈早已从“写本地磁盘”转移到“序列化+网络发送”。
真正有效的优化点:
- 关闭非必要日志字段:
enrich.WithProperty("ThreadId", Thread.CurrentThread.ManagedThreadId)在高并发下开销极大 - 用
Logger.ForContext("RequestId", httpContext.TraceIdentifier)替代字符串拼接,避免重复分配 - 对
Information级别日志启用采样:Filter.ByExcluding(Matching.FromSource("Microsoft"))+ 自定义采样策略 - 本地文件场景优先选
NLog的FileTarget+ArchiveAboveSize+maxArchiveFiles="5",比 Serilog 的RollingFileSink归档更快(后者依赖FileInfo同步扫描)
必须检查的 3 个配置陷阱
很多团队压测时发现日志框架突然吃满 CPU 或内存,问题往往藏在默认配置里:
-
Serilog的MinimumLevel.Verbose()配合Enrich.FromLogContext(),会在每次LogContext.PushProperty()触发ConcurrentDictionary写入,高并发下锁争用严重 -
NLog的internalLogLevel="Info"会记录自身调试日志到internalLogFile,该文件无异步包装,直接拖垮主线程 - 两个框架都默认启用
StackTrace捕获(Exception.ToString()),而Environment.StackTrace是同步且昂贵的操作,应设为captureStackTrace="false"或仅在Error级别启用
高并发下,日志不是“能记下来就行”,而是要主动放弃部分可观测性来保主业务——这点最容易被忽略。











