ประสิทธิภาพที่น่ากลัวโดยใช้วิธี SqlCommand Async ที่มีข้อมูลขนาดใหญ่


95

ฉันมีปัญหาด้านประสิทธิภาพการทำงานของ SQL ที่สำคัญเมื่อใช้การโทรแบบ async ฉันได้สร้างกรณีเล็ก ๆ เพื่อแสดงให้เห็นถึงปัญหา

ฉันได้สร้างฐานข้อมูลบน SQL Server 2016 ซึ่งอยู่ใน LAN ของเรา (ไม่ใช่ 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 (zip ของชุดข้อมูล JSON ที่ใหญ่กว่า))

ตอนนี้ถ้าฉันดำเนินการค้นหาใน 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 มิลลิวินาที

แต่เมื่อเปลี่ยนเป็นรหัส async:

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

ทันใดนั้นเวลาดำเนินการคือ1800 มิลลิวินาที ! นอกจากนี้ใน SQL Server Profiler ฉันเห็นว่าระยะเวลาการดำเนินการเคียวรีนานกว่าหนึ่งวินาที แม้ว่าคำค้นหาที่ดำเนินการซึ่งรายงานโดย profiler จะเหมือนกับเวอร์ชันที่ไม่ใช่ Async ทุกประการ

แต่มันแย่ลง ถ้าฉันเล่นกับ Packet Size ในสตริงการเชื่อมต่อฉันจะได้ผลลัพธ์ดังต่อไปนี้:

ขนาดแพ็คเก็ต 32768: [TIMING]: ExecuteScalarAsync ใน SqlValueStore -> เวลาที่ผ่านไป: 450 ms

ขนาดแพ็คเก็ต 4096: [TIMING]: ExecuteScalarAsync ใน SqlValueStore -> เวลาที่ผ่านไป: 3667 ms

ขนาดแพ็กเก็ต 512: [TIMING]: ExecuteScalarAsync ใน SqlValueStore -> เวลาที่ผ่านไป: 30776 ms

30,000 มิลลิวินาที !! ช้ากว่าเวอร์ชันที่ไม่ใช่ async มากกว่า 1,000 เท่า และ SQL Server Profiler รายงานว่าการเรียกใช้แบบสอบถามใช้เวลานานกว่า 10 วินาที ที่ไม่ได้อธิบายว่าอีก 20 วินาทีหายไปไหน!

จากนั้นฉันก็เปลี่ยนกลับไปใช้เวอร์ชันซิงค์และเล่นกับ Packet Size และแม้ว่าจะส่งผลกระทบต่อเวลาดำเนินการเพียงเล็กน้อย แต่ก็ไม่มีที่ไหนที่น่าทึ่งเท่ากับเวอร์ชัน async

ในฐานะที่เป็นด้านข้างหากใส่เพียงสตริงขนาดเล็ก (<100 ไบต์) ลงในค่าการดำเนินการสืบค้นแบบ async จะเร็วเท่ากับเวอร์ชันซิงค์ (ผลลัพธ์ใน 1 หรือ 2 มิลลิวินาที)

ฉันรู้สึกงุนงงกับเรื่องนี้มากโดยเฉพาะอย่างยิ่งเมื่อฉันใช้บิวท์อินSqlConnectionไม่ใช่ ORM นอกจากนี้เมื่อค้นหาไปรอบ ๆ ฉันไม่พบสิ่งใดที่สามารถอธิบายพฤติกรรมนี้ได้ ความคิดใด ๆ ?


5
@hcd 1.5 MB ????? และคุณถามว่าทำไมการดึงข้อมูลจึงช้าลงด้วยขนาดแพ็คเก็ตที่ลดลง? โดยเฉพาะอย่างยิ่งเมื่อคุณใช้แบบสอบถามผิดสำหรับ BLOBs?
Panagiotis Kanavos

3
@PanagiotisKanavos นั่นเป็นเพียงการเล่นในนามของ OP คำถามที่แท้จริงคือทำไม async จึงช้ากว่ามากเมื่อเทียบกับการซิงค์กับขนาดแพ็คเกจเดียวกัน
Fildor

