调用钉钉机器人API发送消息,调试的时发现问题,这个接口调用速度极慢。经多次测试,平均耗时2分钟。
同样的接口使用postman却只耗时200毫秒~
后来发现,凡是钉钉的API oapi.dingtalk.com
,使用requests就特别慢,而postman就很正常。基本可以排除网络原因,那么是什么因素导致requests这么慢呢?
先看下requests源码:
# requests实际调用的urllib3实现
# 而urllib3最终使用socket实现真正的网络通信
# 以下是urllib3 create_connection()部分源码
def create_connection(
address,
timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
source_address=None,
socket_options=None,
):
host, port = address
if host.startswith("["):
host = host.strip("[]")
err = None
# 注意这一行
family = allowed_gai_family()
try:
host.encode("idna")
except UnicodeError:
return six.raise_from(
LocationParseError(u"'%s', label empty or too long" % host), None
)
for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res
sock = None
try:
# 经过跟踪,代码卡在了这里
sock = socket.socket(af, socktype, proto)
# ...
create_connection()
做的事情其实很简单,设置参数,建立socket连接。而建立scoket连接,本身网络又没有问题,不可能那么慢,那么问题出在哪里了呢?
往上看,host没什么可说的,那么很明显了,问题出在这一行:
# Using the value from allowed_gai_family() in the context of getaddrinfo lets
# us select whether to work with IPv4 DNS records, IPv6 records, or both.
# The original create_connection function always returns all records.
family = allowed_gai_family()
通过注释可以知道,family的作用是requests使用IPv4还是IPv6:
def allowed_gai_family():
"""This function is designed to work in the context of
getaddrinfo, where family=socket.AF_UNSPEC is the default and
will perform a DNS search for both IPv6 and IPv4 records."""
family = socket.AF_INET
if HAS_IPV6:
family = socket.AF_UNSPEC
return family
注意if
那里,若系统支持IPv6,那么HAS_IPV6=True
。现代计算机基本都支持IPv6,所以HAS_IPV6
差不多相当于常量了,也就是说只有系统支持,那么requests默认使用IPv6去连接服务器。
而socket.getaddrinfo()
返回的结果也验证了我的这个猜想:
(‘2401:b180:2000:60::f’, 443, 0, 0)
从函数名大概可以猜测socket.getaddrinfo()
的功能,用host、port、family等解析出服务器的IP地址,2401:b180:2000:60::f
是IPv6地址,再加上前面分析的family
作用,大概可以推导出这个问题的原因所在了:
如果服务器支持IPv6,那么requests默认会使用IPv6去连接服务器,而由于网络原因致使IPv6连接建立很慢,socket.socket()
又是阻塞的,所以程序就会一直卡在这里,导致问题出现。
分别在家和公司(两个不同网络)下测试同一脚本,发现公司网络连接IPv6速度极慢,而家里则正常连接,证实是网络原因导致此问题。
知道原因,解决起来就简单了,既然requests默认使用IPv6,那么强制让丫使用IPv4不就行了吗,直接重写allowed_gai_family()
:
import socket
import urllib3
def allowed_gai_family():
return socket.AF_INET
urllib3.util.connection.allowed_gai_family = allowed_gai_family
一开始出现问题时,我也尝试搜过,但大部分答案都是用session,我也试过,几乎没用。后来在stackoverflow上找到一点灵感,大家有兴趣可以去看看: