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

AIOHTTP: Erkennung von DNS-Zeitüberschreitungen mit benutzerdefinierten Nameservern

Mit 'Client tracing' können wir die Variablen dns_cache_miss und host_resolved erzeugen, um festzustellen, ob im Resolver eine Ausnahme ausgelöst wurde.

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

Wenn Sie AIOHTTP verwenden, um Daten von einer Webseite im Internet abzurufen, verwenden Sie wahrscheinlich einen Timeout, um die maximale Wartezeit zu begrenzen.

Wenn Sie einen Domänennamen verwenden, muss die IP-Adresse aufgelöst werden. Ohne die Verwendung eines separaten Resolvers sind Sie vom zugrunde liegenden Betriebssystem abhängig. Jeder Fehler überträgt sich auf Ihre Anwendung.

Ich wollte diese Abhängigkeit nicht und habe die Nameserver selbst angegeben, indem ich AsyncResolver und TCPConnector verwendet habe.

Nehmen wir nun an, dass eine Zeitüberschreitung auftritt. Woher wissen wir, ob die Zeitüberschreitung durch den Resolver oder durch die Remote-Server-Verbindung verursacht wird?

Das Problem

Die Anfrage AIOHTTP besteht aus zwei Teilen:

  • DNS auflösen
  • Empfangen von Daten
    |----------------- request ----------------->|

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

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

Mit AIOHTTP können wir eine maximale Zeit für die Anfrage angeben. Wenn diese Zeit abläuft, wird eine TimeoutError -Ausnahme ausgelöst.

Diese bezieht sich jedoch auf die gesamte Anfrage. Es gibt keine separate Ausnahme für eine Zeitüberschreitung des Resolvers. Woher wissen wir also, ob die Zeitüberschreitung durch den DNS-Resolver oder durch den Remote-Server verursacht wird?

Client-Tracing zur Rettung

Glücklicherweise können wir den Ausführungsfluss einer Anfrage verfolgen, indem wir Listener-Coroutinen an die von der Instanz TraceConfig bereitgestellten Signale anhängen, die als Parameter für den Konstruktor ClientSession verwendet werden können.

Wenn wir uns die AIOHTTP 'Tracing Reference' ansehen, siehe Links unten, und auf 'Connection acquiring' und 'DNS resolving' zoomen, dann sehen wir, dass wir die folgenden Coroutinen benötigen:

  • on_request_start
  • on_dns_cache_miss
  • on_dns_resolvehost_end

Wenn eine Zeitüberschreitung auftritt und 'on_dns_cache_miss' aufgerufen wurde und 'on_dns_resolvehost_end' nicht, dann können wir davon ausgehen, dass die Zeitüberschreitung durch den Resolver verursacht wurde.

Um die Coroutines zum Laufen zu bringen, erstellen wir ein TraceConfig -Objekt und hängen die Coroutines an. Alles, was wir in diesen Coroutines tun, ist die Zeit seit dem Start der Anfrage zu messen und diese in unserem 'trace_result'-Verzeichnis zu speichern, das als Kontext weitergegeben wird, mit den Anfangswerten None:

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

Der Code

Wenn eine Ausnahme ausgelöst wird, prüfen wir zunächst, ob der Fehler ein TimeoutError ist. Wenn dies der Fall ist, prüfen wir, ob die Ausnahme im Resolver aufgetreten ist, indem wir 'cache_miss' und 'host_resolved' verwenden. Wählen Sie entweder den funktionierenden Resolver mit Nameservern von quad9.net, oder verwenden Sie einfach eine IP-Adresse.

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

Weitere Resolver-Fehler

Es gibt noch mehr Resolver-Fehler und AIOHTTP hilft uns auch hier nicht weiter, zum Beispiel:

  • Could not contact DNS servers
  • ConnectionRefusedError

Die erste hat sicherlich mit dem Resolver zu tun, aber die ConnectionRefusedError kann von beiden Aktionen in der Anfrage herrühren.

Zusammenfassung

Ich möchte wissen, ob eine ausgelöste Ausnahme vom Resolver oder von einem anderen Teil der Anfrage stammt. Wenn es der Resolver ist, dann kann ich diesen Resolver (vorübergehend) als ungültig markieren und einen anderen verwenden.

Ich hatte gehofft, die AIOHHTP-Ausnahmen würden mir alle Informationen liefern, aber das scheint nicht der Fall zu sein. Vielleicht wird es eines Tages implementiert, aber im Moment muss ich die Drecksarbeit selbst machen. Abgesehen davon ist AIOHTTP ein sehr schönes Paket!

Links / Impressum

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

Mehr erfahren

Aiohttp Async

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.