다음을 통해 공유


서버 쪽 Blazor 앱 호스트 및 배포

참고 항목

이 문서의 최신 버전은 아닙니다. 현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.

Warning

이 버전의 ASP.NET Core는 더 이상 지원되지 않습니다. 자세한 내용은 .NET 및 .NET Core 지원 정책을 참조 하세요. 현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.

Important

이 정보는 상업적으로 출시되기 전에 실질적으로 수정될 수 있는 시험판 제품과 관련이 있습니다. Microsoft는 여기에 제공된 정보에 대해 어떠한 명시적, 또는 묵시적인 보증을 하지 않습니다.

현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.

이 문서에서는 ASP.NET Core를 사용하여 서버 쪽 Blazor 앱(Blazor Web App및 앱)을 호스트하고 Blazor Server 배포하는 방법을 설명합니다.

호스트 구성 값

서버 쪽 Blazor 앱은 제네릭 호스트 구성 값을 허용할 수 있습니다.

배포

서버 쪽 호스팅 모델을 Blazor 사용하면 ASP.NET Core 앱 내에서 서버에서 실행됩니다. UI 업데이트, 이벤트 처리 및 JavaScript 호출은 SignalR 연결을 통해 처리됩니다.

ASP.NET Core 앱을 호스팅할 수 있는 웹 서버가 필요합니다. Visual Studio에는 서버 쪽 앱 프로젝트 템플릿이 포함되어 있습니다. Blazor 프로젝트 템플릿에 대한 자세한 내용은 ASP.NET Core Blazor 프로젝트 구조를 참조하세요.

릴리스 구성에서 앱을 게시하고 폴더의 bin/Release/{TARGET FRAMEWORK}/publish 콘텐츠를 배포합니다. 여기서 {TARGET FRAMEWORK} 자리 표시자는 대상 프레임워크입니다.

확장성

단일 서버의 확장성(스케일 업)을 고려할 때, 앱에 사용할 수 있는 메모리는 사용자 요구가 늘어남에 따라 앱이 소진하는 첫 번째 리소스일 가능성이 높습니다. 서버에서 사용 가능한 메모리는 다음 사항에 영향을 줍니다.

  • 서버가 지원할 수 있는 활성 회로 수
  • 클라이언트의 UI 대기 시간

안전하고 확장 가능한 서버 쪽 Blazor 앱을 빌드하는 방법에 대한 지침은 다음 리소스를 참조하세요.

각 회로는 최소 Hello World와 같은 앱에 약 250KB의 메모리를 사용합니다. 회로의 크기는 앱의 코드 및 각 구성 요소와 연결된 상태 유지 관리 요구 사항에 따라 달라집니다. 앱 및 인프라를 개발하는 동안 리소스 수요를 측정하는 것이 좋지만 다음 기준은 배포 대상을 계획하기 위한 시작점이 될 수 있습니다. 앱에서 동시 사용자 5,000명 이상을 지원할 것으로 예상되는 경우 앱에 최소 1.3GB의 서버 메모리(또는 사용자당 ~273KB)의 예산을 책정하는 것이 좋습니다.

SignalR 구성

SignalR의 호스팅 및 스케일링 조건이 SignalR를 사용하는 Blazor 앱에 적용됩니다.

구성 지침을 포함하여 앱에 대한 자세한 내용은 ASP.NET Core BlazorSignalR 지침을 참조하세요.SignalR Blazor

전송

Blazor는 짧은 대기 시간, 보다 나은 안정성, 향상된 보안 덕분에 WebSocket을 SignalR 전송으로 사용하는 경우에 가장 효과적입니다. WebSocket을 사용할 수 없거나 앱이 긴 폴링을 사용하도록 명시적으로 구성된 경우 SignalR에서 긴 폴링을 사용합니다.

긴 폴링이 다음을 사용하는 경우, 콘솔 경고가 나타납니다.

긴 폴링 대체 전송을 사용하여 WebSocket을 통해 연결하지 못했습니다. 연결을 차단하는 VPN 또는 프록시 때문일 수 있습니다.

