PHP et les résolveurs DNS

Aujourd’hui, la plupart des applications Web communiquent avec des API externes. La résolution des noms des domaines associés à ces API externes doit être performante. Une mauvaise configuration du « resolver DNS » du système (linux) hébergeant votre applicatif pourra entraîner des lenteurs de votre applicatif. Selon l’applicatif, cela sera plus ou moins critique.

Contexte

Sur l’applicatif d’un client que nos équipes maintiennent, nous avions parfois des temps de réponse de plus de 5 secondes comme les traces newrelic nous l’indiquent :

stracktrace newrelic - 5s

Sous le capot, cet applicatif envoie des logs en utilisant le handler gelf de monolog (comme expliqué dans l’article Introduction au monitoring d’une application Symfony2). Les logs sont envoyés en UDP. La méthode d’initialisation de la socket permettant l’envoi des informations ressemble à ça :

private function buildSocket()
{
    $socketDescriptor = sprintf(
        "%s://%s:%d",
        $this->scheme,
        $this->host,
        $this->port
    );
    $socket = @stream_socket_client(
        $socketDescriptor,
        $errNo,
        $errStr,
        $this->connectTimeout,
        \STREAM_CLIENT_CONNECT,
        stream_context_create($this->context)
    );
    if ($socket === false) {
        throw new RuntimeException(
            sprintf(
                "Failed to create socket-client for %s: %s (%s)",
                $socketDescriptor,
                $errStr,
                $errNo
            )
        );
    }
    // set non-blocking for UDP
    if (strcasecmp("udp", $this->scheme) == 0) {
        stream_set_blocking($socket, 0);
    }
    return $socket;
}

Source : https://github.com/bzikarsky/gelf-php/blob/1.4.2/src/Gelf/Transport/StreamSocketClient.php#L137

La variable $host contient un nom de domaine (logstash-gelf.client.com) et non pas une IP. Nous utilisons un nom de domaine plutôt qu’une IP pour toutes les bonnes raisons que vous connaissez (simplifie la gestion et l’identification du service, changement d’IP facilité, …).

Notre applicatif met donc jusqu’à 5 secondes pour initialiser la socket. Comment est-ce possible ? Nous avons remplacé le nom de domaine logstash-gelf.client.com par son IP et le problème a disparu.

Fonctionnement général

Sur un système d’exploitation (nous prenons ici pour exemple Linux), lorsqu’un applicatif doit communiquer avec un service tiers et que le service tiers est référencé avec son nom de domaine (exemple : logstash-gelf.client.com), une résolution DNS doit être effectuée. Certains applicatifs vont mettre en cache cette résolution DNS au démarrage de l’applicatif. C’est le cas de Varnish ou NGINX. Il est à noter que curl (donc l’extension curl de PHP) met par défaut en cache les résolutions DNS :

Pass a long, this sets the timeout in seconds. Name resolves will be kept in memory and used for this number of seconds. Set to zero to completely disable caching, or set to –1 to make the cached entries remain forever. By default, libcurl caches this info for 60 seconds. source : https://curl.haxx.se/libcurl/c/CURLOPT_DNS_CACHE_TIMEOUT.html

Lorsque vous utilisez l’extension curl avec PHP, la mise en cache ne sera possible que dans le cas de l’exécution de PHP via php-fpm ou mod_php (plus d’explications).

D’autres applicatifs (par exemple, PHP (n’utilisant pas l’extension curl) ou nodejs) ne vont pas mettre en cache cette résolution DNS et chaque fois qu’ils devront accéder au service, la résolution DNS sera effectuée. Dans la plupart des cas, ce sera extrêmement rapide. Sous linux, c’est le fichier /etc/resolv.conf qui contient la configuration du « resolver » DNS. Ce fichier contient en général le nom des serveurs DNS qui seront utilisés :

search client.com
nameserver 212.X.X.X
nameserver 212.Y.Y.y

Une résolution DNS peut prendre jusqu’à 5 secondes car le timeout par défaut de la configuration du resolver DNS (/etc/resolv.conf) est de 5 secondes (cf. http://man7.org/linux/man-pages/man5/resolv.conf.5.html). Voilà, l’origine des 5 secondes que nous avions.

Par défaut, la plupart des systèmes Linux ne mettront pas en cache la résolution DNS (ce n’est pas le cas de Windows qui dispose par défaut d’un cache DNS). Cela signifie qu’à chaque fois que nous tenterons d’interroger le service distant, une résolution DNS sera effectuée. Comme dit plus haut, c’est en général extrêmement rapide mais dans certains cas, cela pourra être plus long et cela dépendra notamment du timeout défini dans le fichier /etc/resolv.conf.

La requête DNS système pourra être plus lente dans le cas, par exemple, où le resolveur DNS qui est interrogé doit mettre à jour sa base.

Amélioration de la configuration

Pour améliorer les choses, il est possible de réduire ce timeout (via l’option timeout dans le fichier /etc/resolv.confvoir toutes les options).

search client.com
nameserver 212.X.X.X
nameserver 212.Y.Y.y
options timeout:1

Cette configuration peut néanmoins créer des latences et un script pourra attendre jusqu’à 1 seconde la réponse de la résolution DNS. C’est la première chose que nous avons mise en place sur notre applicatif. Nous avons néanmoins eu des lenteurs. La méthode buildSocket peut désormais mettre jusqu’à 1 seconde. C’est mieux que nos 5 secondes initiales.

stracktrace Newrelic - 1s

Attention, selon la configuration de votre système, ce fichier peut être dynamique et géré par votre système d’exploitation (exemple sur Ubuntu avec le NetworkManager).

Mise en cache des requêtes DNS au niveau système

Comme évoqué plus haut, par défaut, le système ne met pas en cache les résolutions DNS. Il a plein de bonnes raisons à ne pas le faire. En cas de changement d’IP, l’applicatif sera plus rapidement informé de ce changement, la modification ne nécessitera pas d’intervention « humaine » sur le système pour invalider le cache DNS…

Pour améliorer les performances de résolution DNS, il est donc possible d’installer un cache pour le « resolver » DNS. Il existes plusieurs solutions :

De notre côté, notre hébergeur nous a recommandé d’installer nscd. La mise en place du cache DNS a supprimé nos problèmes de lenteur.

En interne, nous utilisons dnsmasq sur nos machines locales car il est pratique. En 2 lignes, nous pouvons résoudre tous les *.dev et *.localhost vers 127.0.0.1 :

address=/dev/127.0.0.1
address=/localhost/127.0.0.1

Pour conclure, petit avertissement, dans notre cas, cette solution nous a permis de gagner en performance car notre applicatif effectuait de nombreuses requêtes DNS (via stream_socket_client). Elle n’est pas nécessairement à mettre en œuvre partout sans réfléchir. Une résolution DNS lente peut être liée à d’autres problèmes (réseaux, routage, …).

Quelques ressources pour approfondir le sujet :

blog comments powered by Disqus