构建高效的python requests长连接池详解
前文:
最近在搞全网的CDN刷新系统,在性能调优时遇到了requests长连接的一个问题,以前关注过长连接太多造成浪费的问题,但因为系统都是分布式扩展的,针对这种各别问题就懒得改动了。现在开发的缓存刷新系统,对于性能还是有些敏感的,我后面会给出最优的http长连接池构建方式。
老生常谈:
python下的httpclient库哪个最好用?我想大多数人还是会选择requests库的。原因么?也就是简单,易用!
如何蛋疼的构建reqeusts的短连接请求:
pythonrequests库默认就是长连接的(http1.1,Connection:keepalive),如果单纯在requests头部去掉Connection是不靠谱的,还需要借助httplib来配合.
s=requests.Session()
dels.headers['Connection']
正确发起http1.0的请求姿势是:
#xiaorui.cc importhttplib importrequests httplib.HTTPConnection._http_vsn=10 httplib.HTTPConnection._http_vsn_str='HTTP/1.0' r=requests.get('http://127.0.0.1:8888/')
服务端接收的http包体内容:
GET/HTTP/1.0 Accept-Encoding:gzip,deflate Accept:*/* User-Agent:python-requests/2.5.1CPython/2.7.10Darwin/15.4.0
所谓短连接就是发送HTTP1.0协议,这样web服务端当然会在send完数据后,触发close(),也就是传递\0字符串,达到关闭连接!这里还是要吐槽一下,好多人天天说系统优化,连个基本的网络io都不优化,你还想干嘛。。。下面我们依次聊requests长连接的各种问题及性能优化。
那么requests长连接如何实现?
requests给我们提供了一个Session的长连接类,他不仅仅能实现最基本的长连接保持,还会附带服务端返回的cookie数据。在底层是如何实现的?
把HTTP1.0改成HTTP1.1就可以了,如果你标明了是HTTP1.1,那么有没有Connection:keep-alive都无所谓的。如果HTTP1.0加上Connection:keep-alive,那么server会认为你是长连接。就这么简单!
poll([{fd=5,events=POLLIN}],1,0)=0(Timeout) sendto(5,"GET/HTTP/1.1\r\nHost:www.xiaorui.cc\r\nConnection:keep-alive\r\nAccept-Encoding:gzip,deflate\r\nAccept:*/*\r\nUser-Agent:python-requests/2.9.1\r\n\r\n",144,0,NULL,0)=144 fcntl(5,F_GETFL)=0x2(flagsO_RDWR) fcntl(5,F_SETFL,O_RDWR)=0
Session的长连接支持多个主机么?也就是我在一个服务里先后访问a.com,b.com,c.com那么requestssession能否帮我保持连接?
答案很明显,当然是可以的!
但也仅仅是可以一用,但他的实现有很多的槽点。比如xiaorui.cc的主机上还有多个虚拟主机,那么会出现什么情况么?会不停的创建新连接,因为reqeusts的urllib3连接池管理是基于host的,这个host可能是域名,也可能ip地址,具体是什么,要看你的输入。
strace-p25449-etrace=connect Process25449attached-interrupttoquit connect(13,{sa_family=AF_INET,sin_port=htons(80),sin_addr=inet_addr("61.216.13.196")},16)=0 connect(8,{sa_family=AF_INET,sin_port=htons(53),sin_addr=inet_addr("10.202.72.116")},16)=0 connect(8,{sa_family=AF_INET,sin_port=htons(80),sin_addr=inet_addr("125.211.204.141")},16)=0 connect(8,{sa_family=AF_UNSPEC,sa_data="\0\0\0\0\0\0\0\0\0\0\0\0\0\0"},16)=0 connect(8,{sa_family=AF_INET,sin_port=htons(80),sin_addr=inet_addr("153.37.238.190")},16)=0 connect(8,{sa_family=AF_UNSPEC,sa_data="\0\0\0\0\0\0\0\0\0\0\0\0\0\0"},16)=0 connect(8,{sa_family=AF_INET,sin_port=htons(80),sin_addr=inet_addr("157.255.128.103")},16)=0 connect(8,{sa_family=AF_UNSPEC,sa_data="\0\0\0\0\0\0\0\0\0\0\0\0\0\0"},16)=0 connect(8,{sa_family=AF_INET,sin_port=htons(80),sin_addr=inet_addr("139.215.203.190")},16)=0 connect(8,{sa_family=AF_UNSPEC,sa_data="\0\0\0\0\0\0\0\0\0\0\0\0\0\0"},16)=0 connect(8,{sa_family=AF_INET,sin_port=htons(80),sin_addr=inet_addr("42.56.76.104")},16)=0 connect(8,{sa_family=AF_UNSPEC,sa_data="\0\0\0\0\0\0\0\0\0\0\0\0\0\0"},16)=0 connect(8,{sa_family=AF_INET,sin_port=htons(80),sin_addr=inet_addr("42.236.125.104")},16)=0 connect(8,{sa_family=AF_UNSPEC,sa_data="\0\0\0\0\0\0\0\0\0\0\0\0\0\0"},16)=0 connect(8,{sa_family=AF_INET,sin_port=htons(80),sin_addr=inet_addr("110.53.246.11")},16)=0 connect(8,{sa_family=AF_UNSPEC,sa_data="\0\0\0\0\0\0\0\0\0\0\0\0\0\0"},16)=0 connect(8,{sa_family=AF_INET,sin_port=htons(80),sin_addr=inet_addr("36.248.26.191")},16)=0 connect(8,{sa_family=AF_UNSPEC,sa_data="\0\0\0\0\0\0\0\0\0\0\0\0\0\0"},16)=0 connect(8,{sa_family=AF_INET,sin_port=htons(80),sin_addr=inet_addr("125.211.204.151")},16)=0
又比如你可能都是访问同一个域名,但是子域名不一样,例子a.xiaorui.cc,b.xiaorui.cc,c.xiaorui.cc,xxxx.xiaorui.cc,那么会造成什么问题?哪怕IP地址是一样的,因为域名不一样,那么requestssession还是会帮你实例化长连接。
python24899root3uIPv4271877220t0TCP101.200.80.162:59576->220.181.105.185:http(ESTABLISHED) python24899root4uIPv4271877250t0TCP101.200.80.162:54622->101.200.80.162:http(ESTABLISHED) python24899root5uIPv4271877410t0TCP101.200.80.162:59580->220.181.105.185:http(ESTABLISHED) python24899root6uIPv4271877440t0TCP101.200.80.162:59581->220.181.105.185:http(ESTABLISHED) python24899root7uIPv4271878580t0TCPlocalhost:50964->localhost:http(ESTABLISHED) python24899root8uIPv4271878800t0TCP101.200.80.162:54630->101.200.80.162:http(ESTABLISHED) python24899root9uIPv4271879210t0TCP101.200.80.162:54632->101.200.80.162:http(ESTABLISHED)
如果是同一个二级域名,不同的url会发生呢?是我们要的结果,只需要一个连接就可以了。
importrequests importtime s=requests.Session() while1: r=s.get('http://a.xiaorui.cc/1') r=s.get('http://a.xiaorui.cc/2') r=s.get('http://a.xiaorui.cc/3')
我们可以看到该进程只实例化了一个长连接。
#xiaorui.cc python27173root2uCHR136,110t014/dev/pts/11 python27173root3uIPv4272124800t0TCP101.200.80.162:36090->220.181.105.185:http(ESTABLISHED) python27173root12rCHR1,90t03871/dev/urandom
那么requests还有一个不是问题的性能问题。。。
requestssession是可以保持长连接的,但他能保持多少个长连接?10个长连接!session内置一个连接池,requests库默认值为10个长连接。
requests.adapters.HTTPAdapter(pool_connections=100,pool_maxsize=100)
一般来说,单个session保持10个长连接是绝对够用了,但如果你是那种social爬虫呢?这么多域名只共用10个长连接肯定不够的。
python28484root3uIPv4272254860t0TCP101.200.80.162:54724->103.37.145.167:http(ESTABLISHED) python28484root4uIPv4272253490t0TCP101.200.80.162:36583->120.132.34.62:https(ESTABLISHED) python28484root5uIPv4272254900t0TCP101.200.80.162:46128->42.236.125.104:http(ESTABLISHED) python28484root6uIPv4272254950t0TCP101.200.80.162:43162->222.240.172.228:http(ESTABLISHED) python28484root7uIPv4272256130t0TCP101.200.80.162:37977->116.211.167.193:http(ESTABLISHED) python28484root8uIPv4272254130t0TCP101.200.80.162:40688->106.75.67.54:http(ESTABLISHED) python28484root9uIPv4272254170t0TCP101.200.80.162:59575->61.244.111.116:http(ESTABLISHED) python28484root10uIPv4272255210t0TCP101.200.80.162:39199->218.246.0.222:http(ESTABLISHED) python28484root11uIPv4272255240t0TCP101.200.80.162:46204->220.181.105.184:http(ESTABLISHED) python28484root12rCHR1,90t03871/dev/urandom python28484root14uIPv4272254200t0TCP101.200.80.162:42684->60.28.124.21:http(ESTABLISHED)
让我们看看requests的连接池是如何实现的?通过代码很容易得出Session()默认的连接数及连接池是如何构建的?下面是requests的长连接实现源码片段。如需要再详细的实现细节,那就自己分析吧
#xiaorui.cc classSession(SessionRedirectMixin): def__init__(self): ... self.max_redirects=DEFAULT_REDIRECT_LIMIT self.cookies=cookiejar_from_dict({}) self.adapters=OrderedDict() self.mount('https://',HTTPAdapter())#如果没有单独配置adapter适配器,那么就临时配置一个小适配器 self.mount('http://',HTTPAdapter())#根据schema来分配不同的适配器adapter,上面是https,下面是http self.redirect_cache=RecentlyUsedContainer(REDIRECT_CACHE_SIZE) classHTTPAdapter(BaseAdapter): def__init__(self,pool_connections=DEFAULT_POOLSIZE, pool_maxsize=DEFAULT_POOLSIZE,max_retries=DEFAULT_RETRIES, pool_block=DEFAULT_POOLBLOCK): ifmax_retries==DEFAULT_RETRIES: self.max_retries=Retry(0,read=False) else: self.max_retries=Retry.from_int(max_retries) self.config={} self.proxy_manager={} super(HTTPAdapter,self).__init__() self._pool_connections=pool_connections self._pool_maxsize=pool_maxsize self._pool_block=pool_block self.init_poolmanager(pool_connections,pool_maxsize,block=pool_block)#连接池管理 DEFAULT_POOLBLOCK=False#是否阻塞连接池 DEFAULT_POOLSIZE=10#默认连接池 DEFAULT_RETRIES=0#默认重试次数 DEFAULT_POOL_TIMEOUT=None#超时时间
Pythonrequests连接池是借用urllib3.poolmanager来实现的。
每一个独立的(scheme,host,port)元祖使用同一个Connection,(scheme,host,port)是从请求的URL中解析分拆出来的。
from.packages.urllib3.poolmanagerimportPoolManager,proxy_from_url。
下面是urllib3的一些精简源码,可以看出他的连接池实现也是简单粗暴的。
#解析url,分拆出scheme,host,port defparse_url(url): """ Example:: >>>parse_url('http://google.com/mail/') Url(scheme='http',host='google.com',port=None,path='/mail/',...) >>>parse_url('google.com:80') Url(scheme=None,host='google.com',port=80,path=None,...) >>>parse_url('/foo?bar') Url(scheme=None,host=None,port=None,path='/foo',query='bar',...) returnUrl(scheme,auth,host,port,path,query,fragment) #获取匹配的长连接 defconnection_from_url(self,url,pool_kwargs=None): u=parse_url(url) returnself.connection_from_host(u.host,port=u.port,scheme=u.scheme,pool_kwargs=pool_kwargs) #获取匹配host的长连接 defconnection_from_host(self,host,port=None,scheme='http',pool_kwargs=None): ifscheme=="https": returnsuper(ProxyManager,self).connection_from_host( host,port,scheme,pool_kwargs=pool_kwargs) returnsuper(ProxyManager,self).connection_from_host( self.proxy.host,self.proxy.port,self.proxy.scheme,pool_kwargs=pool_kwargs) #根据url的三个指标获取连接 defconnection_from_pool_key(self,pool_key,request_context=None): withself.pools.lock: pool=self.pools.get(pool_key) ifpool: returnpool scheme=request_context['scheme'] host=request_context['host'] port=request_context['port'] pool=self._new_pool(scheme,host,port,request_context=request_context) self.pools[pool_key]=pool returnpool #获取长连接的主入口 defurlopen(self,method,url,redirect=True,**kw): u=parse_url(url) conn=self.connection_from_host(u.host,port=u.port,scheme=u.scheme)
这里为止,Pythonrequests关于session连接类实现,说的算明白了。但就requests和urllib3的连接池实现来说,还是有一些提升空间的。但问题来了,单单靠着域名和端口会造成一些问题,至于造成什么样子的问题,我在上面已经有详细的描述了。
那么如何解决?
我们可以用scheme+主domain+host_ip+port来实现长连接池的管理。
其实大多数的场景是无需这么细致的实现连接池的,但根据我们的测试的结果来看,在服务初期性能提升还是不小的。
这样既解决了域名ip轮询带来的连接重置问题,也解决了多级域名下不能共用连接的问题。
以上这篇构建高效的pythonrequests长连接池详解就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持毛票票。