суббота, 23 мая 2020 г.

Отладка зависания: все потоки встали на WinHttpGetProxyForUrl

Это перевод Diagnosing a hang: Everybody stuck in WinHttpGetProxyForUrl. Автор: Реймонд Чен.

Клиент сообщил, что их программа рано или поздно полностью встаёт, а все её потоки (750 штук) зависают при вызове WinHttpGetProxyForUrl со следующим стеком вызова:
ntdll!ZwWaitForSingleObject+0x14
KERNELBASE!WaitForSingleObjectEx+0x8f
winhttp!OutProcGetProxyForUrl+0x160
winhttp!WinHttpGetProxyForUrl+0x349
contoso!submit_web_request+0x232
ntdll!TppWorkpExecuteCallback+0x35e
ntdll!TppWorkerThread+0x474
kernel32!BaseThreadInitThunk+0x14
ntdll!RtlUserThreadStart+0x21
(я упростил стек вызова для простоты объяснения)

В программе происходит следующее: вы помещаете некоторое задание (work item) в пул потоков, и это задание вызывает WinHttpGetProxyForUrl. Эта функция является синхронной, но ей нужно делать сетевые запросы HTTP - которые являются асинхронными. Чтобы устранить это противоречие, функция WinHttpGetProxyForUrl выполняет синхронное ожидание завершения асинхронной работы.

И я предполагаю, что для этого функция WinHttpGetProxyForUrl использует пул потоков.

Происходит следующее: программа заполняет пул потоков заданиями submit_web_request. Эти задания вызывают функцию WinHttpGetProxyForUrl, которая добавляет в очередь свои собственные задания и ожидает их завершения. Но эти задания не могут быть запущены, потому что все потоки пула потоков заняты обработкой заданий submit_web_request.

Рано или поздно пул потоков может понять, что он ничего не делает, и запустит новый поток, чтобы справиться с накопившимися заданиями. Возможно, этот поток завершит задание для WinHttpGetProxyForUrl - что позволит продолжить один из потоков для submit_web_request. Как только этот новый поток пула потоков завершает задание от WinHttpGetProxyForUrl, он извлекает следующее задание из очереди - и есть вероятность, что он получит очередное задание submit_web_request, так что теперь мы вернулись к тому, с чего начали, за исключением того, что мы только что создали ещё один застрявший поток в пуле потоков.

Если задания submit_web_request приходят быстрее, чем WinHttpGetProxyForUrl может обработать свои собственные задания, пул потоков начнёт заполнятся потоками, заблокированными в submit_web_request. В итоге пул потоков достигнет своего предела потоков, и всё остановится.

По сути, вы перегружаете пул потоков, заполняя его запросами, которые сами требуют пул потоков. Все потоки пула потоков зависают на обработку ваших запросов, и ни один из них не выполняет задания, сгенерированные вашими запросами.

Это как если бы у вас было много тяжёлого оборудования, которое вам нужно перевезти, поэтому вы нанимаете для перевозки все транспортные компании в городе. Появляется компания А, и они говорят: "Хм, это слишком тяжёлое, чтобы перевезти нашими силами. Давайте мы свяжемся с компаний Б, может быть, они нам помогут". Компания Б говорит: "Извините, мы не можем сейчас вам помочь. Мы только что получили крупный заказ на перевозку". Делая заказ во всех доступных транспортных компаниях, вам удаётся помешать любой из них выполнить работу.

Я подозреваю, что эта программа работает в сетевой среде, где WPAD работает медленно. Т.е. задания от WinHttpGetProxyForUrl будут выполнять свою работу дольше, что повышает вероятность того, что задания submit_web_request будут добавляться быстрее, чем завершаться задания от WinHttpGetProxyForUrl.

Теперь, когда мы диагностировали проблему: что мы можем сделать, чтобы решить её?

Одна из идей состоит в том, чтобы нанять только одну транспортную компанию и позволить ей решать, сколько ещё компаний им нужно. Поместите все свои вызовы submit_web_request в один поток и обрабатывайте их по одному. Это займёт только один поток в пуле, оставляя другие потоки доступными. С другой стороны, это означает, что запросы не могут обрабатываться параллельно.

Лучшим решением будет изменить способ использования пула потоков, чтобы вы не держали его в заложниках надолго.

Я не эксперт по WinHttp, но у других людей были некоторые идеи, как это сделать.

Вы можете переключиться на функцию WinHttpGetProxyForUrlEx, которая возвращает управление немедленно и вызывает вашу функцию обратного вызова, когда у неё появляется ответ. Тогда функция submit_web_request может вызвать WinHttpGetProxyForUrlEx и тоже немедленно выйти. Это освободит поток пула потоков для выполнения другой работы - возможно, даже того самого задания, которое функция WinHttpGetProxyForUrlEx должна выполнить для своего завершения. Когда WinHttpGetProxyForUrlEx выполнит свою асинхронную работу, она вызовет функцию обратного вызова, которая доделает любую работу, которую изначально планировала выполнить submit_web_request после получения информации о прокси.

Короче говоря, нужно идти асинхронно до конца. Это не будет таким уж необоснованным подходом для этой программы, так как сама submit_web_request моделирует асинхронный запрос: она инициирует запрос и вызывает функцию обратного вызова, предоставляемую вызывающим, передавая в неё ответ от сервера. Поскольку она уже ведёт себя асинхронно, вы можете просто сделать её ещё более асинхронной.

Другим предложением было полностью убрать вызов WinHttpGetProxyForUrl и просто передать флаг WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY в WinHttpOpen. Это переложит работу по выяснению прокси на функцию WinHttpOpen, а она сможет выполнить это как часть других своих асинхронных действий. Это кажется хорошей идеей, потому что при этом вы полностью отделяетесь от вопроса выяснения прокси, и вы по-прежнему получаете асинхронное поведение. Ну и вы также получаете удовольствие от исправления ошибки путём удаления кода.

Клиент подтвердил, что переключение на флаг WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY устранило проблему.

Комментариев нет:

Отправка комментария

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.