हमने AspNetCore.Mvc की एक महत्वपूर्ण भेद्यता को कैसे पाया और अपने स्वयं के क्रमांकन पर स्विच किया

नमस्कार, हेब्र!

इस लेख में, हम प्रदर्शन को अनुकूलित करने और AspNetCore.Mvc की सुविधाओं की खोज में अपने अनुभव को साझा करना चाहते हैं।



प्रागितिहास


कई साल पहले, हमारी एक भरी हुई सेवा पर, हमने सीपीयू संसाधनों की महत्वपूर्ण खपत पर ध्यान दिया। यह अजीब लग रहा था, क्योंकि सेवा का कार्य वास्तव में संदेश लेना था और इसे कतार में खड़ा करना था, पहले इस पर कुछ ऑपरेशन किए थे, जैसे कि सत्यापन, डेटा जोड़, आदि।

प्रोफाइलिंग के परिणामस्वरूप, हमने पाया कि प्रोसेसर के अधिकांश समय में डीरिएरलाइजेशन "खाती है"। हमने मानक धारावाहिक को बाहर फेंक दिया और जिल पर अपना स्वयं का लिखा, जिसके परिणामस्वरूप संसाधन की खपत कई गुना कम हो गई। सब कुछ के रूप में काम करना चाहिए और हम इसके बारे में भूल करने में कामयाब रहे।

समस्या


हम सुरक्षा, प्रदर्शन और गलती सहिष्णुता सहित सभी क्षेत्रों में अपनी सेवा में लगातार सुधार कर रहे हैं, इसलिए "सुरक्षा दल" हमारी सेवाओं के विभिन्न परीक्षण करते हैं। और कुछ समय पहले लॉग में एक त्रुटि के बारे में एक चेतावनी "मक्खियों" के लिए हमें - किसी भी तरह हम आगे एक अमान्य संदेश याद किया।

एक विस्तृत विश्लेषण के साथ, सब कुछ अजीब लग रहा था। एक अनुरोध-मॉडल है (यहां मैं एक सरलीकृत कोड उदाहरण दूंगा):

public class RequestModel { public string Key { get; set; } FromBody] Required] public PostRequestModelBody Body { get; set; } } public class PostRequestModelBody { [Required] [MinLength(1)] public IEnumerable<long> ItemIds { get; set; } } 

उदाहरण के लिए, एक्शन पोस्ट के साथ एक नियंत्रक है:

 [Route("api/[controller]")] public class HomeController : Controller { [HttpPost] public async Task<ActionResult> Post(RequestModel request) { if (this.ModelState.IsValid) { return this.Ok(); } return this.BadRequest(); } } 

सब कुछ तार्किक लगता है। अगर बॉडी व्यू से रिक्वेस्ट आती है

 {"itemIds":["","","" … ] } 

एपीआई BadRequest लौटाएगा, और इसके लिए परीक्षण हैं।

फिर भी, लॉग में हम विपरीत देखते हैं। हमने लॉग से संदेश लिया, इसे एपीआई को भेजा और स्थिति ठीक पाई ... और ... लॉग में एक नई त्रुटि। हमारी आँखों पर विश्वास न करते हुए, हमने एक गलती की और सुनिश्चित किया कि हाँ, वास्तव में ModelState.IsValid == सच है। उसी समय, उन्होंने लगभग 500ms की असामान्य रूप से लंबी क्वेरी निष्पादन समय पर ध्यान दिया, जबकि सामान्य प्रतिक्रिया समय शायद ही कभी 50ms से अधिक होता है और यह उत्पादन में होता है, जो प्रति सेकंड हजारों अनुरोध करता है!

इस अनुरोध और हमारे परीक्षणों के बीच का अंतर केवल इतना था कि अनुरोध में 600+ खाली लाइनें थीं ...

आगे बहुत सारे bukaf कोड होंगे।

कारण


वे समझने लगे कि क्या गलत था। त्रुटि को खत्म करने के लिए, उन्होंने एक साफ आवेदन लिखा था (जिसमें से मैंने एक उदाहरण दिया था), जिसके साथ हमने वर्णित स्थिति को पुन: पेश किया। कुल मिलाकर, हमने अनुसंधान, परीक्षण, मानसिक डिबग कोड AspNetCore.Mvc पर मानव-दिन की एक जोड़ी खर्च की और यह पता चला कि समस्या JsonInputFormatter में है

JsonInputFormatter एक JsonSerializer का उपयोग करता है, जो कि deserialization और प्रकार के लिए एक धारा प्राप्त कर रहा है, यदि यह एक सरणी है - इस सरणी के प्रत्येक तत्व - प्रत्येक संपत्ति को क्रमबद्ध करने का प्रयास करता है। उसी समय, यदि क्रमांकन के दौरान कोई त्रुटि होती है, तो JsonInputFormatter प्रत्येक त्रुटि और उसके पथ को बचाता है, इसे संसाधित के रूप में चिह्नित करता है, ताकि आप आगे deserialize करने का प्रयास जारी रख सकें।

नीचे JsonInputFormatter त्रुटि हैंडलर के लिए कोड है (यह ऊपर दिए गए लिंक पर Github पर उपलब्ध है):

 void ErrorHandler(object sender, Newtonsoft.Json.Serialization.ErrorEventArgs eventArgs) { successful = false; // When ErrorContext.Path does not include ErrorContext.Member, add Member to form full path. var path = eventArgs.ErrorContext.Path; var member = eventArgs.ErrorContext.Member?.ToString(); var addMember = !string.IsNullOrEmpty(member); if (addMember) { // Path.Member case (path.Length < member.Length) needs no further checks. if (path.Length == member.Length) { // Add Member in Path.Memb case but not for Path.Path. addMember = !string.Equals(path, member, StringComparison.Ordinal); } else if (path.Length > member.Length) { // Finally, check whether Path already ends with Member. if (member[0] == '[') { addMember = !path.EndsWith(member, StringComparison.Ordinal); } else { addMember = !path.EndsWith("." + member, StringComparison.Ordinal); } } } if (addMember) { path = ModelNames.CreatePropertyModelName(path, member); } // Handle path combinations such as ""+"Property", "Parent"+"Property", or "Parent"+"[12]". var key = ModelNames.CreatePropertyModelName(context.ModelName, path); exception = eventArgs.ErrorContext.Error; var metadata = GetPathMetadata(context.Metadata, path); var modelStateException = WrapExceptionForModelState(exception); context.ModelState.TryAddModelError(key, modelStateException, metadata); _logger.JsonInputException(exception); // Error must always be marked as handled // Failure to do so can cause the exception to be rethrown at every recursive level and // overflow the stack for x64 CLR processes eventArgs.ErrorContext.Handled = true; } 

प्रोसीजर के रूप में मार्किंग एरर प्रोसेसर के बिल्कुल अंत में बनाई गई है

 eventArgs.ErrorContext.Handled = true; 


इस प्रकार, एक विशेषता सभी deserialization त्रुटियों और उन्हें पथ के लिए कार्यान्वित की जाती है, जिस पर वे फ़ील्ड / तत्व थे, अच्छी तरह से "सभी ...

तथ्य यह है कि JsonSerializer में 200 त्रुटियों की सीमा है, जिसके बाद यह क्रैश हो जाता है, जबकि सभी कोड क्रैश हो जाते हैं और ModelState हो जाता है ... मान्य! ... त्रुटियां भी खो जाती हैं।

निर्णय


बिना किसी हिचकिचाहट के, हमने Jil Deserializer का उपयोग करके Asp.Net Core के लिए अपना Json फॉर्मैटर लागू किया। चूंकि शरीर में त्रुटियों की संख्या हमारे लिए बिल्कुल महत्वपूर्ण नहीं है, केवल उनकी उपस्थिति का तथ्य महत्वपूर्ण है (और आमतौर पर ऐसी स्थिति की कल्पना करना मुश्किल है जब यह वास्तव में उपयोगी होगा), सीरियल कोड काफी सरल निकला।

मैं कस्टम JilJsonInputFormatter का मुख्य कोड दूंगा:

 using (var reader = context.ReaderFactory(request.Body, encoding)) { try { var result = JSON.Deserialize( reader: reader, type: context.ModelType, options: this.jilOptions); if (result == null && !context.TreatEmptyInputAsDefaultValue) { return await InputFormatterResult.NoValueAsync(); } else { return await InputFormatterResult.SuccessAsync(result); } } catch { // -   } return await InputFormatterResult.FailureAsync(); } 

चेतावनी! जिल मामला संवेदनशील है, जिसका अर्थ है शरीर की सामग्री

 {"ItemIds":["","","" … ] } 

और

 {"itemIds":["","","" … ] } 

एक ही बात नहीं है। पहले मामले में, अगर कैमलकेस का उपयोग किया जाता है, तो आइटमआईड्स संपत्ति डीरिएरलाइजेशन के बाद शून्य हो जाएगी।

लेकिन चूंकि यह हमारी एपीआई है, हम इसका उपयोग करते हैं और इसे नियंत्रित करते हैं, हमारे लिए यह महत्वपूर्ण नहीं है। समस्या यह हो सकती है यदि यह एक सार्वजनिक एपीआई है और कोई पहले से ही इसे कॉल करता है, जो कैमलकेस में पैरामीटर नाम नहीं है।

परिणाम


परिणाम हमारी अपेक्षाओं से अधिक हो गया, एपीआई ने अपेक्षित अनुरोध के लिए बुरी तरह से वापसी करना शुरू कर दिया और इसे बहुत जल्दी किया। नीचे हमारे कुछ रेखांकन के स्क्रीनशॉट हैं, जो स्पष्ट रूप से प्रतिक्रिया समय और सीपीयू में बदलाव दिखाते हैं, तैनाती से पहले और बाद में।
अनुरोध का नेतृत्व समय:

छवि

लगभग 16:00 बजे एक तैनाती थी। तैनाती से पहले, p99 निष्पादन समय 30-57ms था, तैनाती के बाद यह 9-15ms हो गया। (आप 18:00 की दोहराया चोटियों पर ध्यान नहीं दे सकते - यह एक और तैनाती थी)

इस तरह सीपीयू ग्राफ बदल गया है:

छवि

इस कारण से, हम गितुब के लिए मुद्दा लाए, लेखन के समय, इसे माइलस्टोन 3.0.0-प्रीव्यू 3 के साथ एक बग के रूप में चिह्नित किया गया था।

निष्कर्ष में


जब तक समस्या का समाधान नहीं हो जाता है, तब तक हम मानते हैं कि मानक डिसेरिएलाइज़ेशन के उपयोग को छोड़ना बेहतर है, खासकर यदि आपके पास सार्वजनिक एपीआई है। इस समस्या को जानने के बाद, एक हमलावर आसानी से आपकी सेवा में लगा सकता है, उसी तरह के अमान्य अनुरोधों का एक गुच्छा फेंक सकता है, क्योंकि त्रुटिपूर्ण सरणी जितनी अधिक होगी, उतना ही अधिक बॉडी, जितनी देर तक प्रसंस्करण JsonInputFormatter में होता है।

अर्टिओम एस्टास्किन, विकास टीम लीडर

Source: https://habr.com/ru/post/hi435626/


All Articles