由 上篇 得知 .NET Core 應用程式可以接收 SIGINT/SIGTERM訊號,來完成 graceful shutdown;在處理批次流程中,當應用程式接收到 SIGINT/SIGTERM 訊號後,就要進入狀態的保存,避免服務被強制中斷,導致狀態混亂。預設 Container 預設等待 10 sec,也就是這個時間範圍內就要完成狀態保存,如果超過時間就可以考慮送出 Timeout 參數,延長工作關閉流程。Linux 執行應用程式的時候有區分 shell model 以及 exec mode,若使用不當,會導致接收不到 SIGINT/SIGTERM 訊號,無法 graceful shutdown。
開發環境
- Windows 11 Home
- Rider 2024.3.6
- WSL2 + Ubuntu 24.04
- .NET Core 8 Console App
觀察容器接收 SIGINT/SIGTERM 訊號狀況
新增一個 Console App 專案並使用以下程式碼,如果正確的收到關閉訊號,會快的印出成功;反之,會等到預設時間到達後,才強制關閉應用程式。
GracefulShutdownService1.cs 內容如下:
class GracefulShutdownService1 : BackgroundService
{
private readonly ILogger<GracefulShutdownService1> _logger;
public GracefulShutdownService1(ILogger<GracefulShutdownService1> logger)
{
this._logger = logger;
}
private int _count = 1;
public override Task StartAsync(CancellationToken cancellationToken)
{
this._logger.LogInformation($"{DateTime.Now} 服務已啟動!");
return base.StartAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (true)
{
if (stoppingToken.IsCancellationRequested)
{
break;
}
this._logger.LogInformation($"{DateTime.Now} ,執行次數:{_count++}");
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
}
}
public override Task StopAsync(CancellationToken cancellationToken)
{
this._logger.LogInformation($"{DateTime.Now} 完成!");
return base.StopAsync(cancellationToken);
}
}
在 Program.cs 攔截了相關的訊號
using System.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Runtime.Loader;
using Lab.GracefulShutdown.Net8;
using Serilog;
using Serilog.Formatting.Json;
var sigintReceived = false;
var formatter = new JsonFormatter();
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.Enrich.FromLogContext()
.WriteTo.Console(formatter)
.WriteTo.File("logs/host-.txt", rollingInterval: RollingInterval.Day)
.CreateBootstrapLogger()
;
Log.Information($"Process id: {Process.GetCurrentProcess().Id}");
Log.Information("等待以下訊號 SIGINT/SIGTERM");
Console.CancelKeyPress += (sender, e) =>
{
e.Cancel = true;
Log.Information("已接收 SIGINT (Ctrl+C)");
sigintReceived = true;
};
AssemblyLoadContext.Default.Unloading += ctx =>
{
if (!sigintReceived)
{
Log.Information("已接收 SIGTERM,AssemblyLoadContext.Default.Unloading");
}
else
{
Log.Information("@AssemblyLoadContext.Default.Unloading,已處理 SIGINT,忽略 SIGTERM");
}
};
AppDomain.CurrentDomain.ProcessExit += (sender, e) =>
{
if (!sigintReceived)
{
Log.Information("已接收 SIGTERM,ProcessExit");
}
else
{
Log.Information("@AppDomain.CurrentDomain.ProcessExit,已處理 SIGINT,忽略 SIGTERM");
}
};
await Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
// services.Configure<HostOptions>(opts => opts.ShutdownTimeout = TimeSpan.FromSeconds(15));
// services.AddHostedService<GracefulShutdownService>();
services.AddHostedService<GracefulShutdownService1>();
// services.AddHostedService<GracefulShutdownService_Fail>();
})
.UseSerilog()
.RunConsoleAsync();
Log.Information("下次再來唷~");
Exec mode
在 Dokerfile 使用 ENTRYPOINT 啟動 .NET Core 應用程式
FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["Lab.GracefulShutdown.Net8/Lab.GracefulShutdown.Net8.csproj", "Lab.GracefulShutdown.Net8/"]
RUN dotnet restore "Lab.GracefulShutdown.Net8/Lab.GracefulShutdown.Net8.csproj"
COPY . .
WORKDIR "/src/Lab.GracefulShutdown.Net8"
RUN dotnet build "Lab.GracefulShutdown.Net8.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "Lab.GracefulShutdown.Net8.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Lab.GracefulShutdown.Net8.dll"]
沿用上篇,用 WSL 開啟專案

