Hiệu suất khủng khiếp khi sử dụng các phương pháp SqlCommand Async với dữ liệu lớn


95

Tôi đang gặp sự cố lớn về hiệu suất SQL khi sử dụng lệnh gọi không đồng bộ. Tôi đã tạo một trường hợp nhỏ để chứng minh vấn đề.

Tôi đã tạo cơ sở dữ liệu trên SQL Server 2016 nằm trong mạng LAN của chúng tôi (vì vậy không phải là localDB).

Trong cơ sở dữ liệu đó, tôi có một bảng WorkingCopyvới 2 cột:

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]

Trong bảng đó, tôi đã chèn một bản ghi duy nhất ( id= 'PerfUnitTest', Valuelà một chuỗi 1,5mb (một zip của tập dữ liệu JSON lớn hơn)).

Bây giờ, nếu tôi thực hiện truy vấn trong SSMS:

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

Tôi ngay lập tức nhận được kết quả và tôi thấy trong SQL Servre Profiler rằng thời gian thực thi là khoảng 20 mili giây. Tất cả đều bình thường.

Khi thực thi truy vấn từ mã .NET (4.6) bằng cách sử dụng 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;

Thời gian thực hiện việc này cũng khoảng 20-30 mili giây.

Nhưng khi thay đổi nó thành mã không đồng bộ:

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

Thời gian thực hiện đột ngột là 1800 ms ! Ngoài ra trong SQL Server Profiler, tôi thấy rằng thời lượng thực thi truy vấn là hơn một giây. Mặc dù truy vấn được thực thi được trình biên dịch báo cáo là hoàn toàn giống với phiên bản không Async.

Nhưng nó trở nên tồi tệ hơn. Nếu tôi nghịch ngợm với Kích thước gói trong chuỗi kết nối, tôi nhận được kết quả sau:

Kích thước gói 32768: [TIMING]: ExecuteScalarAsync trong SqlValueStore -> thời gian đã trôi qua: 450 ms

Kích thước gói 4096: [TIMING]: ExecuteScalarAsync trong SqlValueStore -> thời gian đã trôi qua: 3667 ms

Kích thước gói 512: [TIMING]: ExecuteScalarAsync trong SqlValueStore -> thời gian đã trôi qua: 30776 ms

30.000 ms !! Chậm hơn 1000 lần so với phiên bản không đồng bộ. Và SQL Server Profiler báo cáo rằng việc thực thi truy vấn mất hơn 10 giây. Điều đó thậm chí không giải thích được 20 giây còn lại sẽ đi đến đâu!

Sau đó, tôi đã quay trở lại phiên bản đồng bộ và cũng chơi với Kích thước gói, và mặc dù nó có ảnh hưởng một chút đến thời gian thực thi, nhưng nó không có gì ấn tượng như với phiên bản không đồng bộ.

Như một ghi chú bên, nếu nó chỉ đặt một chuỗi nhỏ (<100byte) vào giá trị, thì việc thực thi truy vấn không đồng bộ cũng nhanh như phiên bản đồng bộ (kết quả trong 1 hoặc 2 mili giây).

Tôi thực sự bối rối bởi điều này, đặc biệt là vì tôi đang sử dụng cài sẵn SqlConnection, thậm chí không phải ORM. Ngoài ra, khi tìm kiếm xung quanh, tôi không tìm thấy gì có thể giải thích hành vi này. Bất kỳ ý tưởng?


5
@hcd 1,5 MB ????? Và bạn hỏi tại sao việc truy xuất chậm hơn khi kích thước gói giảm dần? Đặc biệt là khi bạn sử dụng truy vấn sai cho BLOB?
Panagiotis Kanavos

3
@PanagiotisKanavos Đó chỉ là trò đùa thay mặt cho OP. Câu hỏi thực tế là tại sao async lại chậm hơn nhiều so với đồng bộ với cùng kích thước gói.
Fildor 23/02/17

2
Kiểm tra Sửa đổi dữ liệu có giá trị lớn (tối đa) trong ADO.NET để biết cách truy xuất CLOB và BLOB chính xác. Thay vì cố gắng đọc chúng như một giá trị lớn, hãy sử dụng GetSqlCharshoặc GetSqlBinarytruy xuất chúng theo cách truyền trực tuyến. Cũng nên lưu trữ chúng dưới dạng dữ liệu FileStream - không có lý do gì để tiết kiệm 1.5MB dữ liệu trong trang dữ liệu của bảng
Panagiotis Kanavos

8
@PanagiotisKanavos Điều đó không chính xác. OP ghi đồng bộ: 20-30 ms và không đồng bộ với mọi thứ khác cùng 1800 ms. Hiệu quả của việc thay đổi kích thước gói là hoàn toàn rõ ràng và được mong đợi.
Fildor 23/02/17

5
@hcd có vẻ như bạn có thể xóa phần về nỗ lực thay đổi kích thước gói vì nó có vẻ không liên quan đến vấn đề và gây ra sự nhầm lẫn cho một số người bình luận.
Kuba Wyrostek

Câu trả lời:


140

Trên hệ thống không có tải đáng kể, cuộc gọi không đồng bộ có chi phí lớn hơn một chút. Mặc dù bản thân hoạt động I / O là không đồng bộ bất kể, việc chặn có thể nhanh hơn chuyển đổi tác vụ nhóm luồng.