전역 배포 및 연결 실패

지리적 데이터 센터에 대한 전역 배포에 대한 권장 사항:

  • 대부분의 사용자가 있는 지역에 앱을 배포합니다.
  • 대륙 간 트래픽의 대기 시간이 증가하는 것을 고려합니다. 다시 연결 UI의 모양을 제어하려면 ASP.NET Core BlazorSignalR 지침을 참조하세요.
  • Azure SignalR 서비스를 사용하는 것이 좋습니다.

Azure App Service

Azure 앱 Service에서 호스팅하려면 WebSocket 및 세션 선호도(ARR(애플리케이션 요청 라우팅) 선호도라고도 함)에 대한 구성이 필요합니다.

참고 항목

Blazor Azure 앱 Service의 앱에는 Azure SignalR 서비스가 필요하지 않습니다.

Azure 앱 Service에서 앱 등록에 대해 다음을 사용하도록 설정합니다.

  • WebSockets 전송이 작동할 수 있도록 하는 WebSockets입니다. 기본 설정은 끄기입니다.
  • 사용자의 요청을 동일한 App Service 인스턴스로 다시 라우팅하는 세션 선호도입니다. 기본 설정은 켜기입니다.
  1. Azure Portal에서 App Services의 웹앱으로 이동합니다.
  2. 설정>구성을 엽니다.
  3. 웹 소켓켜기로 설정합니다.
  4. 세션 선호도가 On으로 설정되어 있는지 확인합니다.

Azure SignalR 서비스

선택적 Azure SignalR 서비스는 서버 쪽 앱을 많은 수의 동시 연결로 확장하기 위해 앱의 SignalR 허브와 함께 작동합니다. 또한 서비스의 글로벌 및 고성능 데이터 센터는 지리적 위치로 인한 대기 시간을 줄이는 데 큰 도움이 됩니다.

서비스는 Azure 앱 Service 또는 Azure Container Apps에서 호스트되는 앱에 필요하지 Blazor 않지만 다른 호스팅 환경에서 유용할 수 있습니다.

  • 연결을 쉽게 확장할 수 있도록 합니다.
  • 전역 배포를 처리합니다.

참고 항목

