c# - هل async HttpClient من .Net 4.5 خيارًا سيئًا لتطبيقات التحميل المكثف؟





asynchronous .net-4.5 async-await dotnet-httpclient (4)


إلى جانب الاختبارات المذكورة في السؤال ، قمت مؤخرًا بتأسيس بعضًا جديدًا يتضمن عددًا أقل بكثير من مكالمات HTTP (5000 مقارنة بـ 1 مليون سابق) ولكن على الطلبات التي استغرقت وقتًا أطول لتنفيذها (500 مللي ثانية مقارنة بحوالي 1 ملي ثانية سابقًا). أنتجت كل من التطبيقات المختبرة ، واحد multithreaded بشكل متزامن (على أساس HttpWebRequest) و I / O غير متزامن (استنادا إلى عميل HTTP) نتائج مماثلة: حوالي 10 ثانية لتنفيذ استخدام حوالي 3 ٪ من وحدة المعالجة المركزية و 30 MB من الذاكرة. كان الاختلاف الوحيد بين المختبرين هو أن السلسلة ذات مؤشرات ترابط متعددة استخدمت 310 سلاسل لتنفيذها ، في حين أن واحدة غير متزامنة فقط 22. لذلك في التطبيق الذي قد يجمع بين كل من عمليات الإدخال والإدخال وملحقة وحدة المعالجة المركزية ، فإن الإصدار غير المتزامن كان سيؤدي إلى نتائج أفضل لأنه كان من الممكن أن يتوفر وقت وحدة المعالجة المركزية (CPU) أكثر للخيوط التي تقوم بعمليات وحدة المعالجة المركزية (CPU) ، وهي تلك التي تحتاج إليها فعليًا (مؤشرات الترابط التي تنتظر عمليات الإدخال / الإخراج لإكمالها هي مجرد إهدار).

نتيجة لاستنتاجاتي ، فإن مكالمات HTTP غير المتزامنة ليست الخيار الأفضل عند التعامل مع الطلبات السريعة للغاية. السبب وراء ذلك هو أنه عند تشغيل مهمة تحتوي على استدعاء I / O غير متزامن ، يتم إنهاء مؤشر الترابط الذي يتم بدء المهمة عليه بمجرد إجراء الاستدعاء غير المتزامن ويتم تسجيل بقية المهمة على أنها رد اتصال. ثم ، عند اكتمال عملية الإدخال / الإخراج ، يتم وضع قائمة الانتظار في قائمة الانتظار للتنفيذ على أول مؤشر ترابط متوفرة. كل هذا يخلق حملًا ، مما يجعل عمليات الإدخال / الإخراج السريعة أكثر كفاءة عند تنفيذها على مؤشر الترابط الذي بدأها.

تعتبر مكالمات HTTP غير المتزامنة خيارًا جيدًا عند التعامل مع عمليات إدخال / إخراج طويلة أو طويلة احتمالًا لأنها لا تحافظ على أي سلاسل عمليات مشغولة في انتظار اكتمال عمليات الإدخال / الإخراج. يؤدي ذلك إلى تقليل العدد الإجمالي لمؤشرات الترابط التي يستخدمها أحد التطبيقات مما يسمح بإنفاق المزيد من وقت وحدة المعالجة المركزية (CPU) بواسطة عمليات وحدة المعالجة المركزية (CPU). علاوة على ذلك ، في التطبيقات التي تقوم بتخصيص عدد محدود فقط من مؤشرات الترابط (كما هو الحال مع تطبيقات الويب) ، يمنع الإدخال / الإخراج غير المتزامن استنفاد ترابط تجمّع مؤشر الترابط ، والذي يمكن أن يحدث في حالة إجراء مكالمات I / O بشكل متزامن.

لذلك ، لا يعد HtpClient async اختناقًا لتطبيقات التحميل المكثف. إنها ليست ملائمة بطبيعتها لطلبات HTTP السريعة ، بل إنها مثالية للطلبات الطويلة أو الطويلة ، خاصة داخل التطبيقات التي لا يتوفر فيها سوى عدد محدود من المواضيع. كما أنه من الممارسات الجيدة الحد من التزامن عبر ServicePointManager.DefaultConnectionLimit مع قيمة عالية بما يكفي لضمان مستوى جيد من التوازي ، ولكنها منخفضة بدرجة كافية لمنع استنفاد المنافذ المؤقتة. يمكنك العثور على مزيد من التفاصيل حول الاختبارات والاستنتاجات المقدمة لهذا السؤال here .