Chi phí bao nhiêu? Hãy nhìn vào con số thời gian của bạn. 30ms cho cuộc gọi chặn, 450ms cho cuộc gọi không đồng bộ. Kích thước gói 32 kiB có nghĩa là bạn cần, bạn cần khoảng năm mươi hoạt động I / O riêng lẻ. Điều đó có nghĩa là chúng tôi có khoảng 8ms chi phí trên mỗi gói, tương ứng khá tốt với các phép đo của bạn trên các kích thước gói khác nhau. Điều đó không giống như chi phí chỉ vì không đồng bộ, mặc dù các phiên bản không đồng bộ cần thực hiện nhiều công việc hơn phiên bản đồng bộ. Có vẻ như phiên bản đồng bộ là (đơn giản hóa) 1 yêu cầu -> 50 phản hồi, trong khi phiên bản không đồng bộ kết thúc là 1 yêu cầu -> 1 phản hồi -> 1 yêu cầu -> 1 phản hồi -> ..., thanh toán nhiều lần lần nữa.

Đi sâu hơn. ExecuteReaderhoạt động tốt như ExecuteReaderAsync. Hoạt động tiếp theo được Readtheo sau bởi một GetFieldValue- và một điều thú vị xảy ra ở đó. Nếu một trong hai không đồng bộ, toàn bộ hoạt động sẽ chậm. Vì vậy, chắc chắn có điều gì đó rất khác sẽ xảy ra khi bạn bắt đầu làm cho mọi thứ thực sự không đồng bộ - a Readsẽ nhanh và sau đó không đồng bộ GetFieldValueAsyncsẽ chậm hoặc bạn có thể bắt đầu với tốc độ chậm ReadAsync, rồi cả hai GetFieldValueGetFieldValueAsyncđều nhanh. Lần đọc không đồng bộ đầu tiên từ luồng là chậm và độ chậm hoàn toàn phụ thuộc vào kích thước của toàn bộ hàng. Nếu tôi thêm nhiều hàng có cùng kích thước, đọc mỗi hàng có cùng một lượng thời gian như thể tôi chỉ có một hàng, do đó, nó rõ ràng rằng các dữ liệu vẫn đang được phát trực tiếp từng hàng - có vẻ như bạn muốn đọc toàn bộ hàng cùng một lúc khi bạn bắt đầu bất kỳ lần đọc không đồng bộ nào . Nếu tôi đọc hàng đầu tiên không đồng bộ và hàng thứ hai đồng bộ - hàng thứ hai đang được đọc sẽ nhanh trở lại.

Vì vậy, chúng ta có thể thấy rằng vấn đề là kích thước lớn của một hàng và / hoặc cột riêng lẻ. Tổng cộng bạn có bao nhiêu dữ liệu không quan trọng - việc đọc không đồng bộ một triệu hàng nhỏ cũng nhanh như đồng bộ. Nhưng chỉ thêm một trường quá lớn để vừa với một gói duy nhất và bạn phải chịu một cách bí ẩn khi đọc dữ liệu đó không đồng bộ - như thể mỗi gói cần một gói yêu cầu riêng biệt và máy chủ không thể gửi tất cả dữ liệu tại Một lần. Việc sử dụng CommandBehavior.SequentialAccesscải thiện hiệu suất như mong đợi, nhưng khoảng cách lớn giữa đồng bộ hóa và không đồng bộ vẫn tồn tại.

Hiệu suất tốt nhất mà tôi có được là khi thực hiện toàn bộ công việc một cách đúng đắn. Điều đó có nghĩa là sử dụng CommandBehavior.SequentialAccess, cũng như truyền trực tuyến dữ liệu một cách rõ ràng:

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

Với điều này, sự khác biệt giữa đồng bộ hóa và không đồng bộ trở nên khó đo lường và việc thay đổi kích thước gói không còn gây ra chi phí vô lý như trước.

Nếu bạn muốn có hiệu suất tốt trong các trường hợp cạnh, hãy đảm bảo sử dụng các công cụ tốt nhất hiện có - trong trường hợp này, truyền dữ liệu cột lớn thay vì dựa vào các trình trợ giúp như ExecuteScalarhoặc GetFieldValue.


3
Câu trả lời chính xác. Tái hiện kịch bản của OP. Đối với chuỗi 1,5m này mà OP đang đề cập, tôi nhận được 130ms cho phiên bản đồng bộ so với 2200ms cho không đồng bộ. Với cách tiếp cận của bạn, thời gian đo được cho đoạn dây 1,5m là 60ms, không tệ.
Wiktor Zychla

4
Điều tra tốt ở đó, cộng với tôi đã học được một số kỹ thuật điều chỉnh khác cho mã DAL của chúng tôi.
Adam Houldsworth

Vừa trở lại văn phòng và thử mã trên ví dụ của tôi thay vì ExecuteScalarAsync, nhưng tôi vẫn nhận được thời gian thực thi
30 giây

6
Aha, nó đã hoạt động sau tất cả :) Nhưng tôi phải thêm CommandBehavior.SequentialAccess vào dòng này: using (var reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess))
hcd 24/02/17

@hcd xấu của tôi, tôi đã có nó trong văn bản nhưng không có trong mã mẫu :)
Luaan
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.