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

ASP.NET Core 9在Docker容器运行子进程时随机挂起,求排查方案

问题描述

应用环境

我有一个部署在Linux Docker容器中的ASP.NET Core 9应用,使用如下Dockerfile构建:

FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app

# Install base components
RUN apt update && \
    apt install -y curl htop nano
RUN curl -sSL https://get.docker.com/ | sh

# Set up the build env
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /source

# Install NuGet Packages
COPY "src/Dictus.AsrEvaluator.Backend/Dictus.AsrEvaluator.Backend.csproj" "src/Dictus.AsrEvaluator.Backend/Dictus.AsrEvaluator.Backend.csproj"
COPY "src/SunShared/Dictus.Sun.Shared/Dictus.Sun.Shared.csproj" "src/SunShared/Dictus.Sun.Shared/Dictus.Sun.Shared.csproj"
RUN dotnet restore "src/SunShared/Dictus.Sun.Shared/Dictus.Sun.Shared.csproj"
RUN dotnet restore "src/Dictus.AsrEvaluator.Backend/Dictus.AsrEvaluator.Backend.csproj"

# Build the app
COPY "src/" "src/"
ARG VersionSuffix=0
RUN dotnet publish "src/Dictus.AsrEvaluator.Backend/Dictus.AsrEvaluator.Backend.csproj" -c Release -o /app /p:VersionSuffix=$VersionSuffix

# Copy the app to the final build image
FROM base AS final
WORKDIR /app
COPY --from=build /app .

# Setup defaults
HEALTHCHECK CMD curl --max-time 10 --fail http://localhost:80/health || exit 1
ENV ASPNETCORE_HTTPS_PORTS=80
ENTRYPOINT ["dotnet", "Dictus.AsrEvaluator.Backend.dll"]

挂起现象

应用偶尔会完全挂起,表现为:

  • 访问服务器无响应,新增的仅返回常量的接口也无法返回结果;
  • Docker容器仍在运行,可通过docker exec进入容器,dotnet进程仍在运行,但CPU使用率为0,内存占用正常,进程处于空闲状态;
  • 容器内top命令输出如下:
PID USER    PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
1 root      20   0  263.8g 641220 196992 S   0.0   2.0 242:33.04 dotnet

诊断工具尝试情况

我尝试通过以下工具获取应用信息,但均失败:

  • dotnet-counters monitor --process-id 1:完全挂起,无法通过CTRL+C退出,只能关闭终端;
  • dotnet-trace collect -p 1:完全挂起,无法通过CTRL+C退出,只能关闭终端;
  • dotnet-dump collect --process-id 1:无法生成输出文件。

应用日志在挂起时停止输出,无额外信息。

挂起时机与相关代码

应用偶尔会收到新任务,处理时会准备文件和文件夹,然后启动2个并行运行的子进程。挂起发生在这2个子进程运行期间,且随机性强,可能连续运行数周或数天出现一次。我观察到两个子进程均能成功完成,因此怀疑问题不在子进程本身。

应用启动子进程并监听其STDOUT和STDERR的代码如下:

ProcessStartInfo startInfo = new ProcessStartInfo
{
     WindowStyle = ProcessWindowStyle.Hidden,
     RedirectStandardOutput = true,
     RedirectStandardError = true,
     UseShellExecute = false,
     FileName = "docker -v /job_data:/job_data run my_other_docker",
     Arguments = arguments,
};

using Process process = new Process
{
     StartInfo = startInfo,
     EnableRaisingEvents = true
};

// Outputs
StringBuilder stdOut = new StringBuilder();
StringBuilder stdErr = new StringBuilder();
 
// Runs the command
process.Start();

ReadStream(process.StandardOutput, stdOut, progress, true);
ReadStream(process.StandardError, stdErr, progress, false);
 
while (!process.HasExited || !isReadStdOutDone || !isReadErrOutDone)
{
     if (token.IsCancellationRequested)
     {
         process.Kill();

         token.ThrowIfCancellationRequested();
     }

     await Task.Delay(100);
}

// Reads the output
string stdout = stdOut.ToString();
string stderr = stdErr.ToString();
 
private void ReadStream(
     StreamReader stream,
     StringBuilder read,
     IProgress<string>? progress,
     bool isStdOut)
{
     Task.Run(async () =>
     {
         var line = CleanLogLine(await stream.ReadLineAsync());
         while (line != null)
         {
             line = $"{name} - {line}";
             read.AppendLine(line);
             if (progress != null)
                 progress.Report(line);
             line = CleanLogLine(await stream.ReadLineAsync());
         }
         if (isStdOut)
             isReadStdOutDone = true;
         else
             isReadErrOutDone = true;
     });
}

private string? CleanLogLine(string? line)
{
     if (line != null &&
         line.Count(x => x == '\b') > 1000)
     {
         line = line[0..1000];
     }
     return line;
}

我的猜想

  • 并非内存泄漏(无内存不足异常);
  • 不确定是否为线程饥饿,因dotnet诊断工具无法输出信息;
  • 不确定是否是STDOUT/STDERR的处理方式导致问题。

