前言
只要用python做过一些web开发,多少都用过requests这个模块,简单,方便的HTTP客户端,大家都爱。不过,如果你只是停留在requests.get()或者requests.post()这样的调用层面上,那你的程序跑起来,可能有点慢,这篇文章将会回答你,慢的其中一个原因,以及优化的方法。
普通的调用有什么问题?
继续之前,我们先来回顾两件事——HTTP/1.1基于TCP,HTTPS基于TLS。
完成一次HTTP的接口调用或者普通的请求抓取数据,一定也包含了TCP的包交互,如果是HTTPS,还会包含TLS的部分。
为什么提这两件事呢,我用一个实际的HTTP的接口调用的例子来说明——
如下所示,是一段简单的HTTP调用的代码,IP地址和token出于敏感原因隐藏了。
import requests
import json
url1 = "https://XX.XX.XX.XX/api/v2/cmdb/firewall/address"
url2 = "https://XX.XX.XX.XX/api/v2/cmdb/firewall/policy/1"
token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
header = {
"Content-type": "application/json",
"Accept": "application/json",
"Authorization": token
}
res1 = requests.get(url=url1, headers=header, verify=False)
res2 = requests.get(url=url1, headers=header, verify=False)
print(res1)
print(res2)
这段代码在执行的同时,我开了Wireshark抓包,会抓到两个TCP流,我们取第一个来看(末尾处混入了第二个流的第一个SYN,因为我没有去过滤)。
仔细观察,Application Data出现在自第一个包之后的31ms,而整个TCP数据流到最后一个ACK收到,也就经过了41ms,换言之,TCP以及TLS的包交互,消耗了30ms,约70%。
第二个流的情况和第一个基本类似,TCP以及TLS的交互消耗了很多时间。
如果你的程序涉及频繁的接口调用,譬如笔者调用防火墙的接口,常常一个策略里要调用多个不同的接口配置不同的对象,每次调用都要走一遍完整的TCP流程,在频繁调用,压力大的时候(比如有同事会一股脑提交N多的策略),可能会有这么几个问题——冗余的TCP开销导致程序执行慢,效率低。
网络设备内部也是个Linux,短时间大量的TCP连接不仅造成CPU层面的压力,还可能引发著名的TIME-WAIT问题。
所以,怎么办?——TCP长连接,或者说保持TCP连接。
而requests模块给出了实现的办法——requests.Session()。
requests.Session(),复用TCP
参考官方的说明。Keep-Alive
Excellent news — thanks to urllib3, keep-alive is 100% automatic within a session!Any requests that you make within a session will automatically reuse the appropriate connection!
Note that connections are only released back to the pool for reuse once all body data has been read; be sure to either set stream to False or read the content property of the Response object.
关键部分已经用粗斜体标出。再来看一下Session()的用法。
下面这个是官方的示例,大概就是,实例化一个Session()对象,然后就像正常的调用那么用就好了 。
s = requests.Session()
s.get('https://httpbin.org/cookies/set/sessioncookie/123456789')
r = s.get('https://httpbin.org/cookies')
print(r.text)
改改我们自己的代码看看效果
现在,代码改成下面这样。
import requests
import json
s = requests.Session()
url1 = "https://XX.XX.XX.XX/api/v2/cmdb/firewall/address"
url2 = "https://XX.XX.XX.XX/api/v2/cmdb/firewall/policy/1"
token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
header = {
"Content-type": "application/json",
"Accept": "application/json",
"Authorization": token
}
res1 = s.get(url=url1, headers=header, verify=False)
res2 = s.get(url=url1, headers=header, verify=False)
print(res1)
print(res2)
运行,然后抓包,结果——
现在,TCP和TLS只会建立和拆除一次,两个get请求总的执行时间是57ms,而之前是81ms,并且,复用同一个TCP的HTTP越多,效果越好。
传入requests.Session() 对象
众所周知,Python是一个套娃语言,套娃乃是Python的传统艺能。
我们写Python代码的时候,难免要把各种接口抽象成一个个函数,类,然后调来调去,这就麻烦了,我在哪去实例化Session()类呢?——在最外层的入口函数的地方。
我们把上面的代码做个简单的修改。
import requests
import json
s = requests.Session()
url1 = "https://XX.XX.XX.XX/api/v2/cmdb/firewall/address"
url2 = "https://XX.XX.XX.XX/api/v2/cmdb/firewall/policy/1"
url3 = "https://XX.XX.XX.XX/api/v2/cmdb/webfilter/profile"
url4 = "https://XX.XX.XX.XX/api/v2/cmdb/webfilter/urlfilter"
token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
header = {
"Content-type": "application/json",
"Accept": "application/json",
"Authorization": token
}
def session_test(func, url, header):
res = func.get(url=url1, headers=header, verify=False)
return res
print(session_test(func=s, url=url1, header=header))
print(session_test(func=s, url=url2, header=header))
print(session_test(func=s, url=url3, header=header))
print(session_test(func=s, url=url4, header=header))
请注意,s是在全局下实例化的,所以只有整个代码执行结束,s才会消失。
当然,你也可以把s在一个函数中实例化,都一样。
这里真正执行get请求的函数是session_test,它接受一个func参数,也就是我们要把s传进去,换言之func = s。
session_test每次执行结束后,只是func会被垃圾回收,s仍然存在。
所以,这个代码在执行后,TCP仍然是复用的。
如上所示,4个请求,78ms,对比之前两个请求就消耗了81ms……如果不做这层优化,四个请求将会消耗160+ms,网络差的时候,这个问题会更加严重。
所以,你get到了吗?
晚安。