.NET 일반 호스트

이 문서에서는 Microsoft.Extensions.Hosting NuGet 패키지에서 사용할 수 있는 .NET 제네릭 호스트를 구성하고 빌드하기 위한 다양한 패턴에 대해 알아봅니다. .NET 제네릭 호스트는 앱 시작 및 수명 관리를 담당합니다. 작업자 서비스 템플릿은 .NET 제네릭 호스트 HostApplicationBuilder를 만듭니다. 제네릭 호스트는 콘솔 앱과 같은 다른 유형의 .NET 애플리케이션과 함께 사용할 수 있습니다.

호스트는 다음과 같은 앱의 리소스와 수명 기능을 캡슐화하는 개체입니다.

  • DI(종속성 주입)
  • 로깅
  • 구성
  • 앱 종료
  • IHostedService 구현

호스트가 시작될 때 서비스 컨테이너의 호스티드 서비스 컬렉션에 등록된 IHostedService의 각 구현에서 IHostedService.StartAsync을 호출합니다. 작업자 서비스 앱에서 BackgroundService 인스턴스를 포함하는 모든 IHostedService 구현은 BackgroundService.ExecuteAsync 메서드를 호출합니다.

하나의 개체에 앱의 모든 상호 종속적 리소스를 포함하는 주요 원인은 수명 관리 즉, 앱 시작 및 종료에 대한 제어 때문입니다.

호스트 설정

호스트는 일반적으로 Program 클래스의 코드로 구성, 빌드 및 실행됩니다. Main 메서드는 다음 작업을 수행합니다.

  • CreateApplicationBuilder 메서드를 호출하여 작성기 개체를 만들고 구성합니다.
  • Build()를 호출하여 IHost 인스턴스를 만듭니다.
  • 호스트 개체에 대해 Run 또는 RunAsync 메서드를 호출합니다.

.NET 작업자 서비스 템플릿은 다음과 같은 코드를 생성하여 제네릭 호스트를 만듭니다.

using Example.WorkerService;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();

IHost host = builder.Build();
host.Run();

작업자 서비스에 대한 자세한 내용은 .NET의 작업자 서비스를 참조하세요.

호스트 작성기 설정

CreateApplicationBuilder 메서드는 다음 작업을 수행합니다.

  • 콘텐츠 루트를 GetCurrentDirectory()에서 반환된 경로로 설정합니다.
  • 다음에서 호스트 구성을 로드합니다.
    • 접두사가 DOTNET_인 환경 변수.
    • 명령줄 인수.
  • 다음에서 앱 구성을 로드합니다.
    • appsettings.json.
    • appsettings.{Environment}.json.
    • 비밀 관리자: 앱이 Development 환경에서 실행되는 경우
    • 환경 변수입니다.
    • 명령줄 인수.
  • 다음 로깅 공급자를 추가합니다.
    • 콘솔
    • 디버그
    • EventSource
    • EventLog(Windows에서 실행 중인 경우에만)
  • 환경이 Development일 때 범위 유효성 검사 및 종속성 유효성 검사를 사용하도록 설정합니다.

HostApplicationBuilder.ServicesMicrosoft.Extensions.DependencyInjection.IServiceCollection 인스턴스입니다. 이러한 서비스는 등록된 서비스를 확인하기 위해 종속성 주입과 함께 사용되는 IServiceProvider를 빌드하는 데 사용됩니다.

프레임워크에서 제공하는 서비스

IHostBuilder.Build() 또는 HostApplicationBuilder.Build()를 호출하면 다음 서비스가 자동으로 등록됩니다.

IHostApplicationLifetime

IHostApplicationLifetime 서비스를 모든 클래스에 주입하여 시작 후 및 정상 종료 작업을 처리합니다. 인터페이스의 세 가지 속성은 앱 시작 및 앱 중지 이벤트 처리기 메서드를 등록하는 데 사용되는 취소 토큰입니다. 인터페이스에는 StopApplication() 메서드도 포함됩니다.

다음 예제는 IHostedService 이벤트를 등록하는 구현입니다IHostedLifecycleService.IHostApplicationLifetime

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace AppLifetime.Example;

public sealed class ExampleHostedService : IHostedService, IHostedLifecycleService
{
    private readonly ILogger _logger;

    public ExampleHostedService(
        ILogger<ExampleHostedService> logger,
        IHostApplicationLifetime appLifetime)
    {
        _logger = logger;

        appLifetime.ApplicationStarted.Register(OnStarted);
        appLifetime.ApplicationStopping.Register(OnStopping);
        appLifetime.ApplicationStopped.Register(OnStopped);
    }

