Быстрая и эффективная обработка HTTP-запросов в нескольких потоках в .NET - PullRequest
3 голосов
/ 21 марта 2012

Я работаю с .NET с момента его создания и занимаюсь параллельным программированием задолго до этого ... тем не менее, я затрудняюсь объяснить этот феномен.Этот код работает в производственной системе и выполняет свою работу по большей части, просто ищет лучшего понимания.

Я передаю 10 URL для одновременной обработки в следующее:

    public static void ProcessInParellel(IEnumerable<ArchivedStatus> statuses, 
                                         StatusRepository statusRepository, 
                                         WaitCallback callback, 
                                         TimeSpan timeout)
    {
        List<ManualResetEventSlim> manualEvents = new List<ManualResetEventSlim>(statuses.Count());

        try
        {
            foreach (ArchivedStatus status in statuses)
            {
                manualEvents.Add(new ManualResetEventSlim(false));
                ThreadPool.QueueUserWorkItem(callback,
                                             new State(status, manualEvents[manualEvents.Count - 1], statusRepository));
            }

            if (!(WaitHandle.WaitAll((from m in manualEvents select m.WaitHandle).ToArray(), timeout, false))) 
                throw ThreadPoolTimeoutException(timeout);
        }
        finally
        {
            Dispose(manualEvents);
        }
    }

Обратный вызов выглядит примерно так:

    public static void ProcessEntry(object state)
    {
        State stateInfo = state as State;

        try
        {
            using (new LogTimer(new TimeSpan(0, 0, 6)))
            {
               GetFinalDestinationForUrl(<someUrl>);
            }
        }
        catch (System.IO.IOException) { }
        catch (Exception ex)
        {

        }
        finally
        {
            if (stateInfo.ManualEvent != null)
                stateInfo.ManualEvent.Set();
        }
    }

Каждый из обратных вызовов смотрит наURL-адрес и следует за серией перенаправлений (для обработки файлов cookie AllowAutoRedirect намеренно установлено значение false):

    public static string GetFinalDestinationForUrl(string url, string cookie)
    {
        if (!urlsToIgnore.IsMatch(url))
        {
            HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
            request.AllowAutoRedirect = false;
            request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
            request.Method = "GET";
            request.KeepAlive = false;
            request.Pipelined = false;
            request.Timeout = 5000;

            if (!string.IsNullOrEmpty(cookie))
                request.Headers.Add("cookie", cookie);

            try
            {
                string html = null, location = null, setCookie = null;

                using (WebResponse response = request.GetResponse())
                using (Stream stream = response.GetResponseStream())
                using (StreamReader reader = new StreamReader(stream))
                {
                    html = reader.ReadToEnd();
                    location = response.Headers["Location"];
                    setCookie = response.Headers[System.Net.HttpResponseHeader.SetCookie];
                }

                if (null != location)
                    return GetFinalDestinationForUrl(GetAbsoluteUrlFromLocationHeader(url, location),
                                                    (!string.IsNullOrEmpty(cookie) ? cookie + ";" : string.Empty) + setCookie);



                return CleanUrl(url);
            }
            catch (Exception ex)
            {
                if (AttemptRetry(ex, url))
                    throw;
            }
        }

        return ProcessedEntryFlag;
    }

У меня есть высокоточный StopWatch вокруг рекурсивного вызова GetFinalDestinationForUrl с порогом 6 секунд, и обычнозавершенные обратные вызовы делают это в течение этого времени.

Однако WaitAll со значительным таймаутом (0,0,60) для 10 потоков по-прежнему регулярно истекает.

Исключение выводит что-то вроде:

System.Exception: Не все потоки возвращаются за 60 секунд: Макс. Рабочий: 32767, Макс. Ввод / вывод: 1000, Доступный рабочий: 32764, ДоступенI / O: 1000 при Work.Threading.ProcessInParellel (состояния IEnumerable`1, StatusRepository statusRepository, обратный вызов WaitCallback, время ожидания TimeSpan) в Work.UrlExpanderWorker.SyncAllUsers ()

Это работает в .NET 4 с maxConnections , установленным на 100 для всех URL.

Моя единственная теория заключается в том, что это возможно для синхронногоВызов HttpWebRequest для блокировки дольше указанного времени ожидания?Это единственное разумное объяснение.Вопрос в том, почему и как лучше принудительно установить реальный тайм-аут для этой операции?

Да, я знаю, что рекурсивный вызов определяет время ожидания 5 с для каждого вызова, но для обработки может потребоваться несколько вызовов.данный URL.Но я почти не вижу предупреждений StopWatch.На каждые 20-30 ошибок тайм-аута WaitAll, которые я вижу, может появиться одно сообщение, указывающее, что данный поток занимал более 6 секунд.Если проблема действительно в том, что 10 потокам в совокупности требуется более 60 секунд, то я должен увидеть как минимум 1: 1 корреляцию (если не выше) между сообщениями.

ОБНОВЛЕНИЕ (30 марта,2012):

Я могу подтвердить, что одни сетевые вызовы не учитывают таймауты при определенных обстоятельствах:

            Uri uri = new Uri(url);
            HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(uri);
            request.AllowAutoRedirect = false;
            request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
            request.Method = "GET";
            request.KeepAlive = false;
            request.Pipelined = false;
            request.Timeout = 7000;
            request.CookieContainer = cookies;

            try
            {
                string html = null, location = null;

                using (new LogTimer("GetFinalDestinationForUrl", url, new TimeSpan(0, 0, 10)))
                    using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
                    using (Stream stream = response.GetResponseStream())
                    using (StreamReader reader = new StreamReader(stream))
                    {
                        html = reader.ReadToEnd();
                        location = response.Headers["Location"];
                        cookies = Combine(cookies, response.Cookies);

                        if (response.ContentLength > 150000 && !response.ContentType.ContainsIgnoreCase("text/html"))
                            log.Warn(string.Format("Large request ({0} bytes, {1}) detected at {2} on level {3}.", response.ContentLength, response.ContentType, url, level));
                    }

Этот код регулярно регистрирует записи, которые заняли 5-6 минут до завершения И были не больше 150000. И я не говорю об изолированном сервере здесь или там, это случайные (громкие) медиа-сайты.

Что именно здесь происходит иКак мы можем гарантировать, что код завершится в разумные сроки?

Ответы [ 2 ]

2 голосов
/ 21 марта 2012

Я согласен с Алиостад .Я не вижу никаких вопиющих проблем с кодом.Есть ли у вас какие-либо блокировки, которые вызывают сериализацию этих рабочих элементов?Я не вижу ничего на поверхности, но стоит проверить дважды, если ваш код более сложный, чем тот, который вы опубликовали.Вам нужно будет добавить код регистрации, чтобы фиксировать время начала этих HTTP-запросов.Надеюсь, это даст вам больше подсказок.

На неродственной ноте я обычно избегаю использования WaitHandle.WaitAll.Он имеет некоторые ограничения, такие как разрешение только 64 дескрипторов и отсутствие работы с потоком STA.Для чего стоит использовать этот шаблон вместо.

using (var finished = new CountdownEvent(1);
{
  foreach (var item in workitems)
  {
    var capture = item;
    finished.AddCount();
    ThreadPool.QueueUserWorkItem(
      () =>
      {
        try
        {
          ProcessWorkItem(capture);
        }
        finally
        {
          finished.Signal();
        }
      }
  }
  finished.Signal();
  if (!finished.Wait(timeout))
  {
    throw new ThreadPoolTimeoutException(timeout);
  }
}
1 голос
/ 21 марта 2012

Я полностью просмотрел ваш код.Насколько это возможно, и я вижу, я не вижу проблем.

Так что, похоже, есть еще одна проблема, но для обработки я предлагаю:

Написать трассировку,отладочный или консольный вывод в начале GetFinalDestinationForUrl и в конце, а также включение URL-адреса в трассировку.

Это должно помочь вам точно определить проблему.Это поможет вам, если HttpWebRequest не соблюдает ваш 5-секундный тайм-аут или .NET не соблюдает ваши 100 одновременных подключений.

Обновите ваш вопрос с результатом, и я рассмотрю снова.


ОБНОВЛЕНИЕ

Я рассмотрел ваши новые улучшения.Хорошо сделано для изоляции проблемы: теперь подтверждено, что WaitAll не уважает ваш тайм-аут.

Похоже, что это Проблема поддержки Microsoft , стоит поднять ее - если другие не смогут обнаружить проблему с этой деталью .(стоит спросить Эрика Липперта и Джона Скита , чтобы прочитать этот вопрос)

По моему личному опыту, даже когда я послал им код, чтобы воспроизвести его, и онивоспроизвел это, я не получил ответ.Теперь это был BizTalk, это среда .NET, так что я думаю, вы, вероятно, получите лучший ответ.


Моя грубая теория

У меня также есть грубая теория, что я чувствую во время высокой нагрузки и максимального переключения контекста , потоку, отвечающему за ожидание, не дается контекст намного дольше, чем ожидалось, поэтому он не получает возможности тайм-аута и прерывания всех этих потоков, Другая теория состоит в том, что потоки, занятые их операцией ввода-вывода, занимают больше времени для прерывания и не отвечают на прерывание.Теперь, как я уже сказал, это грубо, и я могу доказать или решить это за пределами моей компетенции.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...