상태 저장 다시 연결 (WithStatefulReconnect)은 .NET 8에서 릴리스되었지만 현재 Azure SignalR 서비스에서 지원되지 않습니다. 자세한 내용은 상태 저장 다시 연결 지원을 참조 하세요. (Azure/azure-signalr #1878).

앱이 긴 폴링을 사용하거나 WebSocket 대신 긴 폴링으로 대체되는 경우 Azure SignalR 서비스에서 긴 폴링 연결에 허용되는 최대 폴링 간격을 정의하는 최대 폴링 간격(MaxPollIntervalInSeconds기본값: 5초, 제한: 1-300초)을 구성해야 할 수 있습니다. 다음 폴링 요청이 최대 폴링 간격 내에 도착하지 않으면 서비스는 클라이언트 연결을 닫습니다.

프로덕션 배포에 종속성으로 서비스를 추가하는 방법에 대한 지침은 Azure 앱 서비스에 ASP.NET Core SignalR 앱 게시를 참조하세요.

자세한 내용은 다음을 참조하세요.

Azure Container Apps

Azure Container Apps 서비스에서 서버 쪽 Blazor 앱의 크기를 조정하는 방법을 자세히 알아보려면 Azure에서 ASP.NET Core Apps 크기 조정을 참조하세요. 이 자습서에서는 Azure Container Apps에서 앱을 호스트하는 데 필요한 서비스를 만들고 통합하는 방법을 설명합니다. 이 섹션에서는 기본 단계도 제공합니다.

  1. Azure Container Apps의 세션 선호도(Azure 설명서)지침에 따라 세션 선호도에 대한 Azure Container Apps 서비스를 구성합니다.

  2. ASP.NET Core Data Protection(DP) 서비스는 모든 컨테이너 인스턴스가 액세스할 수 있는 중앙 집중식 위치에 키를 유지하도록 구성해야 합니다. 키는 Azure Blob Storage에 저장하고 Azure Key Vault를 사용하여 보호할 수 있습니다. DP 서비스는 키를 사용하여 구성 요소를 역직렬화 Razor 합니다. Azure Blob Storage 및 Azure Key Vault를 사용하도록 DP 서비스를 구성하려면 다음 NuGet 패키지를 참조하세요.

    참고 항목

    .NET 앱에 패키지를 추가하는 방법에 대한 지침은 패키지 사용 워크플로에서 패키지 설치 및 관리의 문서(NuGet 설명서)를 참조하세요. NuGet.org에서 올바른 패키지 버전을 확인합니다.

  3. 다음 강조 표시된 코드로 업데이트 Program.cs 합니다.

    using Azure.Identity;
    using Microsoft.AspNetCore.DataProtection;
    using Microsoft.Extensions.Azure;
    
    var builder = WebApplication.CreateBuilder(args);
    var BlobStorageUri = builder.Configuration["AzureURIs:BlobStorage"];
    var KeyVaultURI = builder.Configuration["AzureURIs:KeyVault"];
    
    builder.Services.AddRazorPages();
    builder.Services.AddHttpClient();
    builder.Services.AddServerSideBlazor();
    
    builder.Services.AddAzureClientsCore();
    
    builder.Services.AddDataProtection()
                    .PersistKeysToAzureBlobStorage(new Uri(BlobStorageUri),
                                                    new DefaultAzureCredential())
                    .ProtectKeysWithAzureKeyVault(new Uri(KeyVaultURI),
                                                    new DefaultAzureCredential());
    var app = builder.Build();
    
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }
    
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    
    app.UseRouting();
    
    app.UseAuthorization();
    
    app.MapRazorPages();
    
    app.Run();
    

    위의 변경 내용을 통해 앱은 중앙 집중식 확장성 아키텍처를 사용하여 DP 서비스를 관리할 수 있습니다. DefaultAzureCredential 는 코드가 Azure에 배포된 후 관리 identity 되는 컨테이너 앱을 검색하고 이를 사용하여 Blob Storage 및 앱의 키 자격 증명 모음에 연결합니다.

  4. 관리 identity 되는 컨테이너 앱을 만들고 Blob Storage 및 키 자격 증명 모음에 대한 액세스 권한을 부여하려면 다음 단계를 완료합니다.

    1. Azure Portal에서 컨테이너 앱의 개요 페이지로 이동합니다.
    2. 왼쪽 탐색 영역에서 서비스 커넥터를 선택합니다.
    3. 위쪽 탐색 영역에서 + 만들기를 선택합니다.
    4. 연결 플라이아웃 만들기 메뉴에서 다음 값을 입력합니다.
      • 컨테이너: 앱을 호스트하기 위해 만든 컨테이너 앱을 선택합니다.
      • 서비스 유형: Blob Storage를 선택합니다.
      • 구독: 컨테이너 앱을 소유하는 구독을 선택합니다.
      • 연결 이름: scalablerazorstorage의 이름을 입력합니다.
      • 클라이언트 유형: .NET을 선택하고 다음을 선택합니다.
    5. 시스템 할당 관리identity형을 선택하고 다음을 선택합니다.
    6. 기본 네트워크 설정을 사용하고 다음을 선택합니다.
    7. Azure에서 설정의 유효성을 검사한 후 만들기를 선택합니다.

    키 자격 증명 모음에 대한 이전 설정을 반복합니다. 기본 탭에서 적절한 키 자격 증명 모음 서비스 및 키를 선택합니다.

IIS

IIS를 사용하는 경우 다음을 사용하도록 설정합니다.

자세한 내용은 IIS에 ASP.NET Core 앱 게시의 지침 및 외부 IIS 리소스 교차 링크를 참조하세요.

Kubernetes

