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的处理方式导致问题。
调试建议
一、先修复子进程调用与输出读取的明显问题
修正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}", };修复输出读取的线程安全与异常处理问题
- 用
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工具分析:
- 获取进程栈信息
进入容器后,执行pstack 1(若未安装则先执行apt install gdb,再用gdb -p 1 -ex "thread apply all bt" -ex quit),查看所有线程的调用栈,定位阻塞线程。 - 跟踪系统调用
执行strace -p 1,查看进程当前卡在哪个系统调用上,比如是否在等待锁、管道或文件IO。 - 检查线程状态
执行ps -eLf | grep dotnet,查看所有线程的状态(S=睡眠,D=不可中断睡眠),若大量线程处于D状态,说明进程在等待IO资源。
三、增强日志与监控
- 给子进程处理代码添加详细日志
在循环中记录关键状态,方便挂起时回溯: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); } - 启用.NET核心诊断日志
在Docker容器中添加环境变量,启用GC、线程池日志:ENV COMPlus_EnableDiagnostics=1 ENV COMPlus_GCLogLevel=1 ENV COMPlus_ThreadPoolLogLevel=1 - 检查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
相关产品推荐
相关产品推荐

