Linux下SystemD托管.NET服务线程异常增长及ClrMd适配问题
问题描述
我有一个在Windows环境下通过VS22编译调试、部署在Linux系统由SystemD托管的服务,该服务作为MariaDB10数据库的代理,以BackgroundWorker形式通过SignalR为客户端提供服务。
- Windows发布模式运行时,逻辑线程数维持在20-25的合理范围
- Linux系统中运行数分钟后,线程数每秒持续增长,已超过100且仍在上升
已做排查:
- 代码在Windows和Linux环境无差异,CLR线程创建逻辑是现有线程未完成时允许新建线程
- 正在追踪所有修改服务状态及内部结构的API端点的
Monitor.Enter()调用,Windows下运行正常 - 怀疑问题与
UseSystemD()调用有关,但相关文档较少 - 使用ClrMd编写ProcessTracker类扫描线程池,Windows下可正常输出线程信息,但Linux环境下该类依赖Kernel32.dll无法运行
- 服务以Linux64原生单文件、发布模式发布,无.NET基础依赖
相关代码:
public static IHostBuilder CreateWebHostBuilder(string[] args) { string curDir = MondayConfiguration.DefineCurrentDir(); IConfigurationRoot config = new ConfigurationBuilder() .SetBasePath(curDir) .AddJsonFile("servicelocationoptions.json", optional: false, reloadOnChange: true) #if DEBUG .AddJsonFile("appSettings.Debug.json") #else .AddJsonFile("appSettings.json") #endif .Build(); return Host.CreateDefaultBuilder(args) .UseContentRoot(curDir) .ConfigureAppConfiguration((_, configuration) => { configuration .AddIniFile("appSettings.ini", optional: true, reloadOnChange: true) #if DEBUG .AddJsonFile("appSettings.Debug.json") #else .AddJsonFile("appSettings.json") #endif .AddJsonFile("servicelocationoptions.json", optional: false, reloadOnChange: true); }) .UseSerilog((_, services, configuration) => configuration .ReadFrom.Configuration(config, sectionName: "AppLog") .ReadFrom.Services(services) .Enrich.FromLogContext() .WriteTo.Console()) .ConfigureServices((hostContext, services) => { services .Configure<ServiceLocationOptions>(hostContext.Configuration.GetSection(key: nameof(ServiceLocationOptions))) .Configure<HostOptions>(opts => opts.ShutdownTimeout = TimeSpan.FromSeconds(30)); }) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); ServiceLocationOptions locationOptions = config.GetSection(nameof(ServiceLocationOptions)).Get<ServiceLocationOptions>(); string url = locationOptions.HttpBase + "*:" + locationOptions.Port; webBuilder.UseUrls(url); }) .UseSystemd(); }
private async Task Ping() { await _containerServer.SyslogQueue.Writer.WriteAsync(( LogLevel.Information, $"Monday Service active at: {DateTime.UtcNow.ToLocalTime()}")); string processMessage = ProcessTracker.Scan(); await _containerServer.SyslogQueue.Writer.WriteAsync((LogLevel.Information, processMessage)); _logger.DebugInfo() .Information("Monday Service active at: {Now}", DateTime.UtcNow.ToLocalTime()); }
public static class ProcessTracker { static ProcessTracker() { } public static string Scan() { StringBuilder sb = new(); string answer = $"Active Threads{Environment.NewLine}"; int countThread = 0; var pid = Process.GetCurrentProcess().Id; using (var dataTarget = DataTarget.AttachToProcess(pid, 5000, AttachFlag.Passive)) { ClrInfo version = dataTarget.ClrVersions[0]; var runtime = version.CreateRuntime(); foreach (ClrThread thread in runtime.Threads) { try { sb = new(); if (!thread.IsAlive) continue; sb.Append($"Thread {thread.OSThreadId:X}:"); countThread++; ClrException? currException = thread.CurrentException; if (currException is ClrException ex) sb.AppendLine($"Exception: {ex.Address:X} ({ex.Type.Name}), HRESULT={ex.HResult:X}"); sb.AppendLine(" -------> Managed Call stack:"); var collection = thread.EnumerateStackTrace().ToList(); foreach (ClrStackFrame frame in collection) { sb.AppendLine($" {frame}"); } } catch { //skip to the next } finally { answer += sb.ToString(); } } } answer += $"{Environment.NewLine} Total thread listed: {countThread}"; return answer; } }
解决思路
1. 替换跨平台不兼容的线程调试工具
ClrMd依赖Windows的Kernel32.dll,无法在Linux下运行,建议用以下跨平台方案替代:
- 使用.NET原生
System.Diagnostics类获取线程基础信息,先统计线程数量和状态:public static class ProcessTracker { public static string Scan() { StringBuilder sb = new StringBuilder(); var process = Process.GetCurrentProcess(); sb.AppendLine("Active Threads"); int countThread = 0; foreach (ProcessThread thread in process.Threads) { try { sb.AppendLine($"Thread ID: {thread.Id}, State: {thread.ThreadState}, Priority: {thread.PriorityLevel}"); countThread++; } catch { // 跳过无法获取信息的线程 } } sb.AppendLine($"\nTotal thread listed: {countThread}"); return sb.ToString(); } } - 使用Linux下的
dotnet-dump工具生成进程转储文件,分析托管线程栈:# 安装工具 dotnet tool install -g dotnet-dump # 收集转储 dotnet-dump collect -p <进程ID> # 分析转储 dotnet-dump analyze <dump文件路径>
2. 排查线程泄漏核心场景
线程持续增长的本质是线程未被正确回收,重点检查以下点:
- BackgroundWorker管理:是否重复创建BackgroundWorker且未释放,或
DoWork方法存在同步阻塞操作(如同步等待数据库、SignalR调用) - SignalR连接与操作:客户端连接是否未正确断开,Hub中是否存在长时间同步任务占用线程池
- 数据库操作:MariaDB客户端是否使用同步API,连接池配置是否合理,是否存在线程等待数据库响应无法释放的情况
- 锁资源泄漏:所有
Monitor.Enter()是否都对应Monitor.Exit(),异常分支中是否遗漏锁释放,避免线程死锁或挂起 - 异步代码同步阻塞:是否存在
Task.Wait()、Task.Result等阻塞异步任务的代码,导致线程池线程被占用无法回收
3. 验证UseSystemD()的影响
虽然UseSystemD()主要处理服务生命周期,但仍需排除其间接影响:
- 临时移除
UseSystemD(),以普通进程运行服务,观察线程数是否仍增长 - 检查SystemD服务配置,确保
Type=notify(.NET默认配置),避免进程监控逻辑导致额外线程创建 - 验证
ShutdownTimeout=30秒是否合理,避免服务关闭时线程无法正常退出
4. 优化异步逻辑
调整Ping方法,避免线程扫描操作阻塞心跳线程:
private async Task Ping() { await _containerServer.SyslogQueue.Writer.WriteAsync(( LogLevel.Information, $"Monday Service active at: {DateTime.UtcNow.ToLocalTime()}")); // 异步执行线程扫描,不阻塞心跳主线程 string processMessage = await Task.Run(() => ProcessTracker.Scan()); await _containerServer.SyslogQueue.Writer.WriteAsync((LogLevel.Information, processMessage)); _logger.DebugInfo() .Information("Monday Service active at: {Now}", DateTime.UtcNow.ToLocalTime()); }
内容的提问来源于stack exchange,提问作者AlessandroParma
相关产品推荐
相关产品推荐

