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



Answers

هناك شيء واحد يجب مراعاته والذي قد يؤثر على نتائجك هو أنه مع 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 وكافة الرؤوس التي تمت كتابتها بقوة. في حد ذاته ليس الأمثل الأمثل.

Question

قمت مؤخرًا بإنشاء تطبيق بسيط لاختبار إخراج استدعاء 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 لضمان الموثوقية (على الأقل بالنسبة للسيناريو الذي يتم فيه إجراء جميع المكالمات نحو نفس المضيف) ، فإنه لا يزال يبدو أن أداءه يتأثر سلبًا بسبب عدم وجود آلية اختناق مناسبة. شيء من شأنه أن يحد من عدد الطلبات المتزامن إلى قيمة قابلة للتكوين ووضع الباقي في قائمة انتظار سيجعله أكثر ملاءمة لسيناريوهات قابلية عالية.






Links