[c#] 대용량 데이터와 함께 SqlCommand Async 메서드를 사용한 끔찍한 성능

비동기 호출을 사용할 때 주요 SQL 성능 문제가 있습니다. 문제를 설명하기 위해 작은 케이스를 만들었습니다.

LAN에있는 SQL Server 2016에 데이터베이스를 만들었습니다 (localDB가 아님).

해당 데이터베이스 WorkingCopy에는 2 개의 열 이있는 테이블 이 있습니다.

Id (nvarchar(255, PK))
Value (nvarchar(max))

DDL

CREATE TABLE [dbo].[Workingcopy]
(
    [Id] [nvarchar](255) NOT NULL,
    [Value] [nvarchar](max) NULL,

    CONSTRAINT [PK_Workingcopy]
        PRIMARY KEY CLUSTERED ([Id] ASC)
                    WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
                          IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON,
                          ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

이 테이블에 단일 레코드를 삽입했습니다 ( id= ‘PerfUnitTest’ Value는 1.5MB 문자열 (더 큰 JSON 데이터 세트의 zip)).

이제 SSMS에서 쿼리를 실행하면 다음과 같습니다.

SELECT [Value]
FROM [Workingcopy]
WHERE id = 'perfunittest'

즉시 결과를 얻었고 SQL Servre Profiler에서 실행 시간이 약 20 밀리 초라는 것을 알 수 있습니다. 모두 정상입니다.

일반을 사용하여 .NET (4.6) 코드에서 쿼리를 실행할 때 SqlConnection:

// at this point, the connection is already open
var command = new SqlCommand($"SELECT Value FROM WorkingCopy WHERE Id = @Id", _connection);
command.Parameters.Add("@Id", SqlDbType.NVarChar, 255).Value = key;

string value = command.ExecuteScalar() as string;

이를위한 실행 시간도 약 20-30 밀리 초입니다.

그러나 비동기 코드로 변경할 때 :

string value = await command.ExecuteScalarAsync() as string;

실행 시간이 갑자기 1800ms ! 또한 SQL Server Profiler에서 쿼리 실행 기간이 1 초 이상임을 알 수 있습니다. 프로파일 러에서보고 한 실행 된 쿼리는 비동기 버전과 완전히 동일합니다.

그러나 더 나빠집니다. 연결 문자열에서 패킷 크기를 가지고 놀면 다음과 같은 결과가 나타납니다.

패킷 크기 32768 : [TIMING] : SqlValueStore에서 ExecuteScalarAsync-> 경과 시간 : 450ms

Packet Size 4096 : [TIMING] : SqlValueStore의 ExecuteScalarAsync-> 경과 시간 : 3667 ms

패킷 크기 512 : [TIMING] : SqlValueStore의 ExecuteScalarAsync-> 경과 시간 : 30776 ms

30,000ms !! 비동기 버전보다 1000 배 이상 느립니다. 그리고 SQL Server Profiler는 쿼리 실행에 10 초 이상 걸린다고보고합니다. 그것은 다른 20 초가 어디로 갔는지도 설명하지 않습니다!

그런 다음 동기화 버전으로 다시 전환하고 패킷 크기를 사용해 보았습니다. 실행 시간에 약간의 영향을 미쳤지 만 비동기 버전만큼 극적인 것은 아닙니다.

참고로 값에 작은 문자열 (100 바이트 미만) 만 입력하면 비동기 쿼리 실행이 동기화 버전만큼 빠릅니다 (결과는 1ms 또는 2ms).

특히 SqlConnectionORM이 아닌 내장을 사용하고 있기 때문에 정말 당황합니다 . 또한 주변을 검색 할 때이 동작을 설명 할 수있는 것을 찾지 못했습니다. 어떤 아이디어?



답변

상당한 부하가없는 시스템에서 비동기 호출은 약간 더 큰 오버 헤드를 갖습니다. I / O 작업 자체는 비동기식이지만 차단은 스레드 풀 작업 전환보다 빠를 수 있습니다.

오버 헤드는 얼마나됩니까? 타이밍 수치를 살펴 보겠습니다. 차단 호출의 경우 30ms, 비동기 호출의 경우 450ms. 32kiB 패킷 크기는 약 50 개의 개별 I / O 작업이 필요함을 의미합니다. 즉, 각 패킷에 약 8ms의 오버 헤드가 있으며, 이는 다양한 패킷 크기에 대한 측정 값과 매우 잘 일치합니다. 비동기 버전이 동기 버전보다 훨씬 더 많은 작업을 수행해야하지만 비동기 버전에서 발생하는 오버 헤드처럼 들리지 않습니다. 동기 버전은 (단순화) 1 요청-> 50 응답 인 반면 비동기 버전은 결국 1 요청-> 1 응답-> 1 요청-> 1 응답-> …, 비용을 계속해서 지불하는 것처럼 들립니다. 다시.

더 깊이 들어가. ExecuteReader뿐만 아니라 ExecuteReaderAsync. 다음 작업 Read다음에 GetFieldValue-가 나오고 흥미로운 일이 발생합니다. 둘 중 하나가 비동기이면 전체 작업이 느립니다. 그래서 뭔가 확실히 거기에 매우 당신이 일이 진정으로 비동기하기 시작하면 다른 일이 생겼 – A는 Read빠른 것, 다음 비동기가 GetFieldValueAsync느려질 수, 또는 당신은 천천히 시작할 수 있습니다 ReadAsync, 그리고 모두 GetFieldValueGetFieldValueAsync빠르다. 스트림에서 첫 번째 비동기 읽기는 느리고 속도는 전적으로 전체 행의 크기에 따라 다릅니다. 같은 크기의 행을 더 추가하면 각 행을 읽는 데 한 행만있는 것과 같은 시간이 걸리므로 데이터 여전히 행 단위로 스트리밍되고 있습니다 . 비동기 읽기 를 시작 하면 한 번에 전체 행을 읽는 것을 선호하는 것 같습니다 . 첫 번째 행을 비동기 적으로 읽고 두 번째 행을 동 기적으로 읽으면 읽는 두 번째 행이 다시 빨라집니다.

따라서 문제가 개별 행 및 / 또는 열의 큰 크기라는 것을 알 수 있습니다. 총 데이터 양은 중요하지 않습니다. 백만 개의 작은 행을 비동기식으로 읽는 것은 동기식만큼 빠릅니다. 그러나 하나의 패킷에 들어가기에는 너무 큰 단일 필드 만 추가하면 해당 데이터를 비동기식으로 읽는 데 신비하게도 비용이 발생합니다. 마치 각 패킷에 별도의 요청 패킷이 필요하고 서버는 모든 데이터를 다음 주소로 보낼 수 없습니다. 한번. 를 사용하면 CommandBehavior.SequentialAccess예상대로 성능이 향상되지만 동기화와 비동기 사이의 큰 차이는 여전히 존재합니다.

내가 얻은 최고의 성능은 모든 것을 제대로 할 때였습니다. 즉 CommandBehavior.SequentialAccess,를 사용 하고 데이터를 명시 적으로 스트리밍하는 것을 의미합니다 .

using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess))
{
  while (await reader.ReadAsync())
  {
    var data = await reader.GetTextReader(0).ReadToEndAsync();
  }
}

이로 인해 동기화와 비동기의 차이를 측정하기가 어려워지고 패킷 크기를 변경해도 더 이상 이전처럼 말도 안되는 오버 헤드가 발생하지 않습니다.

엣지 케이스에서 좋은 성능을 원한다면 사용 가능한 최고의 도구를 사용해야합니다.이 경우에는 ExecuteScalar또는 같은 도우미에 의존하는 대신 큰 열 데이터를 스트리밍 GetFieldValue하세요.


답변