构建高效的python requests长连接池详解


Posted in Python onMay 02, 2020

前文:

最近在搞全网的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, 0, 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, {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会发生呢? 是我们要的结果,只需要一个连接就可以了。

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, host, port)是从请求的URL中解析分拆出来的。

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

下面是 urllib3的一些精简源码, 可以看出他的连接池实现也是简单粗暴的。

# 解析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)


# 根据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, 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关于session连接类实现,说的算明白了。 但就requests和urllib3的连接池实现来说,还是有一些提升空间的。 但问题来了,单单靠着域名和端口会造成一些问题,至于造成什么样子的问题,我在上面已经有详细的描述了。

那么如何解决?

我们可以用 scheme + 主domain + host_ip + port 来实现长连接池的管理。

其实大多数的场景是无需这么细致的实现连接池的,但根据我们的测试的结果来看,在服务初期性能提升还是不小的。

这样既解决了域名ip轮询带来的连接重置问题,也解决了多级域名下不能共用连接的问题。

以上这篇构建高效的python requests长连接池详解就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持三水点靠木。

Python 相关文章推荐
python逐行读取文件内容的三种方法
Jan 20 Python
跟老齐学Python之dict()的操作方法
Sep 24 Python
python批量提取word内信息
Aug 09 Python
python监控linux内存并写入mongodb(推荐)
Sep 11 Python
Python requests发送post请求的一些疑点
May 20 Python
Python学习小技巧总结
Jun 10 Python
Python字符串、整数、和浮点型数相互转换实例
Aug 04 Python
PyQt Qt Designer工具的布局管理详解
Aug 07 Python
FFT快速傅里叶变换的python实现过程解析
Oct 21 Python
win10下安装Anaconda的教程(python环境+jupyter_notebook)
Oct 23 Python
python实现梯度下降和逻辑回归
Mar 24 Python
python爬取抖音视频的实例分析
Jan 19 Python
如何基于windows实现python定时爬虫
May 01 #Python
如何基于python实现不邻接植花
May 01 #Python
Python接口测试结果集实现封装比较
May 01 #Python
解决python虚拟环境切换无效的问题
Apr 30 #Python
python爬虫实现POST request payload形式的请求
Apr 30 #Python
Pycharm IDE的安装和使用教程详解
Apr 30 #Python
scrapy爬虫:scrapy.FormRequest中formdata参数详解
Apr 30 #Python
You might like
PHP中图片等比缩放的实例
2013/03/24 PHP
解析php5配置使用pdo
2013/07/03 PHP
php fsockopen解决办法 php实现多线程
2014/01/20 PHP
destoon实现不同会员组公司名称显示不同的颜色的方法
2014/08/22 PHP
IE autocomplete internet explorer's autocomplete
2007/06/30 Javascript
语义化 H1 标签
2008/01/14 Javascript
Ext对基本类型的扩展 ext,extjs,format
2010/12/25 Javascript
JavaScript中常用的运算符小结
2012/01/18 Javascript
javascript常用代码段搜集
2014/12/04 Javascript
JavaScript获取function所有参数名的方法
2015/10/30 Javascript
Bootstrap table分页问题汇总
2016/05/30 Javascript
jQuery实现点击任意位置弹出层外关闭弹出层效果
2016/10/19 Javascript
IE8兼容Jquery.validate.js的问题
2016/12/01 Javascript
d3.js中冷门却实用的内置函数总结
2017/02/04 Javascript
webpack开发跨域问题解决办法
2017/08/03 Javascript
vue+vuex+axios实现登录、注册页权限拦截
2018/03/09 Javascript
springMvc 前端用json的方式向后台传递对象数组方法
2018/08/07 Javascript
轻松解决JavaScript定时器越走越快的问题
2019/05/13 Javascript
vue项目中使用AES实现密码加密解密(ECB和CBC两种模式)
2019/08/12 Javascript
微信小程序向Java后台传输参数的方法实现
2020/12/10 Javascript
[00:57]林俊杰助阵DOTA2亚洲邀请赛
2015/01/28 DOTA
Python的消息队列包SnakeMQ使用初探
2016/06/29 Python
Python中工作日类库Busines Holiday的介绍与使用
2017/07/06 Python
python中urlparse模块介绍与使用示例
2017/11/19 Python
Django视图和URL配置详解
2018/01/31 Python
python实现多张图片拼接成大图
2019/01/15 Python
对python 自定义协议的方法详解
2019/02/13 Python
python可迭代对象去重实例
2020/05/15 Python
python 匿名函数与三元运算学习笔记
2020/10/23 Python
Superdry极度干燥美国官网:英国制造的服装品牌
2018/11/13 全球购物
水利学院求职自荐书
2014/02/01 职场文书
幼儿园教师师德师风承诺书
2015/04/28 职场文书
针对吵架老公保证书
2015/05/08 职场文书
晚会主持人开场白台词
2015/05/28 职场文书
sql查询语句之平均分、最高最低分及排序语句
2022/05/30 MySQL
MySQL安装失败的原因及解决步骤
2022/06/14 MySQL