세션 선호도에 대한 다음 Kubernetes 주석을 사용하여 수신 정의를 만듭니다.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: <ingress-name>
  annotations:
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/session-cookie-name: "affinity"
    nginx.ingress.kubernetes.io/session-cookie-expires: "14400"
    nginx.ingress.kubernetes.io/session-cookie-max-age: "14400"

Nginx를 사용하는 Linux

다음 변경 내용에 대해 ASP.NET Core SignalR 앱의 지침을 따릅니다.

  • location 경로를 /hubroute(location /hubroute { ... })에서 루트 경로 /(location / { ... })로 변경합니다.
  • 이 설정은 Blazor 앱 클라이언트-서버 상호 작용과 관련이 없는 SSE(Server-Sent 이벤트)에 적용되므로 프록시 버퍼링(proxy_buffering off;)에 대한 구성을 제거합니다.

자세한 내용 및 구성 지침은 다음 리소스를 참조하세요.

Apache를 사용하는 Linux

Linux에서 Apache 뒤에 Blazor 앱을 호스트하려면 HTTP 및 WebSockets 트래픽에 대해 ProxyPass를 구성합니다.

다음 예제에서

  • Kestrel 서버가 호스트 머신에서 실행되고 있습니다.
  • 앱은 포트 5000에서 트래픽을 수신 대기합니다.
ProxyPreserveHost   On
ProxyPassMatch      ^/_blazor/(.*) http://localhost:5000/_blazor/$1
ProxyPass           /_blazor ws://localhost:5000/_blazor
ProxyPass           / http://localhost:5000/
ProxyPassReverse    / http://localhost:5000/

다음 모듈을 사용하도록 설정합니다.

a2enmod   proxy
a2enmod   proxy_wstunnel

브라우저 콘솔에서 WebSockets 오류를 확인합니다. 오류의 예:

  • Firefox에서 서버(ws://the-domain-name.tld/_blazor?id=XXX)에 대한 연결을 설정할 수 없습니다.
  • 오류: 'WebSockets' 전송을 시작하지 못했습니다. 오류: 전송에 오류가 발생했습니다.
  • 오류: 전송 'LongPolling'을 시작하지 못했습니다. TypeError: this.transport이 정의되지 않았습니다.
  • 오류: 사용 가능한 전송을 사용하여 서버에 연결할 수 없습니다. WebSockets 실패
  • 오류: 연결이 '연결됨' 상태가 아니면 데이터를 보낼 수 없습니다.

자세한 내용 및 구성 지침은 다음 리소스를 참조하세요.

네트워크 대기 시간 측정

JS interop은 다음 예제와 같이 네트워크 대기 시간을 측정하는 데 사용할 수 있습니다.

MeasureLatency.razor:

@inject IJSRuntime JS

<h2>Measure Latency</h2>

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}
@inject IJSRuntime JS

<h2>Measure Latency</h2>

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}
@inject IJSRuntime JS

<h2>Measure Latency</h2>

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}
@inject IJSRuntime JS

<h2>Measure Latency</h2>

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}
@inject IJSRuntime JS

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}

적절한 UI 환경을 위해 UI 대기 시간을 250ms 이하로 유지하는 것이 좋습니다.

메모리 관리

서버에서 각 사용자 세션에 대해 새 회로가 만들어집니다. 각 사용자 세션은 브라우저에서의 단일 문서 렌더링에 대응합니다. 예를 들어, 여러 탭은 여러 세션을 만듭니다.

Blazor는 세션을 시작한 회로라고 하는 브라우저에 대한 상수 연결을 유지합니다. 사용자가 네트워크 연결을 끊거나 브라우저를 갑자기 닫는 경우와 같은 여러 가지 이유로 언제든지 연결이 끊어질 수 있습니다. 연결이 끊어지면, Blazor에는 클라이언트가 세션을 다시 연결하고 다시 설정하는 데 제한된 시간(기본값: 3분)을 제공하는 제한된 수의 회로를 "연결 끊김" 풀에 배치하는 복구 메커니즘이 있습니다.

