Python `socket.getaddrinfo` занимает 5 секунд около 0,1% запросов

Запуск Python в проекте Django, который взаимодействует с различными веб-сервисами, у нас есть проблема, из-за которой иногда запросы занимают около 5 секунд вместо их обычных < 100 мс.

Я сократил это до времени, проведенного в функции socket.getaddrinfo - это называется requests, когда мы подключаемся к внешним службам, но также, по-видимому, влияет на соединение Django по умолчанию с полем базы данных Postgres в кластер. Когда мы перезапускаем uwsgi после развертывания, первые запросы, которые входят в систему, занимают 5 секунд, чтобы отправить ответ. Я также считаю, что наши задачи сельдерея занимают 5 секунд на регулярной основе, но я еще не добавил слежение за ними в statsd.

Я написал код для воспроизведения проблемы:

import socket
import timeit

def single_dns_lookup():
    start = timeit.default_timer()
    socket.getaddrinfo('stackoverflow.com', 443)
    end = timeit.default_timer()
    return int(end - start)

timings = {}

for _ in range(0, 10000):
    time = single_dns_lookup()
    try:
        timings[time] += 1
    except KeyError:
        timings[time] = 1

print timings

Типичные результаты: {0: 9921, 5: 79}

Мой коллега уже указал на возможные проблемы вокруг времени поиска ipv6 и добавил это к /etc/gai.conf:

precedence ::ffff:0:0/96  100

Это определенно улучшило поиск из не-Python-программ, таких как curl, которые мы используем, но не из самого Python. В ящиках сервера работает Ubuntu 16.04.3 LTS, и я могу воспроизвести это на ванильной виртуальной машине с Python 2.

Какие шаги я могу предпринять для повышения производительности всех поисков Python, чтобы они могли принимать < 1s?

Ответ 1

5s - это тайм-аут по умолчанию для поиска DNS.

Вы можете снизить это.

Ваша настоящая проблема - это, вероятно, (тихие) пакеты UDP в сети, однако.

Изменить: Эксперимент с разрешением по TCP. Никогда этого не делал. Могу вам помочь.

Ответ 2

Есть две вещи, которые можно сделать. Во-первых, вы не запрашиваете адрес IPV6, это может быть сделано с помощью исправления обезьяны getaddrinfo

orig_getaddrinfo = socket.getaddrinfo

def _getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
    return orig_getaddrinfo(host, port, socket.AF_INET, type, proto, flags)

socket.getaddrinfo = _getaddrinfo

Затем вы также можете использовать кеш на основе ttl для кэширования результата. Вы можете использовать пакет cachepy для этого.

from cachetools import cached
import socket
import timeit
from cachepy import *
# or from cachepy import Cache

cache_with_ttl = Cache(ttl=600) # ttl given in seconds

orig_getaddrinfo = socket.getaddrinfo

# @cached(cache={})
@cache_with_ttl
def _getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
    return orig_getaddrinfo(host, port, socket.AF_INET, type, proto, flags)

socket.getaddrinfo = _getaddrinfo

def single_dns_lookup():
    start = timeit.default_timer()
    socket.getaddrinfo('stackoverflow.com', 443)
    end = timeit.default_timer()
    return int(end - start)

timings = {}

for _ in range(0, 10000):
    time = single_dns_lookup()
    try:
        timings[time] += 1
    except KeyError:
        timings[time] = 1

print (timings)

Ответ 3

Сначала я попытался понять основную причину медленности, прежде чем создавать кеш или monkeypatching socket.getaddrinfo. Правильно ли настроены ваши серверы имен в /etc/resolv.conf? Вы видите потерю пакетов в сети?

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