बड़े डेटा के साथ SqlCommand Async विधियों का उपयोग करते हुए भयानक प्रदर्शन


95

मुझे एसक्यूएल कॉल का उपयोग करते समय मुख्य एसक्यूएल प्रदर्शन समस्याएं हैं। मैंने समस्या को प्रदर्शित करने के लिए एक छोटा सा मामला बनाया है।

मैंने SQL Server 2016 पर एक डेटाबेस बनाया है जो हमारे LAN में रहता है (इसलिए लोकलडीबी नहीं है)।

उस डेटाबेस में, मेरे पास WorkingCopy2 कॉलम वाली एक तालिका है :

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', 1.5mb Valueस्ट्रिंग (बड़ा 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 सर्वर प्रोफाइलर में भी, मैं देखता हूं कि क्वेरी निष्पादन की अवधि एक सेकंड से अधिक है। हालाँकि प्रोफाइलर द्वारा बताई गई निष्पादित क्वेरी बिल्कुल गैर-एस्किंक संस्करण के समान है।

लेकिन यह बदतर हो जाता है। यदि मैं कनेक्शन स्ट्रिंग में पैकेट के आकार के साथ खेलता हूं, तो मुझे निम्नलिखित परिणाम मिलते हैं:

पैकेट का आकार 32768: [समय]: SqlValueStore में ExecuteScalarAsync -> बीता हुआ समय: 450 मि।

पैकेट का आकार 4096: [समय]: SqlValueStore में ExecuteScalarAsync -> बीता हुआ समय: 3667 एमएस

पैकेट का आकार 512: [समय]: SqlValueStore में ExecuteScalarAsync -> बीता हुआ समय: 30776 एमएस

30,000 एमएस !! यह नॉन-एसिक्स संस्करण की तुलना में 1000x से अधिक धीमा है। और SQL सर्वर प्रोफाइलर रिपोर्ट करता है कि क्वेरी निष्पादन 10 सेकंड से अधिक समय लगा। यह भी स्पष्ट नहीं है कि अन्य 20 सेकंड कहाँ गए हैं!

फिर मैंने सिंक संस्करण पर वापस स्विच किया और पैकेट के आकार के साथ भी खेला, और हालांकि इसने निष्पादन के समय को थोड़ा प्रभावित किया, यह async संस्करण के साथ नाटकीय रूप में कहीं नहीं था।

एक सिडेनोट के रूप में, अगर यह मूल्य में सिर्फ एक छोटी स्ट्रिंग (<100bytes) डालता है, तो async क्वेरी निष्पादन सिंक संस्करण (परिणाम 1 या 2 एमएस में) के समान तेज़ है।

मैं वास्तव में इससे बहुत प्रभावित हुआ हूँ, खासकर जब से मैं SqlConnectionएक ORM भी नहीं बना रहा हूँ । इसके अलावा जब आसपास खोज की, तो मुझे कुछ भी नहीं मिला जो इस व्यवहार की व्याख्या कर सके। कोई विचार?


4
@ एलसीडी 1.5 एमबी ????? और आप पूछते हैं कि क्यों घटते पैकेट आकार के साथ धीमा हो जाता है ? खासकर जब आप BLOB के लिए गलत क्वेरी का उपयोग करते हैं ?
पनगीओटीस कानावोस

3
@PanagiotisKanavos यह सिर्फ ओपी की ओर से खेल रहा था। वास्तविक प्रश्न यह है कि समान पैकेज के आकार के साथ सिंक की तुलना में एसिंक्स इतना धीमा क्यों है ।
फेल्डर

2
CLOB और BLOB को पुनः प्राप्त करने के सही तरीके के लिए ADO.NET में बड़े-मूल्य (अधिकतम) डेटा की जाँच करें । एक बड़े मूल्य के रूप में उन्हें पढ़ने की कोशिश करने के बजाय , स्ट्रीमिंग फैशन में उनका उपयोग करें GetSqlCharsया GetSqlBinaryउन्हें पुनः प्राप्त करने के लिए। इन्हें स्टोरस्ट्राम डेटा के रूप में संग्रहीत करने पर भी विचार करें - तालिका के डेटा पृष्ठ में 1.5MB डेटा सहेजने का कोई कारण नहीं है
Panagiotis Kanavos

8
@PanagiotisKanavos यह सही नहीं है। ओपी सिंक लिखता है: 20-30 एमएस और एसिंक्स बाकी सब कुछ उसी 1800 एमएस के साथ। पैकेट के आकार को बदलने का प्रभाव पूरी तरह से स्पष्ट और अपेक्षित है।
फीलर

5
@ एलसीडी ऐसा लगता है कि आप पैकेज के आकारों में बदलाव के अपने प्रयासों के बारे में हिस्सा निकाल सकते हैं क्योंकि यह समस्या के लिए अप्रासंगिक लगता है और कुछ टिप्पणीकारों के बीच भ्रम का कारण बनता है।
कुबा व्येरोसेक

जवाबों:


140

महत्वपूर्ण लोड के बिना एक सिस्टम पर, एक एसिंक्स कॉल में थोड़ा बड़ा ओवरहेड होता है। हालांकि I / O ऑपरेशन अपने आप में अतुल्यकालिक है, भले ही ब्लॉकिंग थ्रेड-पूल टास्क स्विचिंग से तेज हो।

