c# 在高并发下,日志框架(Serilog/NLog)的异步写入性能

5次阅读

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

c# 在高并发下,日志框架(Serilog/NLog)的异步写入性能

为什么 Serilog 的 Async 装饰器在高并发下可能变慢

Serilog 本身不自带异步 sink,Async 是一个同步包装器:它把日志事件排队到内存队列(BlockingCollection),由单个后台线程消费并调用下游 sink 的同步 Write() 方法。这意味着——即使启用了 Async,最终写磁盘/网络仍被串行化,且队列积会导致内存上涨、GC 压力增大、甚至丢日志(取决于 OverflowStrategy)。

常见现象:BlockingCollection<t>.Add()</t> 在满队列时阻塞调用线程,尤其当磁盘 I/O 持续延迟(如机械盘、远程文件共享),或 sink(如 FileSink)内部加锁频繁时,前端业务线程会被拖慢。

  • 默认队列大小是 10000,超限后行为取决于 OverflowStrategyDropNewestWait
  • Async 不减少日志序列化开销,高频率结构化日志(如每请求打 5+ 条)会显著加重 CPU 和内存压力
  • 若下游 sink 是 ConsoleDebug,因它们本身轻量,Async 反而引入额外调度开销,得不偿失

NLog 的 AsyncTargetWrapper 与线程模型差异

NLog 的异步能力更底层:它支持为任意 Target(包括自定义 Target)套上 AsyncTargetWrapper,并可配置 queueLimittimeToSleepBetweenBatchesbatchSize。关键区别在于——它默认使用 ThreadPool 线程(非固定单线程)批量消费,且支持真正的异步写入(如 FileTargetenableFileDelete + 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%,而换用 SeqSinkhttp 异步)或 ElasticsearchSink(批量+连接池)后,CPU 占用下降 40% 以上——因为瓶颈早已从“写本地磁盘”转移到“序列化+网络发送”。

真正有效的优化点:

  • 关闭非必要日志字段:enrich.WithProperty("ThreadId", Thread.CurrentThread.ManagedThreadId) 在高并发下开销极大
  • Logger.ForContext("RequestId", httpContext.TraceIdentifier) 替代字符串拼接,避免重复分配
  • Information 级别日志启用采样:Filter.ByExcluding(Matching.FromSource("microsoft")) + 自定义采样策略
  • 本地文件场景优先选 NLogFileTarget + ArchiveAboveSize + maxArchiveFiles="5",比 Serilog 的 RollingFileSink 归档更快(后者依赖 FileInfo 同步扫描)

必须检查的 3 个配置陷阱

很多团队压测时发现日志框架突然吃满 CPU 或内存,问题往往藏在默认配置里:

  • SerilogMinimumLevel.Verbose() 配合 Enrich.FromLogContext(),会在每次 LogContext.PushProperty() 触发 ConcurrentDictionary 写入,高并发下锁争用严重
  • NLoginternalLogLevel="Info" 会记录自身调试日志到 internalLogFile,该文件无异步包装,直接拖垮主线程
  • 两个框架都默认启用 StackTrace 捕获(Exception.ToString()),而 Environment.StackTrace 是同步且昂贵的操作,应设为 captureStackTrace="false" 或仅在 Error 级别启用

高并发下,日志不是“能记下来就行”,而是要主动放弃部分可观测性来保主业务——这点最容易被忽略。

text=ZqhQzanResources