观察 .NET Core Container 的 Exec Mode 和 Shell Mode 差异

由 上篇 得知 .NET Core 应用程式可以接收 SIGINT/SIGTERM讯号,在处理批次流程,当应用程式接收到 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 Debian

本以为使用 shell mode 一定会有问题,没想到换一个 image 就没有问题了

改用 Alpine Image

 

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 讯号

 

alpine Image

不管是用哪一种 mode,PID 1 的 process 都是 cmd

範例位置

sample.dotblog/Graceful Shutdown/Lab.GracefulShutdownWithSellMode at master · yaochangyu/sample.dotblog

若有谬误,烦请告知,新手发帖请多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章

5 点赞(415) 阅读(67)