قمت مؤخرًا بإنشاء تطبيق بسيط لاختبار إخراج استدعاء HTTP التي يمكن إنشاؤها بطريقة غير متزامنة مقابل نهج متعدد مؤشرات ترابط الكلاسيكية.

التطبيق قادر على تنفيذ عدد محدد مسبقا من مكالمات HTTP وفي النهاية يعرض إجمالي الوقت اللازم لأداءهم. خلال اختباراتي ، تم إجراء جميع مكالمات HTTP إلى جهاز IIS المحلي ، واستعادت ملفًا نصيًا صغيرًا (12 بايتًا في الحجم).

يتم سرد الجزء الأكثر أهمية من رمز للتنفيذ غير المتزامن أدناه:

public async void TestAsync()
{
    this.TestInit();
    HttpClient httpClient = new HttpClient();

    for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
    {
        ProcessUrlAsync(httpClient);
    }
}

private async void ProcessUrlAsync(HttpClient httpClient)
{
    HttpResponseMessage httpResponse = null;

    try
    {
        Task<HttpResponseMessage> getTask = httpClient.GetAsync(URL);
        httpResponse = await getTask;

        Interlocked.Increment(ref _successfulCalls);
    }
    catch (Exception ex)
    {
        Interlocked.Increment(ref _failedCalls);
    }
    finally
    { 
        if(httpResponse != null) httpResponse.Dispose();
    }

    lock (_syncLock)
    {
        _itemsLeft--;
        if (_itemsLeft == 0)
        {
            _utcEndTime = DateTime.UtcNow;
            this.DisplayTestResults();
        }
    }
}

يتم سرد الجزء الأكثر أهمية في تطبيق تعدد مؤشرات الترابط أدناه:

public void TestParallel2()
{
    this.TestInit();
    ServicePointManager.DefaultConnectionLimit = 100;

    for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
    {
        Task.Run(() =>
        {
            try
            {
                this.PerformWebRequestGet();
                Interlocked.Increment(ref _successfulCalls);
            }
            catch (Exception ex)
            {
                Interlocked.Increment(ref _failedCalls);
            }

            lock (_syncLock)
            {
                _itemsLeft--;
                if (_itemsLeft == 0)
                {
                    _utcEndTime = DateTime.UtcNow;
                    this.DisplayTestResults();
                }
            }
        });
    }
}

private void PerformWebRequestGet()
{ 
    HttpWebRequest request = null;
    HttpWebResponse response = null;

    try
    {
        request = (HttpWebRequest)WebRequest.Create(URL);
        request.Method = "GET";
        request.KeepAlive = true;
        response = (HttpWebResponse)request.GetResponse();
    }
    finally
    {
        if (response != null) response.Close();
    }
}

أظهر تشغيل الاختبارات أن الإصدار ذات مؤشرات ترابط متعددة أسرع. استغرق الأمر حوالي 0.6 ثانية لإكمال طلبات 10k ، في حين أن واحد المتزامن استغرق حوالي 2 ثانية لإكمال نفس الكمية من الحمل. كان هذا مفاجئًا بعض الشيء ، لأنني كنت أتوقع أن يكون المتزامن أسرع. ربما كان ذلك بسبب حقيقة أن مكالماتي HTTP كانت سريعة جدا. في سيناريو العالم الحقيقي ، حيث يجب أن يقوم الخادم بإجراء عملية أكثر وضوحا وحيث يجب أن يكون هناك أيضًا بعض وقت استجابة الشبكة ، قد يتم عكس النتائج.

ومع ذلك ، ما يهمني حقاً هو الطريقة التي يتصرف بها HttpClient عند زيادة الحمل. نظرًا لأنه يستغرق حوالي 2 ثانية لتسليم 10 آلاف رسالة ، اعتقدت أن الأمر سيستغرق حوالي 20 ثانية لتسليم 10 أضعاف عدد الرسائل ، ولكن تشغيل الاختبار أظهر أنه يحتاج إلى حوالي 50 ثانية لتسليم الرسائل 100 كيلو. علاوة على ذلك ، فإنه عادةً ما يستغرق أكثر من دقيقتين لتسليم رسائل 200 كيلو بايت وكثيرًا ما تفشل عدة آلاف منها (3-4 كيلو بايت) في الاستثناء التالي:

تعذر إجراء عملية على مأخذ توصيل نظرًا لأن النظام افتقر إلى مساحة تخزين كافية أو لأن قائمة الانتظار كانت ممتلئة.