调试建议

一、先修复子进程调用与输出读取的明显问题

  1. 修正ProcessStartInfo的参数错误
    当前代码中FileName设置为完整的docker命令是错误的,当UseShellExecute=false时,FileName必须是可执行文件路径,所有参数需放在Arguments中:

    ProcessStartInfo startInfo = new ProcessStartInfo
    {
        WindowStyle = ProcessWindowStyle.Hidden,
        RedirectStandardOutput = true,
        RedirectStandardError = true,
        UseShellExecute = false,
        FileName = "docker",
        Arguments = $"-v /job_data:/job_data run my_other_docker {arguments}",
    };
    
  2. 修复输出读取的线程安全与异常处理问题

    • 用volatile修饰isReadStdOutDone和isReadErrOutDone,确保主循环能及时看到后台任务的更新:
      private volatile bool isReadStdOutDone = false;
      private volatile bool isReadErrOutDone = false;
      
    • 在ReadStream的异步任务中添加异常捕获,避免任务静默失败导致主循环无限等待:
      Task.Run(async () =>
      {
          try
          {
              var line = CleanLogLine(await stream.ReadLineAsync());
              while (line != null)
              {
                  line = $"{name} - {line}";
                  read.AppendLine(line);
                  progress?.Report(line);
                  line = CleanLogLine(await stream.ReadLineAsync());
              }
          }
          catch (Exception ex)
          {
              Console.WriteLine($"Error reading {isStdOut ? "stdout" : "stderr"}: {ex.Message}");
          }
          finally
          {
              if (isStdOut)
                  isReadStdOutDone = true;
              else
                  isReadErrOutDone = true;
          }
      });
      
    • 或者改用更可靠的事件驱动读取方式,避免手动轮询:
      process.OutputDataReceived += (sender, e) =>
      {
          if (!string.IsNullOrEmpty(e.Data))
          {
              var line = $"{name} - {CleanLogLine(e.Data)}";
              stdOut.AppendLine(line);
              progress?.Report(line);
          }
      };
      process.ErrorDataReceived += (sender, e) =>
      {
          if (!string.IsNullOrEmpty(e.Data))
          {
              var line = $"{name} - {CleanLogLine(e.Data)}";
              stdErr.AppendLine(line);
              progress?.Report(line);
          }
      };
      
      process.Start();
      process.BeginOutputReadLine();
      process.BeginErrorReadLine();
      
      await process.WaitForExitAsync(token);
      

二、使用Linux原生诊断工具排查进程挂起

由于.NET诊断工具无法响应,说明进程可能陷入原生代码死锁或GC挂起,直接用Linux工具分析:

  1. 获取进程栈信息
    进入容器后,执行pstack 1(若未安装则先执行apt install gdb,再用gdb -p 1 -ex "thread apply all bt" -ex quit),查看所有线程的调用栈,定位阻塞线程。
  2. 跟踪系统调用
    执行strace -p 1,查看进程当前卡在哪个系统调用上,比如是否在等待锁、管道或文件IO。
  3. 检查线程状态
    执行ps -eLf | grep dotnet,查看所有线程的状态(S=睡眠,D=不可中断睡眠),若大量线程处于D状态,说明进程在等待IO资源。

三、增强日志与监控

  1. 给子进程处理代码添加详细日志
    在循环中记录关键状态,方便挂起时回溯:
    int loopCount = 0;
    while (!process.HasExited || !isReadStdOutDone || !isReadErrOutDone)
    {
        loopCount++;
        if (loopCount % 100 == 0) // 每10秒记录一次
        {
            Console.WriteLine($"Waiting for process exit: HasExited={process.HasExited}, StdOutDone={isReadStdOutDone}, StdErrDone={isReadErrOutDone}, LoopCount={loopCount}");
        }
        if (token.IsCancellationRequested)
        {
            process.Kill();
            token.ThrowIfCancellationRequested();
        }
        await Task.Delay(100);
    }
    
  2. 启用.NET核心诊断日志
    在Docker容器中添加环境变量,启用GC、线程池日志:
    ENV COMPlus_EnableDiagnostics=1
    ENV COMPlus_GCLogLevel=1
    ENV COMPlus_ThreadPoolLogLevel=1
    
  3. 检查ASP.NET Core线程池配置
    确认线程池设置合理,避免任务无法调度:
    ThreadPool.SetMinThreads(100, 100); // 根据实际情况调整
    

四、优化Docker镜像以支持.NET诊断工具

修改Dockerfile的base阶段,安装.NET诊断工具与Linux调试工具:

FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app

# Install base components and diagnostic tools
RUN apt update && \
    apt install -y curl htop nano gdb pstack strace && \
    curl -sSL https://get.docker.com/ | sh && \
    dotnet tool install --global dotnet-counters dotnet-trace dotnet-dump && \
    export PATH="$PATH:/root/.dotnet/tools"

# 后续步骤不变...

内容的提问来源于stack exchange,提问作者Mr. JWolf

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 18:44:57