    Task IHostedLifecycleService.StartingAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("1. StartingAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedService.StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("2. StartAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedLifecycleService.StartedAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("3. StartedAsync has been called.");

        return Task.CompletedTask;
    }

    private void OnStarted()
    {
        _logger.LogInformation("4. OnStarted has been called.");
    }

    private void OnStopping()
    {
        _logger.LogInformation("5. OnStopping has been called.");
    }

    Task IHostedLifecycleService.StoppingAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("6. StoppingAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedService.StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("7. StopAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedLifecycleService.StoppedAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("8. StoppedAsync has been called.");

        return Task.CompletedTask;
    }

    private void OnStopped()
    {
        _logger.LogInformation("9. OnStopped has been called.");
    }
}

ExampleHostedService 구현을 추가하도록 작업자 서비스 템플릿을 수정할 수 있습니다.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using AppLifetime.Example;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHostedService<ExampleHostedService>();
using IHost host = builder.Build();

await host.RunAsync();

애플리케이션은 다음 샘플 출력을 작성합니다.

// Sample output:
//     info: AppLifetime.Example.ExampleHostedService[0]
//           1.StartingAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           2.StartAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           3.StartedAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           4.OnStarted has been called.
//     info: Microsoft.Hosting.Lifetime[0]
//           Application started. Press Ctrl+C to shut down.
//     info: Microsoft.Hosting.Lifetime[0]
//           Hosting environment: Production
//     info: Microsoft.Hosting.Lifetime[0]
//           Content root path: ..\app-lifetime\bin\Debug\net8.0
//     info: AppLifetime.Example.ExampleHostedService[0]
//           5.OnStopping has been called.
//     info: Microsoft.Hosting.Lifetime[0]
//           Application is shutting down...
//     info: AppLifetime.Example.ExampleHostedService[0]
//           6.StoppingAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           7.StopAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           8.StoppedAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           9.OnStopped has been called.

출력은 모든 다양한 수명 주기 이벤트의 순서를 보여 줍니다.

  1. IHostedLifecycleService.StartingAsync
  2. IHostedService.StartAsync
  3. IHostedLifecycleService.StartedAsync
  4. IHostApplicationLifetime.ApplicationStarted

예를 들어 Ctrl+C사용하여 애플리케이션이 중지되면 다음 이벤트가 발생합니다.

  1. IHostApplicationLifetime.ApplicationStopping
  2. IHostedLifecycleService.StoppingAsync
  3. IHostedService.StopAsync
  4. IHostedLifecycleService.StoppedAsync
  5. IHostApplicationLifetime.ApplicationStopped

IHostLifetime

IHostLifetime 구현은 호스트가 시작될 때와 중지될 때를 제어합니다. 등록된 마지막 구현이 사용됩니다. Microsoft.Extensions.Hosting.Internal.ConsoleLifetime은 기본 IHostLifetime 구현입니다. 종료의 수명 메커니즘에 대한 자세한 내용은 호스트 종료를 참조하세요.

인터페이스는 IHostLifetime 메서드를 IHostLifetime.WaitForStartAsync 노출합니다. 이 메서드는 시작 IHost.StartAsync 시 호출되며 계속하기 전에 완료될 때까지 기다립니다. 이는 외부 이벤트에서 신호를 보낼 때까지 시작을 지연시키는 데 사용할 수 있습니다.

또한 인터페이스는 IHostLifetime 호스트가 중지되고 종료할 시간임을 나타내기 위해 호출 IHost.StopAsync 되는 메서드를 노출 IHostLifetime.StopAsync 합니다.

IHostEnvironment

IHostEnvironment 서비스를 클래스에 삽입하여 다음 설정에 대한 정보를 가져옵니다.

또한 IHostEnvironment 서비스는 다음 확장 메서드를 사용하여 환경을 평가하는 기능을 제공합니다.

호스트 구성

호스트 구성은 IHostEnvironment 구현의 속성을 구성하는 데 사용됩니다.

호스트 구성은 IHostApplicationBuilder.Configuration 속성에서 사용할 수 있으며 환경 구현은 IHostApplicationBuilder.Environment 속성에서 사용할 수 있습니다. 호스트를 구성하려면 Configuration 속성에 액세스하고 사용 가능한 확장 메서드를 호출합니다.

호스트 구성을 추가하려면 다음 예를 고려합니다.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Environment.ContentRootPath = Directory.GetCurrentDirectory();
builder.Configuration.AddJsonFile("hostsettings.json", optional: true);
builder.Configuration.AddEnvironmentVariables(prefix: "PREFIX_");
builder.Configuration.AddCommandLine(args);

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

