대용량 데이터와 함께 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).
특히 SqlConnection
ORM이 아닌 내장을 사용하고 있기 때문에 정말 당황합니다 . 또한 주변을 검색 할 때이 동작을 설명 할 수있는 것을 찾지 못했습니다. 어떤 아이디어?
상당한 부하가없는 시스템에서 비동기 호출은 약간 더 큰 오버 헤드를 갖습니다. I / O 작업 자체는 비동기식이지만 차단은 스레드 풀 작업 전환보다 빠를 수 있습니다.
오버 헤드는 얼마나됩니까? 타이밍 수치를 살펴 보겠습니다. 차단 호출의 경우 30ms, 비동기 호출의 경우 450ms. 32kiB 패킷 크기는 약 50 개의 개별 I / O 작업이 필요함을 의미합니다. 즉, 각 패킷에 약 8ms의 오버 헤드가 있으며, 이는 다양한 패킷 크기에 대한 측정 값과 매우 잘 일치합니다. 비동기 버전이 동기 버전보다 훨씬 더 많은 작업을 수행해야하지만 비동기 버전에서 발생하는 오버 헤드처럼 들리지 않습니다. 동기 버전은 (단순화) 1 요청-> 50 응답 인 반면, 비동기 버전은 결국 1 요청-> 1 응답-> 1 요청-> 1 응답-> ..., 비용을 계속해서 지불하는 것처럼 들립니다. 다시.
더 깊이 들어가. ExecuteReader
뿐만 아니라 ExecuteReaderAsync
. 다음 작업 Read
뒤에 GetFieldValue
-가 나오고 흥미로운 일이 발생합니다. 둘 중 하나가 비동기이면 전체 작업이 느립니다. 그래서 뭔가 확실히 거기에 매우 당신이 일이 진정으로 비동기하기 시작하면 다른 일이 생겼 - A는 Read
빠른 것, 다음 비동기가 GetFieldValueAsync
느려질 수, 또는 당신은 천천히 시작할 수 있습니다 ReadAsync
, 그리고 모두 GetFieldValue
와 GetFieldValueAsync
빠르다. 스트림에서 첫 번째 비동기 읽기는 느리고 느림은 전적으로 전체 행의 크기에 따라 다릅니다. 같은 크기의 행을 더 추가하면 각 행을 읽는 데 한 행만있는 것과 같은 시간이 걸리므로 데이터 가여전히 행별로 스트리밍되고 있습니다 . 비동기 읽기 를 시작 하면 한 번에 전체 행을 읽는 것을 선호하는 것 같습니다 . 첫 번째 행을 비동기 적으로 읽고 두 번째 행을 동 기적으로 읽으면 읽는 두 번째 행이 다시 빨라집니다.
So we can see that the problem is a big size of an individual row and/or column. It doesn't matter how much data you have in total - reading a million small rows asynchronously is just as fast as synchronously. But add just a single field that's too big to fit in a single packet, and you mysteriously incur a cost at asynchronously reading that data - as if each packet needed a separate request packet, and the server couldn't just send all the data at once. Using CommandBehavior.SequentialAccess
does improve the performance as expected, but the massive gap between sync and async still exists.
The best performance I got was when doing the whole thing properly. That means using CommandBehavior.SequentialAccess
, as well as streaming the data explicitly:
using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess))
{
while (await reader.ReadAsync())
{
var data = await reader.GetTextReader(0).ReadToEndAsync();
}
}
With this, the difference between sync and async becomes hard to measure, and changing the packet size no longer incurs the ridiculous overhead as before.
If you want good performance in edge cases, make sure to use the best tools available - in this case, stream large column data rather than relying on helpers like ExecuteScalar
or GetFieldValue
.
'Programing' 카테고리의 다른 글
github에서 요점의 이름을 변경하는 방법은 무엇입니까? (0) | 2020.09.16 |
---|---|
C ++에서 참조가 "const"가 아닌 이유는 무엇입니까? (0) | 2020.09.16 |
Python에서 호출 함수 모듈의 __name__ 가져 오기 (0) | 2020.09.16 |
Swift에서 슬라이스는 무엇입니까? (0) | 2020.09.16 |
루프는 print 문없이 다른 스레드에 의해 변경된 값을 보지 못함 (0) | 2020.09.16 |