angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

AIOHTTP: Обнаружение тайм-аута DNS с пользовательскими серверами имен

Используя 'Client tracing', мы можем создать переменные dns_cache_miss и host_resolved , чтобы определить, было ли вызвано исключение в резольвере.

27 июля 2022 Обновленный 27 июля 2022
В Async
post main image
https://www.pexels.com/nl-nl/@fotios-photos

При использовании AIOHTTP для получения данных с веб-страницы в Интернете вы, вероятно, используете тайм-аут для ограничения максимального времени ожидания.

Если вы используете доменное имя, то IP-адрес должен быть разрешен. Без использования отдельного преобразователя вы зависите от базовой операционной системы. Любые ошибки распространяются на ваше приложение.

Я не хотел такой зависимости и указал серверы имен самостоятельно, используя AsyncResolver и TCPConnector.

Теперь предположим, что произойдет тайм-аут. Как узнать, вызван ли тайм-аут резольвером или соединением с удаленным сервером?

Проблема

Запрос AIOHTTP состоит из двух частей:

  • Разрешить DNS
  • Получение данных
    |----------------- request ----------------->|

    |---- resolve DNS --->|---- receive data --->|

    |                     |                      |
----+---------------------+----------------------+---> t
  start

С помощью AIOHTTP мы можем указать максимальное время для запроса. Когда это время истекает, возникает исключение TimeoutError .

Но это касается всего запроса. Нет отдельного исключения для таймаута резольвера. Опять же, как узнать, вызван ли тайм-аут резольвером DNS или удаленным сервером?

Трассировка клиента в помощь

К счастью, мы можем проследить поток выполнения запроса, прикрепив короутины слушателей к сигналам, предоставляемым экземпляром TraceConfig , который можно использовать в качестве параметра для конструктора ClientSession .

Если мы посмотрим на AIOHTTP 'Tracing Reference', см. ссылки ниже, и увеличим 'Connection acquiring' и 'DNS resolving', то увидим, что нам нужны следующие корутины:

  • on_request_start
  • on_dns_cache_miss
  • on_dns_resolvehost_end

Если произошел тайм-аут, и 'on_dns_cache_miss' был вызван, а 'on_dns_resolvehost_end' не был вызван, то можно предположить, что тайм-аут вызван резольвером.

Для того чтобы запустить корутины, мы создаем объект TraceConfig и подключаем к нему корутины. Все, что мы делаем в этих корутинах, это измеряем время с начала запроса и сохраняем его в нашем словаре 'trace_result', передаваемом в качестве контекста, с начальным значением None:

trace_results = {
	'on_dns_cache_hit': None,
	'on_dns_cache_miss': None,
	'on_dns_resolvehost_end': None,
}

Код

Когда возникает исключение, мы сначала проверяем, является ли ошибка TimeoutError. Если это так, мы проверяем, произошло ли исключение в резольвере, используя 'cache_miss' и 'host_resolved'. Выберите либо рабочий резолвер с серверами имен quad9.net, либо просто используйте какой-нибудь IP-адрес.

import asyncio
import aiohttp
from aiohttp.resolver import AsyncResolver
import socket
import sys
import traceback


class Runner:

    def __init__(self):
        pass

    async def on_request_start(self, session, trace_config_ctx, params):
        trace_config_ctx.start = asyncio.get_event_loop().time()

    async def on_dns_cache_miss(self, session, trace_config_ctx, params):
        elapsed = asyncio.get_event_loop().time() - trace_config_ctx.start
        trace_config_ctx.trace_request_ctx['on_dns_cache_miss'] = elapsed

    async def on_dns_resolvehost_end(self, session, trace_config_ctx, params):
        elapsed = asyncio.get_event_loop().time() - trace_config_ctx.start
        trace_config_ctx.trace_request_ctx['on_dns_resolvehost_end'] = elapsed

    async def get_trace_config(self):
        trace_config = aiohttp.TraceConfig()
        trace_config.on_request_start.append(self.on_request_start)
        trace_config.on_dns_cache_miss.append(self.on_dns_cache_miss)
        trace_config.on_dns_resolvehost_end.append(self.on_dns_resolvehost_end)
        trace_results = {
            'on_dns_cache_hit': None,
            'on_dns_cache_miss': None,
            'on_dns_resolvehost_end': None,
        }
        return trace_config, trace_results

    async def run(self, url):
        # quad9.net dns server
        resolver = AsyncResolver(nameservers=['9.9.9.9', '149.112.112.112'])
        # ip address of www.example.com, using this causes a resolver timeout
        resolver = AsyncResolver(nameservers=['93.184.216.34'])

        connector = aiohttp.TCPConnector(
            family=socket.AF_INET,
            resolver=resolver,
        )

        trace_config, trace_results = await self.get_trace_config()

        error = None
        e_str = None
        try:
            async with aiohttp.ClientSession(
                connector=connector,
                timeout=aiohttp.ClientTimeout(total=.5),
                trace_configs=[trace_config],
            ) as session:
                async with session.get(
                    url,
                    trace_request_ctx=trace_results,
                ) as client_response:
                    html = await client_response.text()

        except Exception as e:
            print(traceback.format_exc())
            error = type(e).__name__
            e_str = str(e)
            print('url = {}'.format(url))
            print('error = {}'.format(type(e).__name__))
            print('e_str = {}'.format(e_str))
            print('e.args = {}'.format(e.args))

        finally:
            print('url = {}'.format(url))
            for k, v in trace_results.items():
                print('trace_results: {} = {}'.format(k, v))

        dns_cache_miss = True if trace_results['on_dns_cache_miss'] else False
        host_resolved = True if trace_results['on_dns_resolvehost_end'] else False

        if error == 'TimeoutError':
            if dns_cache_miss and not host_resolved:
                error = 'DNSTimeoutError'

        print('error = {}, e_str = {}'.format(error, e_str))

if __name__=='__main__':
    # 'fast' website
    url = 'http://www.example.com'
    # 'slow' website
    url = 'http://www.imdb.com'
    runner = Runner()
    loop = asyncio.get_event_loop()
    loop.run_until_complete(runner.run(url))

Другие ошибки резолвера

Есть и другие ошибки резолвера, и AIOHTTP здесь нам тоже не поможет, например:

  • Could not contact DNS servers
  • ConnectionRefusedError

Первый, конечно, имеет отношение к резолверу, но ConnectionRefusedError, может возникнуть из-за обоих действий в запросе.

Резюме

Я хочу знать, является ли возникшее исключение результатом работы резольвера или другой части запроса. Если это резольвер, то я могу пометить этот резольвер (временно) недействительным и использовать другой.

Я надеялся, что исключения AIOHHTP дадут мне всю информацию, но это оказалось не так. Возможно, когда-нибудь это будет реализовано, но пока что я должен делать грязную работу сам. Кроме того, AIOHTTP - очень хороший пакет!

Ссылки / кредиты

AIOHTTP - Client exceptions
https://docs.aiohttp.org/en/stable/client_reference.html?highlight=exceptions#client-exceptions

AIOHTTP - Tracing Reference
https://docs.aiohttp.org/en/stable/tracing_reference.html

Monitoring network calls in Python using TIG stack
https://calendar.perfplanet.com/2020/monitoring-network-calls-in-python-using-tig-stack

Подробнее

Aiohttp Async

Оставить комментарий

Комментируйте анонимно или войдите в систему, чтобы прокомментировать.

Комментарии

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.