1. 程式人生 > 程式設計 >構建高效的python requests長連線池詳解

構建高效的python requests長連線池詳解

前文:

最近在搞全網的CDN刷新系統,在效能調優時遇到了requests長連線的一個問題,以前關注過長連線太多造成浪費的問題,但因為系統都是分散式擴充套件的,針對這種各別問題就懶得改動了。 現在開發的快取刷新系統,對於效能還是有些敏感的,我後面會給出最優的http長連線池構建方式。

老生常談:

python下的httpclient庫哪個最好用? 我想大多數人還是會選擇requests庫的。原因麼?也就是簡單,易用!

如何蛋疼的構建reqeusts的短連線請求:

python requests庫預設就是長連線的 (http 1.1,Connection: keep alive),如果單純在requests頭部去掉Connection是不靠譜的,還需要藉助httplib來配合.

s = requests.Session()

del s.headers['Connection']

正確發起 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/')

服務端接收的http包體內容:

GET / HTTP/1.0
Accept-Encoding: gzip,deflate
Accept: */*
User-Agent: python-requests/2.5.1 CPython/2.7.10 Darwin/15.4.0

所謂短連線就是傳送 HTTP 1.0 協議,這樣web服務端當然會在send完資料後,觸發close(),也就是傳遞 \0 字串,達到關閉連線 ! 這裡還是要吐槽一下,好多人天天說系統優化,連個基本的網路io都不優化,你還想幹嘛。。。下面我們依次聊requests長連線的各種問題及效能優化。

那麼requests長連線如何實現?

requests給我們提供了一個Session的長連線類,他不僅僅能實現最基本的長連線保持,還會附帶服務端返回的cookie資料。 在底層是如何實現的?

把HTTP 1.0 改成 HTTP 1.1 就可以了, 如果你標明瞭是HTTP 1.1 ,那麼有沒有 Connection: keep-alive 都無所謂的。 如果 HTTP 1.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,NULL,0) = 144
fcntl(5,F_GETFL)      = 0x2 (flags O_RDWR)
fcntl(5,F_SETFL,O_RDWR)    = 0

Session的長連線支援多個主機麼? 也就是我在一個服務裡先後訪問 a.com,b.com,c.com 那麼requests session能否幫我保持連線 ?

答案很明顯,當然是可以的!

但也僅僅是可以一用,但他的實現有很多的槽點。比如xiaorui.cc的主機上還有多個虛擬主機,那麼會出現什麼情況麼? 會不停的建立新連線,因為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,sin_port=htons(53),sin_addr=inet_addr("10.202.72.116")},sin_addr=inet_addr("125.211.204.141")},{sa_family=AF_UNSPEC,sa_data="\0\0\0\0\0\0\0\0\0\0\0\0\0\0"},sin_addr=inet_addr("153.37.238.190")},sin_addr=inet_addr("157.255.128.103")},sin_addr=inet_addr("139.215.203.190")},sin_addr=inet_addr("42.56.76.104")},sin_addr=inet_addr("42.236.125.104")},sin_addr=inet_addr("110.53.246.11")},sin_addr=inet_addr("36.248.26.191")},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會發生呢? 是我們要的結果,只需要一個連線就可以了。

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')

我們可以看到該程序只例項化了一個長連線。

# 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內建一個連線池,requests庫預設值為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的連線池是如何實現的? 通過程式碼很容易得出Session()預設的連線數及連線池是如何構建的? 下面是requests的長連線實現原始碼片段。如需要再詳細的實現細節,那就自己分析吧

# 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()) # 根據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 # 預設連線池
DEFAULT_RETRIES = 0 # 預設重試次數
DEFAULT_POOL_TIMEOUT = None # 超時時間

Python requests連線池是借用urllib3.poolmanager來實現的。

每一個獨立的(scheme,host,port)元祖使用同一個Connection,(scheme,port)是從請求的URL中解析分拆出來的。

from .packages.urllib3.poolmanager import PoolManager,proxy_from_url 。

下面是 urllib3的一些精簡原始碼, 可以看出他的連線池實現也是簡單粗暴的。

# 解析url,分拆出scheme,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,port=80,path=None,...)
  >>> parse_url('/foo?bar')
  Url(scheme=None,host=None,path='/foo',query='bar',...)

 return Url(scheme,auth,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,scheme='http',pool_kwargs=None):
 if scheme == "https":
  return super(ProxyManager,self).connection_from_host(
   host,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)


# 根據url的三個指標獲取連線
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,request_context=request_context)
  self.pools[pool_key] = pool
 return pool


# 獲取長連線的主入口
def urlopen(self,method,redirect=True,**kw):
 u = parse_url(url)
 conn = self.connection_from_host(u.host,scheme=u.scheme)

這裡為止,Python requests關於session連線類實現,說的算明白了。 但就requests和urllib3的連線池實現來說,還是有一些提升空間的。 但問題來了,單單靠著域名和埠會造成一些問題,至於造成什麼樣子的問題,我在上面已經有詳細的描述了。

那麼如何解決?

我們可以用 scheme + 主domain + host_ip + port 來實現長連線池的管理。

其實大多數的場景是無需這麼細緻的實現連線池的,但根據我們的測試的結果來看,在服務初期效能提升還是不小的。

這樣既解決了域名ip輪詢帶來的連線重置問題,也解決了多級域名下不能共用連線的問題。

以上這篇構建高效的python requests長連線池詳解就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支援我們。