在 Rider 的 Terminal 介面,或是 Windows Terminal 介面,把 Container 叫起來
docker build -t graceful-shutdown-net8:dev -f ./Lab.GracefulShutdown.Net8/Dockerfile .
docker run --name graceful-shutdown-net8 --rm graceful-shutdown-net8:dev
執行結果如下:

在另外一個 Terminal 送出關閉訊號
time docker container stop graceful-shutdown-net8

在 Terminal 可以觀察到,應用程式有接收到 SIGINT/SIGTERM 並且順利的完成流程,StartAsync → ExecuteAsync → StopAsync

如果 10 sec 內無法完成處理,可以再把時間延長
time docker container stop -t 30 graceful-shutdown-net8
Shell Mode
在 Dockerfile,把
ENTRYPOINT ["dotnet", "Lab.GracefulShutdown.Net8.dll"]
換成
CMD dotnet /app/Lab.GracefulShutdown.Net8.dll #shell mode
再按照上述步驟
docker build -t graceful-shutdown-net8:dev -f ./Lab.GracefulShutdown.Net8/Dockerfile .
docker run --name graceful-shutdown-net8 --rm graceful-shutdown-net8:dev
最後執行
time docker container stop graceful-shutdown-net8
從下圖觀察到,這次,等了 10 sec 才強制關閉容器,跟上一個實驗,不一樣。

從下圖觀察到,應用程式的 log 沒有收到 SIGINT/SIGTERM 訊號,硬生生地被強迫關閉

在 shell mode 下,送出 SIGTERM/SIGINT 也完全不會有作用
time docker container kill --signal=SIGTERM graceful-shutdown-net8
time docker container kill --signal=SIGINT graceful-shutdown-net8
但,對 SIGKILL 有作用
time docker container kill --signal=SIGKILL graceful-shutdown-net8
觀察 Container Process
進入到容器
docker container exec -it graceful-shutdown-net8 bash
安裝
apt-get update && apt-get install -y procps
查看 Process 狀況
ps -aux
exec mode
PID 1 為 dotnet,kill 1 可以順利的終止應用程式/關閉整個容器

shell mode
PID 1 為 /bin/sh -c

kill 1,沒有反應,kill 7 才能終止應用程式/關閉整個容器

Alpine vs Ubuntu Image
本以為使用 shell mode 一定會有問題,沒想到換一個 image 就沒有問題了,改用 Alpine Image
mcr.microsoft.com/dotnet/runtime:8.0-alpine
PID 1 就變成 dotnet 而不是 /bin/sh -c,如下圖

對 container 送出 stop 命令也有順利處理 SIGINT/SIGTERM

順利地走完流程了

cat /etc/os-release 觀察作業系統版本

心得
Ubuntu Image
- shell mode:用 bin/sh 執行 cmd 時,PID 1 的 process 會是 bin/sh
- exec mode:直接執行 cmd,PID 1 的 process 會是 cmd
差異在於,當 container 送出 SIGTERM 訊號,例如 docker container stop, bin/sh 不會把 SIGTERM 訊號傳給 cmd(子 Process),導致 process 沒有處理關閉流程;反之 exec mode 會收到 SIGTERM 訊號
使用以下語法觀察 bin/sh 連結到的是 dash
docker container exec -it graceful-shutdown-net8 ls -l /bin/sh

alpine Image
不管是用哪一種 mode,PID 1 的 process 都是 cmd
使用以下語法觀察 bin/sh 連結到的是 busybox
docker container exec -it graceful-shutdown-net8 ls -l /bin/sh

不同的 image 的可能連結的應用程式有些不一樣,必須要確定 PID 1 的 process 是 cmd (你開發的應用程式),而不是 bin/sh
範例位置
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET