핵심 성능 모범 사례 ASP.NET

Mike Rousos 작성

이 문서에서는 ASP.NET Core의 성능 모범 사례에 대한 지침을 제공합니다.

적극적으로 캐시

캐싱은 이 문서의 여러 부분에서 설명합니다. 자세한 내용은 ASP.NET Core의 응답 캐싱를 참조하세요.

핫 코드 경로 이해

이 문서에서 실행 코드 경로는 자주 호출되고 대부분의 실행 시간이 발생하는 코드 경로로 정의됩니다. 핫 코드 경로는 일반적으로 앱 스케일 아웃 및 성능을 제한하며 이 문서의 여러 부분에서 설명합니다.

호출 차단 방지

ASP.NET Core 앱은 많은 요청을 동시에 처리하도록 설계되어야 합니다. 비동기 API를 사용하면 작은 스레드 풀에서 차단 호출을 기다리지 않고 수천 개의 동시 요청을 처리할 수 있습니다. 장기 실행 동기 작업이 완료되기를 기다리는 대신 스레드가 다른 요청에서 작동할 수 있습니다.

ASP.NET Core 앱의 일반적인 성능 문제는 비동기일 수 있는 호출을 차단하는 것입니다. 동기 차단 호출이 많을 경우 스레드 풀이 고갈되고 응답 시간이 저하됩니다.

하지 마십시오:

  • Task.Wait 또는 Task.Result를 호출하여 비동기 실행을 차단합니다.
  • 일반 코드 경로에서 잠금을 획득합니다. ASP.NET Core 앱은 코드를 병렬로 실행하도록 설계될 때 가장 좋은 성과를 발휘합니다.
  • Task.Run을 호출하고 즉시 기다립니다. ASP.NET Core는 정상적인 스레드 풀 스레드에서 이미 앱 코드를 실행 하 고 있으므로 작업을 호출 하면 추가 불필요 한 스레드 풀 예약이 발생 합니다. 예약 된 코드에서 스레드를 차단 하는 경우에도 작업. 실행은이를 방지 하지 않습니다.

Do:

  • 핫 코드 경로 를 비동기식으로 만듭니다.
  • 비동기 API를 사용할 수 있는 경우 데이터 액세스, i/o 및 장기 실행 작업 Api를 비동기적으로 호출 합니다. 작업을 실행 하 여 동기 API를 비동기식으로 만듭니다 .
  • 컨트롤러/ Razor 페이지 작업을 비동기식으로 만듭니다. 비동기 / 대기 패턴을 활용 하기 위해 전체 호출 스택은 비동기입니다.

Perfview와 같은 프로파일러를 사용 하 여 스레드 풀에 자주 추가 되는 스레드를 찾을 수 있습니다. 이벤트는 스레드 Microsoft-Windows-DotNETRuntime/ThreadPoolWorkerThread/Start 풀에 추가 된 스레드를 나타냅니다.

여러 개의 작은 페이지에서 많은 컬렉션 반환

웹 페이지에서 한 번에 대량의 데이터를 로드 하지 않아야 합니다. 개체의 컬렉션을 반환할 때 성능 문제가 발생할 수 있는지 여부를 고려 합니다. 디자인에서 다음과 같은 잘못 된 결과를 생성할 수 있는지 여부를 확인 합니다.

이전 시나리오를 완화 하려면 페이지 매김을 추가 합니다 . 개발자는 페이지 크기 및 페이지 인덱스 매개 변수를 사용 하 여 일부 결과를 반환 하는 디자인을 선호 해야 합니다. 철저한 결과가 필요한 경우 페이지 매김을 사용하여 서버 리소스 잠금을 방지하기 위해 결과의 일괄 처리를 비동기적으로 채워야 합니다.

페이징 및 반환 된 레코드 수를 제한에 대 한 자세한 내용은 참조 하세요.

또는 를 IEnumerable<T> 반환합니다. IAsyncEnumerable<T>

작업에서 를 IEnumerable<T> 반환하면 serializer가 동기 컬렉션 반복을 수행합니다. 그 결과는 호출 차단이며 스레드 풀 고갈의 가능성도 있습니다. 동기 열거를 방지하려면 ToListAsync 열거형을 반환하기 전에 를 사용합니다.

ASP.NET Core 3.0부터 를 IAsyncEnumerable<T> IEnumerable<T> 비동기적으로 열거하는 대신 사용할 수 있습니다. 자세한 내용은 컨트롤러 작업 반환 형식을 참조하세요.

큰 개체 할당 최소화

.NET Core 가비지 수집기는 ASP.NET Core 앱에서 자동으로 메모리 할당 및 해제를 관리합니다. 일반적으로 자동 가비지 수집은 개발자가 메모리가 해제되는 방법이나 시기에 대해 걱정할 필요가 없다는 것을 의미합니다. 그러나 참조되지 않은 개체를 정리하는 데는 CPU 시간이 걸리므로 개발자는 핫 코드 경로 에서 개체 할당을 최소화해야 합니다. 가비지 수집은 큰 개체(> 85K바이트)에서 특히 비용이 많이 듭니다. 큰 개체는 큰 개체 힙에 저장되며 정리하려면 전체(2세대) 가비지 수집이 필요합니다. 0세대 및 1세대 컬렉션과 달리 2세대 컬렉션은 앱 실행을 일시적으로 일시 중단해야 합니다. 큰 개체를 자주 할당하고 할당을 해제하면 성능이 일관되지 않게 될 수 있습니다.

권장 사항:

  • 자주 사용되는 큰 개체를 캐싱하는 것이 좋습니다. 큰 개체를 캐시하는 것은 비용이 많이 드는 할당을 방지합니다.
  • ArrayPool을 <T> 사용하여 큰 배열을 저장하여 버퍼를 수행합니다.
  • 핫 코드 경로 에 수명이 짧은 많은 큰 개체를 할당하지 마십시오.

Perfview 및 검사에서 GC (가비지 수집) 통계를 검토 하 여 위와 같은 메모리 문제를 진단할 수 있습니다.

  • 가비지 수집 일시 중지 시간입니다.
  • 가비지 수집에 소요 되는 프로세서 시간의 비율입니다.
  • 0 세대, 1, 2 세대의 가비지 컬렉션 수입니다.

자세한 내용은 가비지 수집 및 성능을 참조 하세요.

데이터 액세스 및 i/o 최적화

데이터 저장소 및 다른 원격 서비스와의 상호 작용은 종종 ASP.NET Core 앱의 가장 느린 부분입니다. 데이터를 효율적으로 읽고 쓰는 것이 좋은 성능을 위해 중요 합니다.

권장 사항:

  • 모든 데이터 액세스 Api를 비동기적 으로 호출 합니다 .
  • 필요한 것 보다 더 많은 데이터를 검색 하지 않습니다. 현재 HTTP 요청에 필요한 데이터만 반환 하는 쿼리를 작성 합니다.
  • 약간 오래 된 데이터를 사용할 수 있는 경우 데이터베이스 또는 원격 서비스에서 검색 되는 자주 액세스 하는 데이터를 캐시 하 는 것이 좋습니다. 시나리오에 따라 Memorycache 또는 microsoft.web.distributedcache를 사용 합니다. 자세한 내용은 ASP.NET Core의 응답 캐싱를 참조하세요.
  • 네트워크 왕복 을 최소화 합니다 . 목표는 여러 호출이 아닌 단일 호출에서 필요한 데이터를 검색 하는 것입니다.
  • 읽기 전용 용도로 데이터에 액세스할 때 Entity Framework Core에서 추적 안 함 쿼리 를 사용 합니다 . EF Core 추적 되지 않는 쿼리 결과를 보다 효율적으로 반환할 수 있습니다.
  • LINQ 쿼리 (예:, 또는 문 포함) 를 필터링 하 고 집계 .Where 하 여 .Select .Sum 데이터베이스에서 필터링을 수행 하도록 합니다.
  • EF Core에서 클라이언트의 일부 쿼리 연산자를 확인 하므로 비효율적인 쿼리 실행이 발생할 수 있습니다. 자세한 내용은 클라이언트 평가 성능 문제를 참조 하세요.
  • 컬렉션에 프로젝션 쿼리를 사용하지 마십시오. 이 경우 "N + 1" SQL 쿼리가 실행됩니다. 자세한 내용은 상관 관계가 있는 하위 검색 최적화를 참조하세요.

대규모 앱에서 성능을 향상시킬 수 있는 방법은 EF 고성능을 참조하세요.

코드베이스를 커밋하기 전에 이전 고성능 접근 방식의 영향을 측정하는 것이 좋습니다. 컴파일된 쿼리의 추가 복잡성으로 성능 향상이 정당화되지 않을 수 있습니다.

쿼리 문제는 Application Insights 또는 프로파일링 도구를 사용하여 데이터에 액세스하는 데 소요된 시간을 검토하여 검색할 수 있습니다. 또한 대부분의 데이터베이스는 자주 실행되는 쿼리와 관련된 통계를 사용할 수 있도록 합니다.

HttpClientFactory를 통해 HTTP 연결 풀

HttpClient는 IDisposable 인터페이스를 구현하지만 다시 사용할 수 있도록 설계되었습니다. HttpClient닫힌 인스턴스는 소켓을 짧은 기간 동안 상태로 열어 TIME_WAIT 둡니다. 개체를 만들고 삭제하는 코드 HttpClient 경로가 자주 사용되는 경우 앱은 사용 가능한 소켓을 소모할 수 있습니다. HttpClientFactory는 이 문제에 대한 솔루션으로 ASP.NET Core 2.1에서 도입되었습니다. 풀링 HTTP 연결을 처리하여 성능 및 안정성을 최적화합니다.

권장 사항:

일반적인 코드 경로를 빠르게 유지

모든 코드를 빠르게 만들고자 합니다. 자주 호출되는 코드 경로는 최적화에 가장 중요합니다. 추가 설정은 다음과 같습니다.

  • 앱의 요청 처리 파이프라인에 있는 미들웨어 구성 요소, 특히 미들웨어는 파이프라인 초기에 실행됩니다. 이러한 구성 요소는 성능에 큰 영향을 줍니다.
  • 요청 마다 또는 요청 마다 여러 번 실행 되는 코드입니다. 예를 들어 사용자 지정 로깅, 권한 부여 처리기 또는 임시 서비스 초기화가 있습니다.

권장 사항:

HTTP 요청 외부에서 장기 실행 작업 완료

ASP.NET Core 앱에 대 한 대부분의 요청은 컨트롤러 또는 페이지 모델에서 필요한 서비스를 호출 하 고 HTTP 응답을 반환 하 여 처리할 수 있습니다. 장기 실행 작업을 포함 하는 일부 요청의 경우 전체 요청-응답 프로세스를 비동기식으로 만드는 것이 좋습니다.

권장 사항:

  • 일반 HTTP 요청 처리의 일부로 장기 실행 작업이 완료 될 때까지 기다리지 마세요 .
  • 백그라운드 서비스 를 사용 하 여 장기 실행 요청을 처리 하거나 Azure 함수를 사용 하 여 out-of-process를 처리 하는 것이 좋습니다 . Out-of-process 작업을 완료 하는 작업은 특히 CPU를 많이 사용 하는 작업에 유용 합니다.
  • 와 같은 실시간 통신 옵션 을 사용 하 여 SignalR 비동기적으로 클라이언트와 통신 합니다.

클라이언트 자산 축소

복잡 한 프런트 엔드가 있는 ASP.NET Core 앱은 종종 많은 JavaScript, CSS 또는 이미지 파일을 제공 합니다. 초기 로드 요청의 성능은 다음과 같은 방법으로 향상 시킬 수 있습니다.

  • 번들은 여러 파일을 하나로 결합 합니다.
  • 축소-공백과 주석을 제거 하 여 파일 크기를 줄입니다.

권장 사항:

  • 호환 되는 도구를 언급 하 고 ASP.NET Core 태그 를 사용 하 여 및 환경을 처리 하는 방법을 보여 주는 묶음 및 축소 지침을 사용 environment Development Production 합니다.
  • 복합 클라이언트 자산 관리를 위해 다른 타사 도구 (예: Webpack) 를 고려 합니다 .

응답 압축

응답 크기를 줄이면 일반적으로 앱의 응답성이 크게 증가합니다. 페이로드 크기를 줄이는 한 가지 방법은 앱의 응답을 압축하는 것입니다. 자세한 내용은 응답 압축을 참조하세요.

최신 ASP.NET Core 릴리스 사용

ASP.NET Core의 각 새 릴리스에는 성능 향상이 포함됩니다. .NET Core 및 ASP.NET Core의 최적화는 최신 버전이 일반적으로 이전 버전보다 우수하다는 것을 의미합니다. 예를 들어 .NET Core 2.1은 컴파일된 정규식에 대한 지원을 추가하고 Span <T> 을 활용했습니다. ASP.NET Core 2.2에는 HTTP/2에 대한 지원이 추가되었습니다. ASP.NET Core 3.0에는 메모리 사용량을 줄이고 처리량을 향상시키는 많은 개선이 추가되었습니다. 성능이 우선 순위인 경우 ASP.NET Core의 현재 버전으로 업그레이드하는 것이 좋습니다.

예외 최소화

예외는 드물어야 합니다. 예외를 throw하고 포착하는 것은 다른 코드 흐름 패턴에 비해 느립니다. 이로 인해 예외는 정상적인 프로그램 흐름을 제어하는 데 사용하면 안 됩니다.

권장 사항:

  • 특히 핫 코드 경로에서 예외를 throw하거나 포착하는 것은 정상적인 프로그램 흐름의 수단으로 사용하지 마십시오.
  • 예외를 발생시키는 조건을 검색하고 처리하려면 앱에 논리를 포함합니다.
  • 비정상적이거나 예기치 않은 조건에 대한 예외를 throw하거나 catch합니다.

Application Insights 같은 앱 진단 도구는 성능에 영향을 줄 수 있는 앱의 일반적인 예외를 식별하는 데 도움이 될 수 있습니다.

성능 및 안정성

다음 섹션에서는 성능 팁과 알려진 안정성 문제 및 솔루션을 제공합니다.

HttpRequest/HttpResponse 본문에서 동기 읽기 또는 쓰기 방지

ASP.NET Core의 모든 i/o는 비동기입니다. 서버는 Stream 동기 및 비동기 오버 로드를 모두 포함 하는 인터페이스를 구현 합니다. 스레드 풀 스레드를 차단 하지 않도록 비동기를 사용 하는 것이 좋습니다. 스레드를 차단 하면 스레드 풀이 고갈 될 수 있습니다.

이 작업을 수행 하지 마십시오. 다음 예제에서는를 사용 합니다 ReadToEnd . 결과를 대기 하는 현재 스레드를 차단 합니다. 이는 async를 통한 동기화의 예입니다.

public class BadStreamReaderController : Controller
{
    [HttpGet("/contoso")]
    public ActionResult<ContosoData> Get()
    {
        var json = new StreamReader(Request.Body).ReadToEnd();

        return JsonSerializer.Deserialize<ContosoData>(json);
    }
}

위의 코드에서는 Get 전체 HTTP 요청 본문을 메모리에 동기적으로 읽습니다. 클라이언트에 느린 업로드가 있으면 앱이 비동기를 통해 동기화 됩니다. Kestrel는 동기 읽기를 지원 하지 않기 때문에 앱은 async를 통해 동기화 됩니다.

다음 작업을 수행 합니다. 다음 예제에서는를 사용 하 ReadToEndAsync 고 읽는 동안 스레드를 차단 하지 않습니다.

public class GoodStreamReaderController : Controller
{
    [HttpGet("/contoso")]
    public async Task<ActionResult<ContosoData>> Get()
    {
        var json = await new StreamReader(Request.Body).ReadToEndAsync();

        return JsonSerializer.Deserialize<ContosoData>(json);
    }

}

위의 코드는 전체 HTTP 요청 본문을 메모리에 비동기적으로 읽습니다.

경고

요청이 클 경우 전체 HTTP 요청 본문을 메모리로 읽으면 OOM (메모리 부족) 조건이 발생할 수 있습니다. OOM은 서비스 거부를 유발할 수 있습니다. 자세한 내용은이 문서의 메모리에 대 한 대량 요청 본문 또는 응답 본문 읽기 방지 를 참조 하세요.

다음 작업을 수행 합니다. 다음 예제는 버퍼링 되지 않은 요청 본문을 사용 하 여 완전 비동기입니다.

public class GoodStreamReaderController : Controller
{
    [HttpGet("/contoso")]
    public async Task<ActionResult<ContosoData>> Get()
    {
        return await JsonSerializer.DeserializeAsync<ContosoData>(Request.Body);
    }
}

위의 코드는 요청 본문을 c # 개체로 비동기식으로 역직렬화 합니다.

요청을 통해 ReadFormAsync를 선호 합니다.

HttpContext.Request.Form 대신 HttpContext.Request.ReadFormAsync을(를) 사용합니다. HttpContext.Request.Form 는 다음과 같은 경우에만 안전 하 게 읽을 수 있습니다.

  • 을 호출 하 여 폼을 읽은 ReadFormAsync 경우
  • 를 사용하여 캐시된 양식 값을 읽습니다. HttpContext.Request.Form

이렇게 하지 마십시오. 다음 예제에서는 HttpContext.Request.Form 를 사용합니다. HttpContext.Request.Form비동기를 통해 동기화를 사용하며 스레드 풀 고갈로 이어질 수 있습니다.

public class BadReadController : Controller
{
    [HttpPost("/form-body")]
    public IActionResult Post()
    {
        var form =  HttpContext.Request.Form;

        Process(form["id"], form["name"]);

        return Accepted();
    }

이 작업을 수행합니다. 다음 예제에서는 를 사용하여 HttpContext.Request.ReadFormAsync 양식 본문을 비동기적으로 읽습니다.

public class GoodReadController : Controller
{
    [HttpPost("/form-body")]
    public async Task<IActionResult> Post()
    {
       var form = await HttpContext.Request.ReadFormAsync();

        Process(form["id"], form["name"]);

        return Accepted();
    }

큰 요청 신체 또는 응답 신체를 메모리로 읽지 않도록 방지

.NET에서 85KB를 초과하는 모든 개체 할당은 큰 개체 힙(LOH)에 포함됩니다. 큰 개체는 다음 두 가지 방법으로 비용이 많이 듭니다.

  • 새로 할당된 큰 개체에 대한 메모리를 지워야 하므로 할당 비용이 높습니다. CLR은 새로 할당된 모든 개체에 대한 메모리가 지워지게 합니다.
  • LOH는 나머지 힙과 함께 수집됩니다. LOH에는 전체 가비지 수집 또는 Gen2 수집이 필요합니다.

블로그 게시물에서는 문제를 간결하게 설명합니다.

큰 개체가 할당되면 Gen 2 개체로 표시됩니다. 작은 개체의 경우 Gen 0이 아닙니다. 그 결과 LOH에서 메모리가 부족하면 GC는 LOH뿐만 아니라 관리되는 전체 힙을 정리합니다. 따라서 LOH를 포함하여 Gen 0, Gen 1 및 Gen 2를 정리합니다. 이를 전체 가비지 수집이라고 하며 시간이 가장 많이 걸리는 가비지 수집입니다. 많은 애플리케이션에서 허용될 수 있습니다. 그러나 평균 웹 요청을 처리하는 데 필요한 큰 메모리 버퍼가 거의 없는 고성능 웹 서버의 경우는 아닙니다(소켓에서 읽기, 압축 풀기, JSON 디코딩 & 추가).

큰 요청 또는 응답 본문을 단일 또는 에 순차적으로 저장합니다. byte[] string

  • LOH의 공간이 빠르게 부족 해질 수 있습니다.
  • 전체 Gc를 실행 하 여 앱에 대 한 성능 문제를 일으킬 수 있습니다.

동기 데이터 처리 API 작업

동기 읽기 및 쓰기를 지 원하는 serializer/직렬 변환기를 사용 하는 경우 (예: JSON.NET):

  • 데이터를 직렬 변환기/역직렬화로 전달 하기 전에 비동기적으로 메모리에 버퍼링 합니다.

경고

요청이 크면 OOM (메모리 부족) 조건이 발생할 수 있습니다. OOM은 서비스 거부를 유발할 수 있습니다. 자세한 내용은이 문서의 메모리에 대 한 대량 요청 본문 또는 응답 본문 읽기 방지 를 참조 하세요.

ASP.NET Core 3.0는 System.Text.Json 기본적으로 JSON serialization에 사용 됩니다. System.Text.Json:

  • JSON을 비동기적으로 읽고 씁니다.
  • UTF-8 텍스트에 최적화되어 있습니다.
  • 일반적으로 Newtonsoft.Json보다 성능이 높습니다.

IHttpContextAccessor를 필드에 저장 하지 마십시오.

IHttpContextAccessorHttpContext 요청 스레드에서 액세스 될 때 활성 요청의를 반환 합니다. 는 IHttpContextAccessor.HttpContext 필드 또는 변수에 저장 하면 안 됩니다 .

이 작업을 수행 하지 마십시오. 다음 예에서는를 HttpContext 필드에 저장 한 다음 나중에 사용 하려고 시도 합니다.

public class MyBadType
{
    private readonly HttpContext _context;
    public MyBadType(IHttpContextAccessor accessor)
    {
        _context = accessor.HttpContext;
    }

    public void CheckAdmin()
    {
        if (!_context.User.IsInRole("admin"))
        {
            throw new UnauthorizedAccessException("The current user isn't an admin");
        }
    }
}

위의 코드는 생성자에서 null 또는 잘못 된를 캡처하는 경우가 많습니다 HttpContext .

다음 작업을 수행 합니다. 다음 예제를 수행 합니다.

  • IHttpContextAccessor 필드에 저장 합니다.
  • HttpContext는 필드를 올바른 시간에 사용 하 고를 확인 null 합니다.
public class MyGoodType
{
    private readonly IHttpContextAccessor _accessor;
    public MyGoodType(IHttpContextAccessor accessor)
    {
        _accessor = accessor;
    }

    public void CheckAdmin()
    {
        var context = _accessor.HttpContext;
        if (context != null && !context.User.IsInRole("admin"))
        {
            throw new UnauthorizedAccessException("The current user isn't an admin");
        }
    }
}

여러 스레드에서 HttpContext에 액세스 하지 않음

HttpContext 는 스레드로부터 안전 하지 않습니다 . HttpContext여러 스레드에서 병렬로 액세스 하면 중단, 충돌 및 데이터 손상과 같은 정의 되지 않은 동작이 발생할 수 있습니다.

이렇게 하지 마십시오. 다음 예제에서는 세 개의 병렬 요청을 만들고 나가는 HTTP 요청 전후에 들어오는 요청 경로를 기록합니다. 요청 경로는 잠재적으로 병렬로 여러 스레드에서 액세스됩니다.

public class AsyncBadSearchController : Controller
{       
    [HttpGet("/search")]
    public async Task<SearchResults> Get(string query)
    {
        var query1 = SearchAsync(SearchEngine.Google, query);
        var query2 = SearchAsync(SearchEngine.Bing, query);
        var query3 = SearchAsync(SearchEngine.DuckDuckGo, query);

        await Task.WhenAll(query1, query2, query3);

        var results1 = await query1;
        var results2 = await query2;
        var results3 = await query3;

        return SearchResults.Combine(results1, results2, results3);
    }       

    private async Task<SearchResults> SearchAsync(SearchEngine engine, string query)
    {
        var searchResults = _searchService.Empty();
        try
        {
            _logger.LogInformation("Starting search query from {path}.", 
                                    HttpContext.Request.Path);
            searchResults = _searchService.Search(engine, query);
            _logger.LogInformation("Finishing search query from {path}.", 
                                    HttpContext.Request.Path);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed query from {path}", 
                             HttpContext.Request.Path);
        }

        return await searchResults;
    }

이 작업을 수행합니다. 다음 예제에서는 세 개의 병렬 요청을 만들기 전에 들어오는 요청에서 모든 데이터를 복사합니다.

public class AsyncGoodSearchController : Controller
{       
    [HttpGet("/search")]
    public async Task<SearchResults> Get(string query)
    {
        string path = HttpContext.Request.Path;
        var query1 = SearchAsync(SearchEngine.Google, query,
                                 path);
        var query2 = SearchAsync(SearchEngine.Bing, query, path);
        var query3 = SearchAsync(SearchEngine.DuckDuckGo, query, path);

        await Task.WhenAll(query1, query2, query3);

        var results1 = await query1;
        var results2 = await query2;
        var results3 = await query3;

        return SearchResults.Combine(results1, results2, results3);
    }

