问题发现
调用钉钉机器人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连接。
往上看,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
是IPv6地址,再加上前面分析的family
作用,可以确定问题出现的根本原因了:
如果服务器支持IPv6,那么requests默认会使用IPv6去连接服务器,某些外部因素导致IPv6连接建立很慢(比如IPv6被禁用),socket.socket()
又是阻塞的,导致此问题出现。
分别在家和公司(两个不同网络)下测试同一脚本,发现公司网络连接IPv6速度极慢,而家里则正常连接,证实是网络原因导致此问题。
解决办法
知道原因,解决起来就简单了,直接重写allowed_gai_family()
,强制使用IPv4:
import socket
import urllib3
def allowed_gai_family():
return socket.AF_INET
urllib3.util.connection.allowed_gai_family = allowed_gai_family
再次运行:
搞定!
参考文档
网上大部分答案都是用session,根本没用。后来在stackoverflow上找到一点灵感,大家有兴趣可以去看看:
- https://stackoverflow.com/questions/62599036/python-requests-is-slow-and-takes-very-long-to-complete-http-or-https-request
- https://stackoverflow.com/questions/33046733/force-requests-to-use-ipv4-ipv6/46972341#46972341