راجعت سجلات IIS والعمليات التي فشلت أبدا وصل إلى الخادم. فشلوا داخل العميل. أجريت الاختبارات على جهاز يعمل بنظام التشغيل Windows 7 مع النطاق الافتراضي للمنافذ المؤقتة من 49152 إلى 65535. وأظهر تشغيل netstat أنه تم استخدام حوالي 5-6 آلاف منفذًا أثناء الاختبارات ، لذا كان ينبغي توفير الكثير من الناحية النظرية. إذا كان عدم وجود منافذ هو بالفعل سبب الاستثناءات ، فهذا يعني أن netstat لم يبلغ بشكل صحيح عن الموقف أو HttClient يستخدم فقط الحد الأقصى لعدد المنافذ التي يبدأ بعدها في رمي الاستثناءات.

على النقيض من ذلك ، تصرف نهج multithread لتوليد المكالمات HTTP متوقعة للغاية. أخذت حوالي 0.6 ثانية لرسائل 10k ، حوالي 5.5 ثانية لرسائل 100k وكما هو متوقع حوالي 55 ثانية لمليون رسالة. فشل أي من الرسائل. وأكثر من ذلك ، في حين أنها تعمل ، لم تستخدم أكثر من 55 ميغابايت من ذاكرة الوصول العشوائي (وفقاً لإدارة مهام Windows). زادت الذاكرة المستخدمة عند إرسال الرسائل بشكل غير متناسب مع الحمل. استخدم حوالي 500 ميغابايت من ذاكرة الوصول العشوائي أثناء اختبارات رسائل 200 كيلو.

أعتقد أن هناك سببين رئيسيين للنتائج المذكورة أعلاه. أول واحد هو أن HttpClient يبدو أنه شديد الجشع في خلق اتصالات جديدة مع الخادم. يشير العدد الكبير للمنافذ المستخدمة التي تم الإعلام عنها بواسطة netstat إلى أنه من المحتمل أنه لا يستفيد كثيرًا من HTTP.

والثاني هو أن HttpClient لا يبدو أن لديها آلية اختناق. في الواقع ، يبدو أن هذه مشكلة عامة تتعلق بعمليات المتزامن. إذا كنت بحاجة إلى تنفيذ عدد كبير جدًا من العمليات ، فسيتم البدء في تشغيلها مرة واحدة ، ثم يتم تنفيذ عمليات المتابعة عند توفرها. من الناحية النظرية ، يجب أن يكون الأمر على ما يرام ، لأن الحمل في العمليات غير المتزامنة يكون على أنظمة خارجية ، ولكن كما ثبت أعلاه ، ليس الأمر كذلك. سيؤدي الحصول على عدد كبير من الطلبات في وقت واحد إلى زيادة استخدام الذاكرة وإبطاء التنفيذ بالكامل.

تمكنت من الحصول على نتائج أفضل ، والذاكرة ووقت التنفيذ ، من خلال الحد من الحد الأقصى لعدد الطلبات غير المتزامنة مع آلية تأخير بسيطة ولكنها بدائية:

public async void TestAsyncWithDelay()
{
    this.TestInit();
    HttpClient httpClient = new HttpClient();

    for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
    {
        if (_activeRequestsCount >= MAX_CONCURENT_REQUESTS)
            await Task.Delay(DELAY_TIME);

        ProcessUrlAsyncWithReqCount(httpClient);
    }
}

سيكون من المفيد حقًا إذا تضمن HttpClient آلية للحد من عدد الطلبات المتزامنة. عند استخدام فئة المهام (التي تستند إلى تجمع مؤشرات الترابط .Net) يتحقق تلقائيا التحكم عن طريق الحد من عدد من المواضيع المتزامنة.

للحصول على نظرة عامة كاملة ، قمت أيضًا بإنشاء إصدار من اختبار التزامن استنادًا إلى HttpWebRequest بدلاً من HttpClient وتمكنت من الحصول على نتائج أفضل كثيرًا. كبداية ، يسمح بتحديد حد لعدد الاتصالات المتزامنة (مع ServicePointManager.DefaultConnectionLimit أو عبر التهيئة) ، مما يعني أنه لم يتم تشغيل المنافذ مطلقًا وفشلها أبداً في أي طلب (يعتمد HttpClient ، بشكل افتراضي ، على HttpWebRequest ، ولكن يبدو أن تجاهل الإعداد حد الاتصال).