    private async Task<SearchResults> SearchAsync(SearchEngine engine, string query,
                                                  string path)
    {
        var searchResults = _searchService.Empty();
        try
        {
            _logger.LogInformation("Starting search query from {path}.",
                                   path);
            searchResults = await _searchService.SearchAsync(engine, query);
            _logger.LogInformation("Finishing search query from {path}.", path);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed query from {path}", path);
        }

        return await searchResults;
    }

요청이 완료된 후 HttpContext를 사용하지 마십시오.

HttpContext 는 ASP.NET Core 파이프라인에 활성 HTTP 요청이 있는 한 유효합니다. 전체 ASP.NET Core 파이프라인은 모든 요청을 실행하는 대리자의 비동기 체인입니다. 이 Task 체인에서 반환된 가 완료되면 HttpContext 가 재활용됩니다.

이렇게 하지 마십시오. 다음 예제에서는 첫 번째 에 async void 도달할 때 HTTP 요청을 완료하는 를 사용합니다. await

  • 이는 ASP.NET Core 앱에서 항상 잘못된 사례입니다.
  • HttpResponseHTTP 요청이 완료된 후 에 액세스합니다.
  • 프로세스와 충돌합니다.
public class AsyncBadVoidController : Controller
{
    [HttpGet("/async")]
    public async void Get()
    {
        await Task.Delay(1000);

        // The following line will crash the process because of writing after the 
        // response has completed on a background thread. Notice async void Get()

        await Response.WriteAsync("Hello World");
    }
}

이 작업을 수행합니다. 다음 예제에서는 Task 프레임워크에 를 반환하므로 작업이 완료될 때까지 HTTP 요청이 완료되지 않습니다.

public class AsyncGoodTaskController : Controller
{
    [HttpGet("/async")]
    public async Task Get()
    {
        await Task.Delay(1000);

        await Response.WriteAsync("Hello World");
    }
}

백그라운드 스레드에서 HttpContext를 캡처하지 않음

이렇게 하지 마십시오. 다음 예제에서는 클로저가 속성에서 를 캡처하는 HttpContext 것을 Controller 보여줍니다. 작업 항목이 다음을 수행할 수 있기 때문에 이는 잘못된 사례입니다.

  • 요청 범위 외부에서 실행합니다.
  • 잘못된 를 읽으려고 HttpContext 시도합니다.
[HttpGet("/fire-and-forget-1")]
public IActionResult BadFireAndForget()
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        var path = HttpContext.Request.Path;
        Log(path);
    });

    return Accepted();
}

이 작업을 수행합니다. 다음 예제는 다음과 같습니다.

  • 요청 중에 백그라운드 작업에 필요한 데이터를 복사합니다.
  • 컨트롤러에서 아무것도 참조하지 않습니다.
[HttpGet("/fire-and-forget-3")]
public IActionResult GoodFireAndForget()
{
    string path = HttpContext.Request.Path;
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        Log(path);
    });

    return Accepted();
}

백그라운드 작업은 호스팅된 서비스로 구현 되어야 합니다. 자세한 내용은 호스티드 서비스를 사용하는 백그라운드 작업을 참조하세요.

백그라운드 스레드에서 컨트롤러에 삽입 된 서비스를 캡처하지 마십시오.

이 작업을 수행 하지 마십시오. 다음 예제에서는 DbContext 작업 매개 변수에서를 캡처하는 방법을 보여 줍니다 Controller . 이는 잘못 된 방법입니다. 작업 항목은 요청 범위 외부에서 실행 될 수 있습니다. ContosoDbContext이 요청으로 범위가 지정 되 면이 발생 ObjectDisposedException 합니다.

[HttpGet("/fire-and-forget-1")]
public IActionResult FireAndForget1([FromServices]ContosoDbContext context)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        context.Contoso.Add(new Contoso());
        await context.SaveChangesAsync();
    });

    return Accepted();
}

