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

AIOHTTP: Opsporen van DNS timeout met aangepaste nameservers

Met behulp van "Client tracing" kunnen we variabelen dns_cache_miss en host_resolved genereren om te bepalen of een exceptie werd opgewekt in de resolver.

27 juli 2022 Bijgewerkt 27 juli 2022
In Async
post main image
https://www.pexels.com/nl-nl/@fotios-photos

Wanneer u AIOHTTP gebruikt om gegevens van een webpagina op het internet op te halen, gebruikt u waarschijnlijk een time-out om de maximale wachttijd te beperken.

Als u een domeinnaam gebruikt, moet het IP-adres worden omgezet. Zonder gebruik te maken van een aparte resolver bent u afhankelijk van het onderliggende besturingssysteem. Eventuele fouten verspreiden zich naar je applicatie.

Ik wilde deze afhankelijkheid niet en specificeer de nameservers zelf, met behulp van de AsyncResolver en TCPConnector.

Stel nu dat er een timeout optreedt. Hoe weten we of de timeout wordt veroorzaakt door de resolver of door de verbinding met de remote server?

Het probleem

Het AIOHTTP verzoek bestaat uit twee delen:

  • DNS oplossen
  • Gegevens ontvangen
    |----------------- request ----------------->|

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

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

Met AIOHTTP kunnen we een maximale tijd voor het verzoek opgeven. Wanneer deze tijd verstrijkt, wordt een TimeoutError exceptie opgewekt.

Maar dit is voor het hele verzoek. Er is geen aparte uitzondering voor een resolver timeout. Nogmaals, hoe weten we of de timeout wordt veroorzaakt door de DNS resolver of door de server op afstand?

Client tracering als redding

Gelukkig kunnen we de uitvoeringsstroom van een verzoek volgen door listener coroutines te koppelen aan de signalen van de TraceConfig instantie, die gebruikt kan worden als parameter voor de ClientSession constructor.

Als we kijken naar de AIOHTTP 'Tracing Reference', zie links hieronder, en inzoomen op 'Connection acquiring' en 'DNS resolving', dan zien we dat we de volgende coroutines nodig hebben:

  • on_request_start
  • on_dns_cache_miss
  • on_dns_resolvehost_end

Wanneer een timeout optreedt en 'on_dns_cache_miss' werd aangeroepen en 'on_dns_resolvehost_end' werd niet aangeroepen, dan kunnen we aannemen dat de timeout wordt veroorzaakt door de resolver.

Om de coroutines aan de gang te krijgen, maken we een TraceConfig object en koppelen de coroutines. Het enige wat we in deze coroutines doen is de tijd sinds het begin van het verzoek meten en dit opslaan in ons 'trace_result' woordenboek, dat wordt doorgegeven als de context, met beginwaarden None:

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

De code

Wanneer een exceptie wordt opgewekt, controleren we eerst of de fout een TimeoutError is. Indien dit het geval is, controleren we of de uitzondering zich voordeed in de resolver met behulp van 'cache_miss' en 'host_resolved'. Kies ofwel de werkende resolver met nameservers van quad9.net, of gebruik gewoon een IP-adres.

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))

Meer resolverfouten

Er zijn meer resolverfouten en AIOHTTP helpt ons ook hier niet, bijvoorbeeld

  • Could not contact DNS servers
  • ConnectionRefusedError

De eerste heeft zeker te maken met de resolver, maar de ConnectionRefusedError, kan van beide acties in het verzoek afkomstig zijn.

Samenvatting

Ik wil weten of een verhoogde uitzondering afkomstig is van de resolver of van een ander deel van het verzoek. Als het de resolver is, dan kan ik deze resolver (tijdelijk) ongeldig verklaren en een andere gebruiken.

Ik hoopte dat de AIOHHTP exceptions me alle informatie zouden geven, maar dat bleek niet waar te zijn. Misschien wordt het ooit geïmplementeerd, maar voorlopig moet ik het vuile werk zelf doen. Verder is AIOHTTP een heel mooi pakket!

Links / credits

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

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.