कितना उपरि? चलिए आपके टाइमिंग नंबर्स पर नजर डालते हैं। एक अवरुद्ध कॉल के लिए 30ms, एक अतुल्यकालिक कॉल के लिए 450ms। 32 kiB पैकेट आकार का मतलब है कि आपको पचास व्यक्तिगत I / O संचालन की आवश्यकता है। इसका मतलब है कि हमारे पास प्रत्येक पैकेट पर लगभग 8ms ओवरहेड है, जो विभिन्न पैकेट आकारों पर आपके माप के साथ बहुत अच्छी तरह से मेल खाता है। यह सिर्फ अतुल्यकालिक होने से ओवरहेड की तरह ध्वनि नहीं करता है, भले ही अतुल्यकालिक संस्करणों को सिंक्रोनस की तुलना में बहुत अधिक काम करने की आवश्यकता है। ऐसा लगता है कि तुल्यकालिक संस्करण है (सरलीकृत) 1 अनुरोध -> 50 प्रतिक्रियाएं, जबकि एसिंक्रोनस संस्करण 1 अनुरोध समाप्त होता है -> 1 प्रतिक्रिया -> 1 अनुरोध -> 1 प्रतिक्रिया -> ..., लागत से अधिक का भुगतान करना फिर।

गहराई तक जा रहे हैं। ExecuteReaderसाथ ही साथ काम करता है ExecuteReaderAsync। अगले ऑपरेशन के Readबाद GetFieldValue- और एक दिलचस्प बात होती है। यदि दोनों में से कोई भी समान नहीं है, तो पूरा ऑपरेशन धीमा है। तो वहाँ निश्चित रूप से कुछ बहुत अलग हो रहा है एक बार आप चीजों को सही मायने में अतुल्यकालिक कमाना शुरू - एक Readतेजी से हो जाएगा, और उसके बाद async GetFieldValueAsyncधीमी गति से हो जाएगा, या आप धीमी गति के साथ शुरू कर सकते हैं ReadAsync, और फिर दोनों GetFieldValueऔर GetFieldValueAsyncतेजी से कर रहे हैं। स्ट्रीम से पहला अतुल्यकालिक पाठ धीमा है, और धीमापन पूरी तरह से पूरी पंक्ति के आकार पर निर्भर करता है। यदि मैं एक ही आकार की अधिक पंक्तियों को जोड़ता हूं, तो प्रत्येक पंक्ति को पढ़ने में उतना ही समय लगता है जैसे कि मेरे पास केवल एक पंक्ति है, इसलिए यह स्पष्ट है कि डेटा हैअभी भी पंक्ति द्वारा पंक्तिबद्ध किया जा रहा है - एक बार जब आप किसी भी अतुल्यकालिक पाठ को शुरू करते हैं तो यह पूरी पंक्ति को पढ़ना पसंद करते हैं। अगर मैं पहली पंक्ति को एसिंक्रोनस रूप से पढ़ता हूं, और दूसरी तुल्यकालिक रूप से - दूसरी पंक्ति को पढ़ा जा रहा है फिर से तेजी से होगा।

तो हम देख सकते हैं कि समस्या एक व्यक्तिगत पंक्ति और / या स्तंभ का एक बड़ा आकार है। इससे कोई फ़र्क नहीं पड़ता कि आपके पास कुल कितना डेटा है - एक लाख छोटी पंक्तियों को असिंक्रोनस रूप से पढ़ना उतना ही तेज़ है जितना कि सिंक्रोनाइज़ करना। लेकिन एक एकल फ़ील्ड जोड़ें जो एक पैकेट में फिट होने के लिए बहुत बड़ा है, और आप रहस्यमय तरीके से उस डेटा को अतुल्यकालिक रूप से पढ़ने पर एक लागत खर्च करते हैं - जैसे कि प्रत्येक पैकेट को एक अलग अनुरोध पैकेट की आवश्यकता होती है, और सर्वर सिर्फ सभी डेटा नहीं भेज सकता है एक बार। प्रयोग 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


3
बहुत बढ़िया जवाब। ओपी के परिदृश्य को पुन: प्रस्तुत किया। इसके लिए 1.5 मीटर स्ट्रिंग ओपी का उल्लेख किया गया है, मुझे सिंक के लिए 130ms बनाम 2200 के लिए async मिलता है। आपके दृष्टिकोण के साथ, 1.5 मीटर स्ट्रिंग के लिए मापा समय 60ms है, बुरा नहीं है।
विकटोर ज़िकला

4
वहाँ की अच्छी जाँच, प्लस मैंने हमारे DAL कोड के लिए कुछ अन्य ट्यूनिंग तकनीकों को सीखा।
एडम हल्ड्सवर्थ

बस कार्यालय में वापस आ गया और ExecuteScalarAsync के बजाय मेरे उदाहरण पर कोड की कोशिश की, लेकिन मुझे अभी भी
सेकंड के

6
अहा, यह सब के बाद काम किया :) लेकिन मुझे इस लाइन में CommandBehavior.SequentialAccess को जोड़ना होगा: using (var reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess))
hcd

@ एलसीडी मेरा बुरा है, मैं इसे पाठ में था, लेकिन नमूना कोड में नहीं :)
Luaan
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.