كان النهج المتزامن HttpWebRequest لا يزال حوالي 50 - 60 ٪ أبطأ من multithreading واحد ، لكنه كان متوقعا وموثوقا به. وكان الجانب السلبي الوحيد لذلك هو أنها استخدمت كمية هائلة من الذاكرة تحت حمولة كبيرة. على سبيل المثال ، تحتاج إلى حوالي 1.6 جيجابايت لإرسال مليون طلب. من خلال الحد من عدد الطلبات المتزامنة (مثلما فعلت أعلاه بالنسبة إلى HttpClient) ، تمكنت من تقليل الذاكرة المستخدمة إلى 20 ميغابايت فقط والحصول على وقت تنفيذ أقل بنسبة 10٪ أبطأ من أسلوب تعدد المؤشرات.

بعد هذا العرض المطول ، أسئلتي هي: هل الطبقة HttpClient من .Net 4.5 خيارًا سيئًا لتطبيقات التحميل المكثفة؟ هل هناك أي طريقة لخنقها ، والتي ينبغي أن تحل المشاكل التي ذكرتها؟ ماذا عن النكهة غير المتزامنة من HttpWebRequest؟

تحديث (شكرا @ ستيفن كليري)

كما اتضح ، HttpClient ، تماما مثل HttpWebRequest (الذي يستند إليه افتراضيا) ، يمكن أن يكون عدد الاتصالات المتزامنة على نفس المضيف محدودة مع ServicePointManager.DefaultConnectionLimit. الشيء الغريب هو أنه وفقا ل MSDN ، القيمة الافتراضية لحد الاتصال هو 2. أنا أيضا التحقق من ذلك على جانبي باستخدام المصحح الذي أشار إلى أن 2 في الواقع هو القيمة الافتراضية. ومع ذلك ، يبدو أنه ما لم يتم تعيين قيمة بشكل صريح إلى ServicePointManager.DefaultConnectionLimit ، فسيتم تجاهل القيمة الافتراضية. نظرًا لأنني لم أضع قيمة صريحة لذلك أثناء اختبارات HttpClient ، ظننت أنه تم تجاهلها.

بعد تعيين ServicePointManager.DefaultConnectionLimit إلى 100 HttpClient أصبحت موثوقة ويمكن التنبؤ بها (Netstat يؤكد أنه تم استخدام 100 منفذ فقط). ما زال أبطأ من async HttpWebRequest (حوالي 40٪) ، ولكن بشكل غريب ، فإنه يستخدم ذاكرة أقل. بالنسبة للاختبار الذي يتضمن 1 مليون طلب ، استخدم الحد الأقصى من 550 ميغابايت ، مقارنة بـ 1.6 غيغابايت في المتزامن HttpWebRequest.

لذلك ، في حين يبدو HttpClient في تركيبة ServicePointManager.DefaultConnectionLimit لضمان الموثوقية (على الأقل بالنسبة للسيناريو الذي يتم فيه إجراء جميع المكالمات نحو نفس المضيف) ، فإنه لا يزال يبدو أن أداءه يتأثر سلبًا بسبب عدم وجود آلية اختناق مناسبة. شيء من شأنه أن يحد من عدد الطلبات المتزامن إلى قيمة قابلة للتكوين ووضع الباقي في قائمة انتظار سيجعله أكثر ملاءمة لسيناريوهات قابلية عالية.




في حين أن هذا لا يجيب بشكل مباشر على الجزء "المتزامن" من سؤال البروتوكول الاختياري ، فإن ذلك يعالج خطأ في التطبيق الذي يستخدمه.

إذا كنت تريد توسيع نطاق تطبيقك ، فتجنب استخدام HttpClients المستند إلى مثيل. الفرق هو ضخم! اعتمادا على الحمل ، سترى أرقام أداء مختلفة للغاية. تم تصميم HttpClient ليتم إعادة استخدامه عبر الطلبات. تم تأكيد ذلك من قبل اللاعبين في فريق BCL الذي كتبه.

كان أحد المشروعات التي أجريتها مؤخرًا هو مساعدة مقياس كبير جدًا ومعروف على الإنترنت لمتاجر التجزئة عبر الإنترنت على حركة مرور الجمعة / العطل السوداء لبعض الأنظمة الجديدة. واجهنا بعض مشكلات الأداء حول استخدام HttpClient. منذ أن نفذت IDisposable ، فإن devs فعل ما كنت تفعل عادة عن طريق إنشاء مثيل ووضعها داخل بيان using() . وبمجرد أن بدأنا اختبار التحميل ، أحضر التطبيق الخادم إلى ركبتيه - نعم ، الخادم ليس التطبيق فقط. السبب هو أن كل مثيل من HttpClient يفتح منفذ إتمام I / O على الخادم. بسبب وضع اللمسات النهائية غير المحددة لـ GC وحقيقة أنك تعمل مع موارد الكمبيوتر التي تمتد عبر طبقات OSI متعددة ، يمكن أن يستغرق إغلاق منافذ الشبكة بعض الوقت. في الواقع ، يمكن لنظام Windows OS نفسه أن يستغرق ما يصل إلى 20 ثانية لإغلاق منفذ (لكل Microsoft). كنا نفتح الموانئ بشكل أسرع مما كان يمكن أن يكون مغلقًا - استنفاد منفذ الخادم الذي ضرب وحدة المعالجة المركزية إلى 100٪. كان الإصلاح الخاص بي لتغيير HttpClient إلى مثيل ثابت حل المشكلة. نعم ، إنه مورد يمكن التخلص منه ، ولكن أي زيادة في النفقات العامة تفوق إلى حد كبير الفرق في الأداء. أشجعك على إجراء بعض اختبارات التحميل لمعرفة كيفية تصرف تطبيقك.

أجاب أيضا على الرابط أدناه:

ما مقدار الحمل لإنشاء HttpClient جديد لكل مكالمة في عميل WebAPI؟

https://www.asp.net/web-api/overview/advanced/calling-a-web-api-from-a-net-client




هناك شيء واحد يجب مراعاته والذي قد يؤثر على نتائجك هو أنه مع HttpWebRequest لا تحصل على ResponseStream وتستهلك ذلك الدفق. باستخدام HttpClient ، سيقوم بشكلٍ افتراضي بنسخ تدفق الشبكة إلى دفق ذاكرة. لاستخدام HttpClient بالطريقة نفسها التي تستخدم بها حاليًا HttpWebRquest ، ستحتاج إلى إجراء

var requestMessage = new HttpRequestMessage() {RequestUri = URL};
Task<HttpResponseMessage> getTask = httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead);

والشيء الآخر هو أنني لست متأكدا حقا ما الفرق الحقيقي ، من منظور خيوط ، فأنت تختبر في الواقع. إذا قمت بحفر في أعماق HttpClientHandler فإنه ببساطة Task.Factory.StartNew من أجل تنفيذ طلب متزامن. يتم تفويض سلوك مؤشر الترابط إلى سياق التزامن بالطريقة نفسها تمامًا كما هو المثال الخاص بك مع مثال HttpWebRequest.

بلا شك ، HttpClient إضافة بعض الحمل كما افتراضياً يستخدم HttpWebRequest كما في مكتبة النقل الخاصة به. لذلك سوف تتمكن دوماً من الحصول على أفضل أداء مع HttpWebRequest مباشرة أثناء استخدام HttpClientHandler. الفوائد التي تجلبها HttpClient هي مع الفئات القياسية مثل HttpResponseMessage و HttpRequestMessage و HttpContent وكافة الرؤوس التي تمت كتابتها بقوة. في حد ذاته ليس الأمثل الأمثل.




ما هو الهاتف المحمول الذي سيستخدم للاتصال الحقيقي؟ هل يحدث ذلك عبر HTTP / HTTPS إلى نفس موقع الويب؟ إذا كان الأمر كذلك ، فهذه هي الطريقة المثلى التي ينبغي اتباعها - الخطوة التالية هي معرفة سبب طول هذا الوقت.

هل اتصال الشبكة موجود بالتأكيد قبل أن تتمكن من الطلب؟ هل أنت بالتأكيد لا تقدم أي طلبات مسبقا؟ أسأل لأنني لاحظت أنك لا تتخلص من الاستجابة - إذا حدث ذلك في مكان آخر أيضًا ، فقد تجد أنك تمشي ضد تجمع الاتصال فقط مما يمنحك بضع اتصالات إلى خادم معين. والأخلاقية هي أن تضع دائما s HttpWebResponse في بيان using . بالطبع ، قد لا تكون هذه هي المشكلة في قضيتك ، ولكنها تستحق المحاولة.

إذا كان التطبيق الفعلي الخاص بك سينتقل إلى مكان آخر ، فهذا هو المكان الذي يجب أن يتحقق فيه الاختبار أيضًا. بشكل أساسي ، اجعلها قريبة من الشيء الحقيقي قدر الإمكان.

بغض النظر عن الاهتمام ، أنت تقول أنه تطبيق "softphone" - هل يتم تشغيله بالفعل على هاتف يحتوي على بعض الوصف ، باستخدام الإطار المضغوط ، أم أنه تطبيق سطح مكتب؟







c# asynchronous .net-4.5 async-await dotnet-httpclient