構(gòu)建高效的python requests長連接池詳解
前文:
最近在搞全網(wǎng)的CDN刷新系統(tǒng),在性能調(diào)優(yōu)時遇到了requests長連接的一個問題,以前關(guān)注過長連接太多造成浪費的問題,但因為系統(tǒng)都是分布式擴展的,針對這種各別問題就懶得改動了。 現(xiàn)在開發(fā)的緩存刷新系統(tǒng),對于性能還是有些敏感的,我后面會給出最優(yōu)的http長連接池構(gòu)建方式。
老生常談:
python下的httpclient庫哪個最好用? 我想大多數(shù)人還是會選擇requests庫的。原因么?也就是簡單,易用!
如何蛋疼的構(gòu)建reqeusts的短連接請求:
python requests庫默認(rèn)就是長連接的 (http 1.1, Connection: keep alive),如果單純在requests頭部去掉Connection是不靠譜的,還需要借助httplib來配合.
s = requests.Session()
del s.headers['Connection']
正確發(fā)起 http 1.0的請求姿勢是:
#xiaorui.cc import httplib import requests httplib.HTTPConnection._http_vsn = 10 httplib.HTTPConnection._http_vsn_str = 'HTTP/1.0' r = requests.get('http://127.0.0.1:8888/')
服務(wù)端接收的http包體內(nèi)容:
GET / HTTP/1.0 Accept-Encoding: gzip, deflate Accept: */* User-Agent: python-requests/2.5.1 CPython/2.7.10 Darwin/15.4.0
所謂短連接就是發(fā)送 HTTP 1.0 協(xié)議,這樣web服務(wù)端當(dāng)然會在send完數(shù)據(jù)后,觸發(fā)close(),也就是傳遞 \0 字符串,達(dá)到關(guān)閉連接 ! 這里還是要吐槽一下,好多人天天說系統(tǒng)優(yōu)化,連個基本的網(wǎng)絡(luò)io都不優(yōu)化,你還想干嘛。。。下面我們依次聊requests長連接的各種問題及性能優(yōu)化。
那么requests長連接如何實現(xiàn)?
requests給我們提供了一個Session的長連接類,他不僅僅能實現(xiàn)最基本的長連接保持,還會附帶服務(wù)端返回的cookie數(shù)據(jù)。 在底層是如何實現(xiàn)的?
把HTTP 1.0 改成 HTTP 1.1 就可以了, 如果你標(biāo)明了是HTTP 1.1 ,那么有沒有 Connection: keep-alive 都無所謂的。 如果 HTTP 1.0加上Connection: keep-alive ,那么server會認(rèn)為你是長連接。 就這么簡單 !
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 (flags O_RDWR) fcntl(5, F_SETFL, O_RDWR) = 0
Session的長連接支持多個主機么? 也就是我在一個服務(wù)里先后訪問 a.com, b.com, c.com 那么requests session能否幫我保持連接 ?
答案很明顯,當(dāng)然是可以的!
但也僅僅是可以一用,但他的實現(xiàn)有很多的槽點。比如xiaorui.cc的主機上還有多個虛擬主機,那么會出現(xiàn)什么情況么? 會不停的創(chuàng)建新連接,因為reqeusts的urllib3連接池管理是基于host的,這個host可能是域名,也可能ip地址,具體是什么,要看你的輸入。
strace -p 25449 -e trace=connect Process 25449 attached - interrupt to quit 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地址是一樣的,因為域名不一樣,那么requests session還是會幫你實例化長連接。
python 24899 root 3u IPv4 27187722 0t0 TCP 101.200.80.162:59576->220.181.105.185:http (ESTABLISHED) python 24899 root 4u IPv4 27187725 0t0 TCP 101.200.80.162:54622->101.200.80.162:http (ESTABLISHED) python 24899 root 5u IPv4 27187741 0t0 TCP 101.200.80.162:59580->220.181.105.185:http (ESTABLISHED) python 24899 root 6u IPv4 27187744 0t0 TCP 101.200.80.162:59581->220.181.105.185:http (ESTABLISHED) python 24899 root 7u IPv4 27187858 0t0 TCP localhost:50964->localhost:http (ESTABLISHED) python 24899 root 8u IPv4 27187880 0t0 TCP 101.200.80.162:54630->101.200.80.162:http (ESTABLISHED) python 24899 root 9u IPv4 27187921 0t0 TCP 101.200.80.162:54632->101.200.80.162:http (ESTABLISHED)
如果是同一個二級域名,不同的url會發(fā)生呢? 是我們要的結(jié)果,只需要一個連接就可以了。
import requests import time s = requests.Session() while 1: r = s.get('http://a.xiaorui.cc/1') r = s.get('http://a.xiaorui.cc/2') r = s.get('http://a.xiaorui.cc/3')
我們可以看到該進(jìn)程只實例化了一個長連接。
# xiaorui.cc python 27173 root 2u CHR 136,11 0t0 14 /dev/pts/11 python 27173 root 3u IPv4 27212480 0t0 TCP 101.200.80.162:36090->220.181.105.185:http (ESTABLISHED) python 27173 root 12r CHR 1,9 0t0 3871 /dev/urandom
那么requests還有一個不是問題的性能問題。。。
requests session是可以保持長連接的,但他能保持多少個長連接? 10個長連接! session內(nèi)置一個連接池,requests庫默認(rèn)值為10個長連接。
requests.adapters.HTTPAdapter(pool_connections=100, pool_maxsize=100)
一般來說,單個session保持10個長連接是絕對夠用了,但如果你是那種social爬蟲呢?這么多域名只共用10個長連接肯定不夠的。
python 28484 root 3u IPv4 27225486 0t0 TCP 101.200.80.162:54724->103.37.145.167:http (ESTABLISHED) python 28484 root 4u IPv4 27225349 0t0 TCP 101.200.80.162:36583->120.132.34.62:https (ESTABLISHED) python 28484 root 5u IPv4 27225490 0t0 TCP 101.200.80.162:46128->42.236.125.104:http (ESTABLISHED) python 28484 root 6u IPv4 27225495 0t0 TCP 101.200.80.162:43162->222.240.172.228:http (ESTABLISHED) python 28484 root 7u IPv4 27225613 0t0 TCP 101.200.80.162:37977->116.211.167.193:http (ESTABLISHED) python 28484 root 8u IPv4 27225413 0t0 TCP 101.200.80.162:40688->106.75.67.54:http (ESTABLISHED) python 28484 root 9u IPv4 27225417 0t0 TCP 101.200.80.162:59575->61.244.111.116:http (ESTABLISHED) python 28484 root 10u IPv4 27225521 0t0 TCP 101.200.80.162:39199->218.246.0.222:http (ESTABLISHED) python 28484 root 11u IPv4 27225524 0t0 TCP 101.200.80.162:46204->220.181.105.184:http (ESTABLISHED) python 28484 root 12r CHR 1,9 0t0 3871 /dev/urandom python 28484 root 14u IPv4 27225420 0t0 TCP 101.200.80.162:42684->60.28.124.21:http (ESTABLISHED)
讓我們看看requests的連接池是如何實現(xiàn)的? 通過代碼很容易得出Session()默認(rèn)的連接數(shù)及連接池是如何構(gòu)建的? 下面是requests的長連接實現(xiàn)源碼片段。如需要再詳細(xì)的實現(xiàn)細(xì)節(jié),那就自己分析吧
# xiaorui.cc class Session(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()) # 根據(jù)schema來分配不同的適配器adapter,上面是https,下面是http self.redirect_cache = RecentlyUsedContainer(REDIRECT_CACHE_SIZE) class HTTPAdapter(BaseAdapter): def __init__(self, pool_connections=DEFAULT_POOLSIZE, pool_maxsize=DEFAULT_POOLSIZE, max_retries=DEFAULT_RETRIES, pool_block=DEFAULT_POOLBLOCK): if max_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 # 默認(rèn)連接池 DEFAULT_RETRIES = 0 # 默認(rèn)重試次數(shù) DEFAULT_POOL_TIMEOUT = None # 超時時間
Python requests連接池是借用urllib3.poolmanager來實現(xiàn)的。
每一個獨立的(scheme, host, port)元祖使用同一個Connection, (scheme, host, port)是從請求的URL中解析分拆出來的。
from .packages.urllib3.poolmanager import PoolManager, proxy_from_url 。
下面是 urllib3的一些精簡源碼, 可以看出他的連接池實現(xiàn)也是簡單粗暴的。
# 解析url,分拆出scheme, host, port def parse_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', ...) return Url(scheme, auth, host, port, path, query, fragment) # 獲取匹配的長連接 def connection_from_url(self, url, pool_kwargs=None): u = parse_url(url) return self.connection_from_host(u.host, port=u.port, scheme=u.scheme, pool_kwargs=pool_kwargs) # 獲取匹配host的長連接 def connection_from_host(self, host, port=None, scheme='http', pool_kwargs=None): if scheme == "https": return super(ProxyManager, self).connection_from_host( host, port, scheme, pool_kwargs=pool_kwargs) return super(ProxyManager, self).connection_from_host( self.proxy.host, self.proxy.port, self.proxy.scheme, pool_kwargs=pool_kwargs) # 根據(jù)url的三個指標(biāo)獲取連接 def connection_from_pool_key(self, pool_key, request_context=None): with self.pools.lock: pool = self.pools.get(pool_key) if pool: return pool 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 return pool # 獲取長連接的主入口 def urlopen(self, method, url, redirect=True, **kw): u = parse_url(url) conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme)
這里為止,Python requests關(guān)于session連接類實現(xiàn),說的算明白了。 但就requests和urllib3的連接池實現(xiàn)來說,還是有一些提升空間的。 但問題來了,單單靠著域名和端口會造成一些問題,至于造成什么樣子的問題,我在上面已經(jīng)有詳細(xì)的描述了。
那么如何解決?
我們可以用 scheme + 主domain + host_ip + port 來實現(xiàn)長連接池的管理。
其實大多數(shù)的場景是無需這么細(xì)致的實現(xiàn)連接池的,但根據(jù)我們的測試的結(jié)果來看,在服務(wù)初期性能提升還是不小的。
這樣既解決了域名ip輪詢帶來的連接重置問題,也解決了多級域名下不能共用連接的問題。
以上這篇構(gòu)建高效的python requests長連接池詳解就是小編分享給大家的全部內(nèi)容了,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Python可變參數(shù)*args和**kwargs用法實例小結(jié)
這篇文章主要介紹了Python可變參數(shù)*args和**kwargs用法,結(jié)合實例形式總結(jié)分析了Python中可變參數(shù)*args和**kwargs的功能、區(qū)別與具體使用技巧,需要的朋友可以參考下2018-04-04Python中.py程序在CMD控制臺以指定虛擬環(huán)境運行
本文主要介紹了Python中.py程序在CMD控制臺以指定虛擬環(huán)境運行,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07在python中利用dict轉(zhuǎn)json按輸入順序輸出內(nèi)容方式
今天小編就為大家分享一篇在python中利用dict轉(zhuǎn)json按輸入順序輸出內(nèi)容方式,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-02-02Python 通過微信控制實現(xiàn)app定位發(fā)送到個人服務(wù)器再轉(zhuǎn)發(fā)微信服務(wù)器接收位置信息
這篇文章主要介紹了Python 通過微信控制實現(xiàn)app定位發(fā)送到個人服務(wù)器,再轉(zhuǎn)發(fā)微信服務(wù)器接收位置信息,本文給出了實例代碼,代碼簡單易懂,非常不錯具有一定的參考借鑒價值,需要的朋友可以參考下2019-08-08