2
ตรวจสอบการแก้ไขข้อมูลขนาดใหญ่ (สูงสุด) ใน ADO.NETสำหรับวิธีการดึง CLOBs และ BLOB ที่ถูกต้อง แทนที่จะพยายามอ่านเป็นค่าเดียวให้ใช้GetSqlCharsหรือGetSqlBinaryเรียกดูในรูปแบบสตรีมมิง พิจารณาจัดเก็บเป็นข้อมูล FILESTREAM ด้วย - ไม่มีเหตุผลที่จะต้องบันทึกข้อมูล 1.5MB ในหน้าข้อมูลของตาราง
Panagiotis Kanavos

8
@PanagiotisKanavos ไม่ถูกต้อง OP เขียนการซิงค์: 20-30 ms และ async กับทุกอย่างที่เหมือนกัน 1800 ms ผลของการเปลี่ยนขนาดแพ็คเก็ตนั้นชัดเจนและคาดหวังได้ทั้งหมด
Fildor

5
@hcd ดูเหมือนว่าคุณสามารถลบส่วนที่เกี่ยวกับความพยายามในการปรับเปลี่ยนขนาดแพ็คเกจได้เนื่องจากดูเหมือนว่าไม่เกี่ยวข้องกับปัญหาและทำให้เกิดความสับสนในหมู่ผู้แสดงความคิดเห็น
Kuba Wyrostek

คำตอบ:


141

ในระบบที่ไม่มีภาระมากการเรียก async จะมีค่าโสหุ้ยที่ใหญ่กว่าเล็กน้อย แม้ว่าการดำเนินการ I / O จะเป็นแบบอะซิงโครนัสโดยไม่คำนึงถึงการบล็อกอาจเร็วกว่าการสลับงานเธรดพูล

ค่าโสหุ้ยเท่าไหร่? ลองดูตัวเลขเวลาของคุณ 30ms สำหรับการโทรแบบบล็อค 450ms สำหรับการโทรแบบอะซิงโครนัส ขนาดแพ็คเก็ต 32 kiB หมายความว่าคุณต้องการการดำเนินการ I / O ของแต่ละบุคคลประมาณห้าสิบรายการ นั่นหมายความว่าเรามีค่าใช้จ่ายประมาณ 8ms สำหรับแต่ละแพ็คเก็ตซึ่งสอดคล้องกับการวัดของคุณในขนาดแพ็กเก็ตที่แตกต่างกัน นั่นไม่ได้ฟังดูเหมือนโอเวอร์เฮดจากการเป็นแบบอะซิงโครนัสแม้ว่าเวอร์ชันอะซิงโครนัสจะต้องทำงานมากกว่าซิงโครนัส ดูเหมือนว่าเวอร์ชันซิงโครนัสคือ (แบบง่าย) 1 คำขอ -> 50 คำตอบในขณะที่เวอร์ชันอะซิงโครนัสจะกลายเป็น 1 คำขอ -> 1 คำตอบ -> 1 คำขอ -> 1 คำตอบ -> ... โดยจ่ายค่าใช้จ่ายซ้ำแล้วซ้ำเล่า อีกครั้ง.

ลึกลงไปอีก ExecuteReaderใช้งานได้เช่นเดียวกับExecuteReaderAsync. การดำเนินการต่อไปReadตามด้วยGetFieldValue- และสิ่งที่น่าสนใจก็เกิดขึ้นที่นั่น หากทั้งสองไม่ตรงกันการดำเนินการทั้งหมดจะช้า ดังนั้นจึงมีบางสิ่งที่แตกต่างออกไปอย่างแน่นอนเมื่อคุณเริ่มสร้างสิ่งต่าง ๆ แบบอะซิงโครนัสอย่างแท้จริง - a Readจะเร็วจากนั้นอะซิงโครนัสGetFieldValueAsyncจะช้าหรือคุณสามารถเริ่มด้วยการช้าReadAsyncจากนั้นทั้งสองอย่างGetFieldValueและGetFieldValueAsyncเร็ว การอ่านแบบอะซิงโครนัสครั้งแรกจากสตรีมนั้นช้าและความช้าจะขึ้นอยู่กับขนาดของแถวทั้งหมด ถ้าฉันเพิ่มแถวที่มีขนาดเท่ากันการอ่านแต่ละแถวจะใช้เวลาเท่ากันราวกับว่าฉันมีเพียงแถวเดียวดังนั้นจึงเห็นได้ชัดว่าข้อมูลนั้นยังคงสตรีมทีละแถว - ดูเหมือนว่าจะชอบอ่านทั้งแถวพร้อมกันเมื่อคุณเริ่มการอ่านแบบอะซิงโครนัส ถ้าฉันอ่านแถวแรกแบบอะซิงโครนัสและแถวที่สองพร้อมกัน - แถวที่สองที่อ่านจะเร็วอีกครั้ง