그런 다음, Blazor은 회로를 해제하고 세션을 삭제합니다. 이 시점부터, 회로는 GC(가비지 수집)에 적합하며 회로의 GC 생성에 대한 컬렉션이 트리거될 때 클레임됩니다. 이해해야 할 한 가지 중요한 측면은 회로의 수명이 길다는 것입니다. 즉, 회로에 의해 루팅된 대부분의 개체가 결국 Gen 2에 도달합니다. 따라서 Gen 2 컬렉션이 발생할 때까지 해제된 개체가 표시되지 않을 수 있습니다.

일반 메모리 사용량 측정

필수 조건:

  • 앱은 릴리스 구성에 게시되어야 합니다. 생성된 코드는 프로덕션 배포에 사용되는 코드를 대표하지 않으므로 디버그 구성 측정은 관련이 없습니다.
  • 앱의 동작에 영향을 미치고 결과를 망칠 수도 있으므로 앱은 디버거를 연결하지 않고 실행해야 합니다. Visual Studio에서 메뉴 모음에서 디버깅하지 않고 디버그> 시작을 선택하거나 키보드를 사용하여 CtrlF5+ 키를 눌러디버깅하지 않고 앱을 시작합니다.
  • .NET에서 실제로 얼마나 많은 메모리를 사용하는지 이해하려면 다양한 유형의 메모리를 고려합니다. 일반적으로, 개발자는 Windows OS의 작업 관리자에서 앱 메모리 사용량을 검사합니다. 이는 일반적으로 사용 중인 실제 메모리의 상한을 제공합니다. 자세한 내용은 다음 문서를 참조하세요.

Blazor에 적용된 메모리 사용량

다음과 같이 사용되는 blazor 메모리를 계산합니다.

(활성 회로 × 회로당 메모리) + (연결이 끊긴 회로 × 회로당 메모리)

회로에서 사용하는 메모리 양과 앱이 유지할 수 있는 최대 잠재적 활성 회로는 주로 앱 작성 방법에 따라 달라집니다. 가능한 활성 회로의 최대 개수는 대략 다음과 같습니다.

사용 가능한 최대 메모리 / 회로당 메모리 = 최대 잠재적 활성 회로

Blazor에서 메모리 누수가 발생하려면 다음에 해당해야 합니다.

  • 메모리는 앱이 아닌 프레임워크에서 할당되어야 합니다. 앱에서 1GB 배열을 할당하는 경우, 앱은 배열의 삭제를 관리해야 합니다.
  • 메모리를 적극적으로 사용하면 안 됩니다. 즉, 회로가 활성화되지 않고 연결이 끊긴 회로 캐시에서 제거되어야 합니다. 최대 활성 회로가 실행 중인 경우, 메모리 부족은 메모리 누수가 아니라 스케일링 문제입니다.
  • 회로의 GC 생성에 대한 GC(가비지 수집)가 실행되었지만 프레임워크의 다른 개체가 회로에 대한 강력한 참조를 보유하고 있기 때문에 가비지 수집기가 회로를 클레임할 수 없습니다.

다른 경우에는 메모리 누수가 없습니다. 회로가 활성 상태이거나 연결이 끊어진 경우, 회로가 여전히 사용 중입니다.

회로의 GC 생성에 대한 컬렉션이 실행되지 않으면, 가비지 수집기가 해당 시간에 메모리를 해제할 필요가 없으므로 메모리가 해제되지 않습니다.

GC 생성에 대한 컬렉션이 실행되고 회로를 해제하는 경우, .NET이 가상 메모리를 활성 상태로 유지하기로 결정할 수 있으므로 프로세스가 아닌 GC 통계에 대해 메모리의 유효성을 검사해야 합니다.

메모리가 해제되지 않은 경우, 활성 또는 연결이 끊어지지 않고 프레임워크의 다른 개체에 의해 루팅된 회로를 찾아야 합니다. 다른 경우에 메모리를 해제할 수 없는 것은 개발자 코드의 앱 문제입니다.