다음 작업을 수행 합니다. 다음 예제를 수행 합니다.

  • IServiceScopeFactory백그라운드 작업 항목에서 범위를 만들기 위해를 삽입 합니다. IServiceScopeFactory 는 단일 항목입니다.
  • 백그라운드 스레드에서 새 종속성 주입 범위를 만듭니다.
  • 는 컨트롤러에서 아무것도 참조 하지 않습니다.
  • ContosoDbContext들어오는 요청에서를 캡처하지 않습니다.
[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory 
                                    serviceScopeFactory)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        using (var scope = serviceScopeFactory.CreateScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<ContosoDbContext>();

            context.Contoso.Add(new Contoso());

            await context.SaveChangesAsync();                                        
        }
    });

    return Accepted();
}

강조 표시 된 코드는 다음과 같습니다.

  • 백그라운드 작업의 수명 범위를 만들고 해당 작업에서 서비스를 확인 합니다.
  • ContosoDbContext는 올바른 범위에서를 사용 합니다.
[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory 
                                    serviceScopeFactory)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        using (var scope = serviceScopeFactory.CreateScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<ContosoDbContext>();

            context.Contoso.Add(new Contoso());

            await context.SaveChangesAsync();                                        
        }
    });

    return Accepted();
}

응답 본문이 시작 된 후 상태 코드 또는 헤더를 수정 하지 마십시오.

ASP.NET Core는 HTTP 응답 본문을 버퍼링 하지 않습니다. 처음 응답을 쓸 때:

  • 헤더는 해당 본문의 청크와 함께 클라이언트에 전송 됩니다.
  • 더 이상 응답 헤더를 변경할 수 없습니다.

이렇게 하지 마십시오. 다음 코드는 응답이 이미 시작된 후 응답 헤더를 추가하려고 시도합니다.

app.Use(async (context, next) =>
{
    await next();

    context.Response.Headers["test"] = "test value";
});

앞의 코드에서 가 context.Response.Headers["test"] = "test value"; 응답에 쓴 경우 next() 는 예외를 throw합니다.

이 작업을 수행합니다. 다음 예제에서는 헤더를 수정하기 전에 HTTP 응답이 시작되었는지 확인합니다.

app.Use(async (context, next) =>
{
    await next();

    if (!context.Response.HasStarted)
    {
        context.Response.Headers["test"] = "test value";
    }
});

이 작업을 수행합니다. 다음 예제에서는 를 사용하여 HttpResponse.OnStarting 응답 헤더가 클라이언트에 플러시되기 전에 헤더를 설정합니다.

응답이 시작되지 않은지 확인하면 응답 헤더가 작성되기 직전에 호출될 콜백을 등록할 수 있습니다. 응답이 시작되지 않은지 확인:

  • 헤더를 적시에 추가하거나 재정의하는 기능을 제공합니다.
  • 파이프라인의 다음 미들웨어에 대한 지식이 필요하지 않습니다.
app.Use(async (context, next) =>
{
    context.Response.OnStarting(() =>
    {
        context.Response.Headers["someheader"] = "somevalue";
        return Task.CompletedTask;
    });

    await next();
});

응답 본문에 쓰기를 이미 시작한 경우 next()를 호출하지 마십시오.

구성 요소는 응답을 처리하고 조작할 수 있는 경우에만 호출되어야 합니다.

IIS에서 In-process 호스팅 사용

In-Process 호스팅을 사용하면 ASP.NET Core 앱은 IIS 작업자 프로세스와 동일한 프로세스에서 실행됩니다. In Process 호스팅은 요청이 루프백 어댑터를 통해 프록시되지 않으므로 Out of Process 호스팅에 비해 향상된 성능을 제공합니다. 루프백 어댑터는 나가는 네트워크 트래픽을 동일한 컴퓨터로 다시 반환하는 네트워크 인터페이스입니다. IIS는 Windows Process Activation Service(WAS)를 사용하여 프로세스 관리를 처리합니다.

프로젝트는 기본적으로 ASP.NET Core 3.0 이상에서 in-process 호스팅 모델로 설정됩니다.

자세한 내용은 IIS를 통해 Windows에서 ASP.NET Core 호스트를 참조하세요.