ดังนั้นเราจะเห็นว่าปัญหามีขนาดใหญ่ของแต่ละแถวและ / หรือคอลัมน์ ไม่สำคัญว่าคุณจะมีข้อมูลทั้งหมดเท่าใดการอ่านแถวเล็ก ๆ ล้านแถวแบบอะซิงโครนัสนั้นเร็วพอ ๆ กับซิงโครนัส แต่เพิ่มเพียงช่องเดียวที่ใหญ่เกินไปที่จะใส่ลงในแพ็คเก็ตเดียวและคุณต้องเสียค่าใช้จ่ายอย่างลึกลับในการอ่านข้อมูลนั้นแบบอะซิงโครนัสราวกับว่าแต่ละแพ็กเก็ตต้องการแพ็กเก็ตคำขอแยกต่างหากและเซิร์ฟเวอร์ไม่สามารถส่งข้อมูลทั้งหมดที่ ครั้งเดียว. การใช้CommandBehavior.SequentialAccessจะช่วยเพิ่มประสิทธิภาพตามที่คาดไว้ แต่ช่องว่างขนาดใหญ่ระหว่างการซิงค์และ async ยังคงมีอยู่

ประสิทธิภาพที่ดีที่สุดที่ฉันได้รับคือเมื่อทำทุกสิ่งอย่างถูกต้อง นั่นหมายถึงการใช้CommandBehavior.SequentialAccessและการสตรีมข้อมูลอย่างชัดเจน:

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

ด้วยเหตุนี้ความแตกต่างระหว่างการซิงค์และ async จึงยากที่จะวัดและการเปลี่ยนขนาดแพ็คเก็ตจะไม่ก่อให้เกิดค่าใช้จ่ายที่ไร้สาระเหมือนเมื่อก่อนอีกต่อไป

หากคุณต้องการผลงานที่ดีในกรณีขอบให้แน่ใจว่าจะใช้เครื่องมือที่ดีที่สุดที่มีอยู่ - ในกรณีนี้กระแสคอลัมน์ข้อมูลขนาดใหญ่มากกว่าอาศัยผู้ช่วยเหลือเหมือนหรือExecuteScalarGetFieldValue


3
คำตอบที่ดี จำลองสถานการณ์ของ OP สำหรับ OP 1.5m สตริงที่กล่าวถึงฉันได้รับ 130ms สำหรับเวอร์ชันซิงค์เทียบกับ 2200ms สำหรับ async ด้วยวิธีการของคุณเวลาที่วัดได้สำหรับสตริง 1.5 ม. คือ 60ms ไม่เลว
Wiktor Zychla

4
การตรวจสอบที่ดีที่นั่นและฉันได้เรียนรู้เทคนิคการปรับแต่งอื่น ๆ สำหรับรหัส DAL ของเรา
Adam Houldsworth

เพิ่งกลับมาที่สำนักงานและลองใช้รหัสในตัวอย่างของฉันแทน ExecuteScalarAsync แต่ฉันยังคงมีเวลาดำเนินการ 30 วินาทีด้วยขนาดแพ็คเก็ต 512 ไบต์ :(
hcd

6
Aha มันใช้งานได้จริง :) แต่ฉันต้องเพิ่ม CommandBehavior.SequentialAccess ในบรรทัดนี้: using (var reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess))
hcd

@hcd แย่จังฉันมีในข้อความ แต่ไม่มีในโค้ดตัวอย่าง :)
Luaan
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.