메모리 사용량 감소

앱의 메모리 사용량을 줄이기 위해 다음 전략을 채택합니다.

  • .NET 프로세스에서 사용하는 총 메모리 양을 제한합니다. 자세한 내용은 가비지 수집에 대한 런타임 구성 옵션을 참조하세요.
  • 연결이 끊긴 회로 수를 줄입니다.
  • 회로가 연결이 끊어진 상태일 수 있는 시간을 줄입니다.
  • 가동 중지 시간 동안 수집이 수행하도록 가비지 수집을 수동으로 트리거합니다.
  • 서버 모드 대신 가비지 수집을 적극적으로 트리거하는 워크스테이션 모드에서 가비지 수집을 구성합니다.

일부 모바일 디바이스 브라우저의 힙 크기

클라이언트에서 실행되고 모바일 디바이스 브라우저, 특히 iOS의 Safari를 대상으로 하는 앱을 빌드 Blazor 할 때 MSBuild 속성을 EmccMaximumHeapSize 사용하여 앱의 최대 메모리를 줄여야 할 수 있습니다. 자세한 내용은 ASP.NET Core Blazor WebAssembly 호스트 및 배포를 참조하세요.

추가 작업 및 고려 사항

  • 메모리 요구 사항이 높은 경우 프로세스의 메모리 덤프를 캡처하고 개체가 가장 많은 메모리를 사용하고 해당 개체(참조를 보유하는 개체)가 루트되는 위치를 식별합니다.
  • 를 사용하여 dotnet-counters앱의 메모리가 어떻게 동작하는지에 대한 통계를 검사할 수 있습니다. 자세한 내용은 성능 카운터 조사(dotnet-counters)를 참조하세요.
  • GC가 트리거되는 경우에도 .NET은 가까운 장래에 메모리를 다시 사용할 가능성이 있으므로 OS에 즉시 반환하는 대신 메모리를 유지합니다. 이렇게 하면 메모리 커밋 및 커밋 해제가 지속적으로 방지되므로 비용이 많이 듭니다. dotnet-counters GC가 발생하고 사용된 메모리 양이 0(0)으로 내려가는 것을 볼 수 있지만 작업 집합 카운터 감소가 표시되지 않습니다. 이는 .NET이 메모리를 다시 사용하기 위해 메모리를 유지하고 있다는 신호입니다. 이 동작을 제어하기 위한 프로젝트 파일(.csproj) 설정에 대한 자세한 내용은 가비지 수집에 대한 런타임 구성 옵션을 참조하세요.
  • 서버 GC는 앱이 동결되는 것을 방지하기 위해 반드시 필요하다고 판단하고 앱이 컴퓨터에서 실행되는 유일한 항목이므로 시스템의 모든 메모리를 사용할 수 있다고 판단할 때까지 가비지 수집을 트리거하지 않습니다. 시스템에 50GB가 있는 경우, 가비지 수집기는 Gen 2 컬렉션을 트리거하기 전에 사용 가능한 메모리의 전체 50GB를 사용하려고 합니다.
  • 연결이 끊긴 회로 보존 구성에 대한 자세한 내용은 ASP.NET Core BlazorSignalR 지침을 참조하세요.

메모리 측정

  • 릴리스 구성에 앱을 게시합니다.
  • 게시된 버전의 앱을 실행합니다.
  • 실행 중인 앱에 디버거를 연결하지 마세요.
  • Gen 2 강제 압축 컬렉션을 트리거하나요(GC.Collect(2, GCCollectionMode.Aggressive | GCCollectionMode.Forced, blocking: true, compacting: true)) 메모리를 해제하나요?
  • 앱이 큰 개체 힙에 개체를 할당하는 경우를 고려합니다.
  • 앱이 요청 및 처리로 준비된 후 메모리 증가를 테스트하고 있나요? 일반적으로 코드가 처음 실행될 때 앱의 공간 공간으로 일정한 양의 메모리를 추가하는 캐시가 채워집니다.