Serilog.Sinks.Async生成数千线程问题排查与优化咨询
我们同时使用Serilog的File Sink和Elasticsearch Sink,且均通过Serilog异步Sink将日志处理放在后台线程执行。近期服务器出现全域性能下降,系统工程师抓取完整用户Dump后,定位到问题与Serilog.Sinks.Async.BackgroundWorkerSink相关。
线程堆栈信息
近3000个线程中,某一线程的CLR堆栈如下:
OS Thread Id: 0x30b7c Child SP IP Call Site 00000025AAA8F048 00007ffd91720bb4 [HelperMethodFrame_1OBJ: 00000025aaa8f048] System.Threading.Monitor.ObjWait(Int32, System.Object) 00000025AAA8F170 00007ffce4454638 System.Threading.SemaphoreSlim.WaitUntilCountOrTimeout(Int32, UInt32, System.Threading.CancellationToken) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs @ 462] 00000025AAA8F1C0 00007ffce43f1b4a System.Threading.SemaphoreSlim.Wait(Int32, System.Threading.CancellationToken) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs @ 365] 00000025AAA8F270 00007ffce44b1e27 System.Collections.Concurrent.BlockingCollection`1[[System.__Canon, System.Private.CoreLib]].TryTakeWithNoTimeValidation(System.__Canon ByRef, Int32, System.Threading.CancellationToken, System.Threading.CancellationTokenSource) 00000025AAA8F2F0 00007ffce44b1ce4 System.Collections.Concurrent.BlockingCollection`1+d__68[[System.__Canon, System.Private.CoreLib]].MoveNext() 00000025AAA8F340 00007ffce2ede22d Serilog.Sinks.Async.BackgroundWorkerSink.Pump() 00000025AAA8F390 00007ffce43d6617 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ExecutionContext.cs @ 183] 00000025AAA8F400 00007ffce44124fe System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 2333] 00000025AAA8F700 00007ffd421eaed3 [DebuggerU2MCatchHandlerFrame: 00000025aaa8f700]
大量线程处于SemaphoreSlim.Wait状态,等待可用线程。我们怀疑存在配置错误或优化空间,且问题大概率与File Sink相关。
疑问
- File Sink设置
shared:true是否合理? - Elasticsearch Sink与异步Sink结合使用是否合理?
- Serilog应使用线程池,为何会生成约3000个线程?
当前配置代码
var logCfg = new LoggerConfiguration() .Enrich.WithProperty("machine", System.Environment.MachineName) .WriteTo.Map(keyPropertyName: "$filename", defaultKey: "fallback", configure: (fileName, wt) => wt.Async(c => c.File(formatter: formatter , path: logOptions.AuditPath , shared: true , fileSizeLimitBytes: fileSizeLimitBytes ?? 41943040 , rollingInterval: RollingInterval.Day , rollOnFileSizeLimit: true ) ) ) .WriteTo.Async(c => c.Elasticsearch(new Serilog.Sinks.Elasticsearch.ElasticsearchSinkOptions(new Uri(elasticUrl)) { IndexFormat = "my-audit-" + DateTime.Now.Year, ModifyConnectionSettings = x => x.MyAuthentication(elasticCreds[0], elasticCreds[1]) } ) );
相关NuGet版本
- Serilog.Sinks.Async 1.5.0.0
- Serilog.Sinks.Elasticsearch 8.4.1
- Serilog.Sinks.File 5.0.0
问题解答与优化建议
1. File Sink设置shared:true是否合理?
shared:true仅适用于多进程共享写入同一个日志文件的场景,单进程写入时完全没必要开启——它会引入额外的文件锁逻辑,导致写入线程频繁等待锁,进而阻塞异步Sink的队列处理,这很可能是当前线程等待的直接原因。
如果是多进程场景,也不建议共享同一日志文件,更合理的做法是按进程ID或业务标识拆分日志文件,从根源避免锁竞争。
2. Elasticsearch Sink与异步Sink结合使用是否合理?
不合理。Serilog.Sinks.Elasticsearch从7.x版本开始,默认已经实现了异步批量发送日志的能力,再套一层Serilog.Sinks.Async属于重复异步封装,会额外增加队列开销和线程调度成本,不仅无法提升性能,反而可能拖慢日志处理速度。直接使用Elasticsearch Sink的内置异步能力即可。
3. 为何会生成约3000个线程?
问题出在WriteTo.Map的配置上:Map会根据$filename属性的不同取值,创建独立的File Sink实例,而每个File Sink又被包裹在Async中——每个Async Sink默认会启动一个独立的后台工作线程。如果系统中$filename的取值非常多(比如每个请求生成不同的文件名),就会瞬间创建大量Async Sink实例,每个对应一个线程,最终导致线程数暴涨。这完全违背了线程池复用的设计,属于典型的配置错误。
优化方案
- 移除File Sink的
shared:true配置(单进程场景),多进程场景改为按进程/业务拆分日志文件。 - 移除Elasticsearch Sink外层的
Async封装,直接使用其内置的异步批量发送能力。 - 调整
WriteTo.Map的使用方式:- 如果
$filename取值过多,建议放弃使用Map,改为通过日志模板或过滤器区分内容,统一写入少数文件; - 必须使用Map时,添加
limit参数限制最大Sink实例数(超过限制后会复用旧实例),示例:.WriteTo.Map(keyPropertyName: "$filename", defaultKey: "fallback", limit: 10, configure: (fileName, wt) => wt.File(...) )
- 如果
- 升级Serilog.Sinks.Async到最新版本(当前最新为1.5.1),修复潜在的线程调度问题。
内容的提问来源于stack exchange,提问作者AardVark71