앞의 코드가 하는 역할은 다음과 같습니다.

  • 콘텐츠 루트를 GetCurrentDirectory()에서 반환된 경로로 설정합니다.
  • 다음에서 호스트 구성을 로드합니다.
    • hostsettings.json.
    • 접두사가 PREFIX_인 환경 변수.
    • 명령줄 인수.

앱 구성

앱 구성은 IHostApplicationBuilder에서 ConfigureAppConfiguration을 호출하여 생성됩니다. publicIHostApplicationBuilder.Configuration 속성을 사용하면 소비자가 사용 가능한 확장 메서드를 사용하여 기존 구성을 읽거나 변경할 수 있습니다.

자세한 내용은 .NET의 구성을 참조하세요.

호스트 종료

호스트된 프로세스를 중지하는 방법에는 여러 가지가 있습니다. 가장 일반적으로 호스트된 프로세스는 다음과 같은 방법으로 중지할 수 있습니다.

호스팅 코드는 이러한 시나리오를 처리할 책임이 없습니다. 프로세스 소유자는 다른 앱과 동일하게 이를 처리해야 합니다. 호스티드 서비스 프로세스를 중지할 수 있는 다른 방법은 여러 가지가 있습니다.

  • ConsoleLifetime을 사용하는 경우(UseConsoleLifetime), 다음 신호를 수신하고 호스트를 정상적으로 중지하려고 시도합니다.
    • SIGINT(또는 CTRL+C)
    • SIGQUIT(또는 Windows의 경우 CTRL+BREAK, Unix의 경우 CTRL+\).
    • SIGTERM(docker stop 같은 다른 앱에서 보냄)
  • 앱이 Environment.Exit을 호출할 경우

기본 제공된 호스팅 논리는 이러한 시나리오, 특히 ConsoleLifetime 클래스를 처리합니다. ConsoleLifetime은 애플리케이션의 정상 종료를 위해 ‘종료’ 신호인 SIGINT, SIGQUIT 및 SIGTERM을 처리하려고 합니다.

.NET 6 이전에는 .NET 코드에서 SIGTERM을 정상적으로 처리할 수 없었습니다. 이 제한을 해결하기 위해 ConsoleLifetimeSystem.AppDomain.ProcessExit을 구독합니다. ProcessExit가 발생하면 ConsoleLifetimeProcessExit 스레드를 중지하고 차단하도록 호스트에 신호를 보내 호스트가 중지되기를 기다립니다.

프로세스 종료 처리기를 사용하면 애플리케이션의 정리 코드(예: Main 메서드에서 IHost.StopAsyncHostingAbstractionsHostExtensions.Run 이후의 코드)를 실행할 수 있습니다.

그러나 SIGTERM이 ProcessExit가 발생하는 유일한 방법이 아니었기 때문에 이 방식에는 다른 문제가 있었습니다. SIGTERM은 앱 코드가 Environment.Exit를 호출할 때도 발생합니다. Environment.ExitMicrosoft.Extensions.Hosting 앱 모델에서 프로세스를 종료하는 정상적인 방법이 아닙니다. ProcessExit 이벤트를 발생시키고 프로세스를 종료합니다. Main 메서드의 끝이 실행되지 않습니다. 백그라운드 및 포그라운드 스레드가 종료되고 finally 블록이 실행되지 않습니다.

호스트가 종료되기를 기다리는 동안 ConsoleLifetimeProcessExit를 차단했기 때문에 이 동작으로 인해 Environment.Exit에서 교착 상태가 발생했으며 ProcessExit에 대한 호출을 기다리는 것도 차단되었습니다. 또한 SIGTERM 처리가 프로세스의 정상 종료를 시도했으므로 ConsoleLifetimeExitCode0으로 설정했으며 이는 Environment.Exit에 전달된 사용자의 종료 코드를 변경했습니다.

.NET 6에서는 POSIX 신호가 지원 및 처리됩니다. ConsoleLifetime은 SIGTERM을 적절하게 처리하고 Environment.Exit가 호출될 때 더 이상 관련되지 않습니다.

.NET 6 이상에서는 ConsoleLifetime이 더 이상 Environment.Exit 시나리오를 처리하는 논리를 포함하지 않습니다. Environment.Exit을 호출하고 정리 논리를 수행해야 하는 앱은 ProcessExit을 구독할 수 있습니다. 이러한 시나리오에서는 호스팅이 더 이상 호스트를 정상적으로 중지하려고 시도하지 않습니다.

