问题 Python`socket.getaddrinfo`占用大约0.1%的请求5秒


在与各种Web服务进行通信的Django项目上运行Python,我们遇到一个问题,偶尔请求大约需要5秒而不是通常的<100毫秒。

我把它缩小到时间了 socket.getaddrinfo 功能 - 这是被调用的 requests 当我们连接到外部服务时,它似乎也影响到集群中Postgres数据库框的默认Django连接。当我们重新开始 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的vanilla虚拟机上重现这一点。

我可以采取哪些步骤来提高所有Python查找的性能,使它们可以<1s?


9565
2017-10-19 18:31


起源

听起来你的dns解析器很慢,试试ncsd? - georgexsh
@georgexsh我不确定nscd如何帮助 - 第一个没有缓存的请求仍然需要5s。在缓存值到期后,通过解析器的第一个请求将再次花费5秒。这只会减少慢速请求的百分比而不是完全删除它们吗? - jamesc
您是否曾尝试完全禁用IPv6堆栈 sysctl net.ipv6.conf.all.disable_ipv6=1,并检查是否可以解决您的问题?如果是,那么很可能是你的 python 链接到glibc(或静态编译)的版本,它不尊重gai.conf。 - zeppelin
无法在vagrant ubuntu / xenial64中本地重现它 - ffeast
考虑使用 collections.Counter 而不是自己滚动 KeyError - claymation


答案:


5s是DNS查找的默认超时。

你可以降低它。

你的真正问题可能是(无声)UDP数据包在网络上丢失了。

编辑: 试验 TCP的解决方案。从未这样做过。可能会帮助你。


8
2017-10-26 20:37





有两件事可以做。一个是你不查询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)

2
2017-10-29 19:06





在构建缓存或monkeypatching之前,我首先尝试了解缓慢的根本原因 socket.getaddrinfo。是否正确配置了您的名称服务器 /etc/resolv.conf?您是否在网络上看到丢包?

如果您遇到了无法控制的损失,请运行缓存服务器(nscd)将掩盖但不完全消除问题。


2
2017-10-30 14:45