You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 22:01:07