애플리케이션이 호스팅을 사용하는 경우 호스트를 정상적으로 중지하려면 Environment.Exit 대신 IHostApplicationLifetime.StopApplication을 호출할 수 있습니다.

호스팅 종료 프로세스

다음 시퀀스 다이어그램은 호스팅 코드에서 신호를 내부적으로 처리하는 방법을 보여 줍니다. 대부분의 사용자는 이 프로세스를 이해할 필요가 없습니다. 그러나 깊은 이해가 필요한 개발자의 경우 좋은 시각적 자료가 시작하는 데 도움이 될 수 있습니다.

호스트가 시작된 후 사용자가 Run 또는 WaitForShutdown을 호출하면 처리기가 IApplicationLifetime.ApplicationStopping에 등록됩니다. 실행이 WaitForShutdown에서 일시 중지되어 ApplicationStopping 이벤트가 발생할 때까지 기다립니다. Main 메서드는 즉시 반환되지 않으며 앱은 Run 또는 WaitForShutdown이 반환될 때까지 계속 실행됩니다.

프로세스에 신호를 보내면 다음 시퀀스가 시작됩니다.

호스팅 종료 시퀀스 다이어그램

  1. 제어가 ConsoleLifetime에서 ApplicationLifetime으로 흘러서 ApplicationStopping 이벤트를 발생시킵니다. 이는 WaitForShutdownAsync 신호를 보내서 Main 실행 코드의 차단을 해제합니다. 그동안 이 POSIX 신호가 처리되었으므로 POSIX 신호 처리기는 Cancel = true를 반환합니다.
  2. Main 실행 코드가 다시 실행되기 시작하고 호스트에 StopAsync()를 알리면 호스트되는 모든 서비스가 중지되고 다른 중지된 이벤트를 발생시킵니다.
  3. 마지막으로 WaitForShutdown이 존재하므로 모든 애플리케이션이 코드 정리를 실행하고 Main 메서드를 정상적으로 종료합니다.

웹 서버 시나리오의 호스트 종료

HTTP/1.1 및 HTTP/2 프로토콜 모두에 대해 Kestrel에서 정상 종료가 작동하는 다양한 일반적인 시나리오와 트래픽을 원활하게 드레이닝하기 위해 부하 분산 장치를 사용하여 다양한 환경에서 이를 구성하는 방법이 있습니다. 웹 서버 구성은 이 문서의 범위를 벗어나지만 ASP.NET Core Kestrel 웹 서버에 대한 옵션 구성 설명서에서 자세한 내용을 확인할 수 있습니다.

호스트가 종료 신호(예: CTL+C 또는 StopAsync)를 수신하면 ApplicationStopping 신호를 통해 애플리케이션에 알립니다. 정상적으로 완료해야 하는 장기 실행 작업이 있는 경우 이 이벤트를 구독해야 합니다.

다음으로 호스트는 구성할 수 있는 종료 시간 제한(기본값 30초)을 사용하여 IServer.StopAsync를 호출합니다. Kestrel(및 Http.Sys)은 포트 바인딩을 닫고 새 연결 수락을 중지합니다. 또한 현재 연결에 새 요청 처리를 중지하라고 지시합니다. HTTP/2 및 HTTP/3의 경우 예비 GOAWAY 메시지가 클라이언트로 전송됩니다. HTTP/1.1의 경우 요청이 순서대로 처리되므로 연결 루프를 중지합니다. IIS는 503 상태 코드가 있는 새 요청을 거부하여 다르게 동작합니다.

활성 요청은 종료 시간 제한까지 완료되어야 합니다. 시간 제한 전에 모두 완료되면 서버는 더 빨리 제어권을 호스트에 반환합니다. 제한 시간이 만료되면 보류 중인 연결과 요청이 강제로 중단되어 로그와 클라이언트에 오류가 발생할 수 있습니다.

부하 분산 장치 고려 사항

부하 분산 장치 작업 시 클라이언트를 새 대상으로 원활하게 전환하려면 다음 단계를 따릅니다.

  • 새 인스턴스를 가져와 트래픽 밸런싱을 시작합니다(크기 조정 목적으로 이미 여러 인스턴스가 있을 수 있음).
  • 부하 분산 장치 구성에서 이전 인스턴스를 사용하지 않도록 설정하거나 제거하여 새 트래픽 수신을 중지합니다.
  • 이전 인스턴스에 종료 신호를 보냅니다.
  • 드레이닝되거나 시간이 초과될 때까지 기다립니다.

참고 항목