构建高效的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 相关文章推荐
详解Django框架中用户的登录和退出的实现
Jul 23 Python
django框架如何集成celery进行开发
May 24 Python
简单谈谈python中的语句和语法
Aug 10 Python
python unittest实现api自动化测试
Apr 04 Python
使用Python和xlwt向Excel文件中写入中文的实例
Apr 21 Python
selenium+python 去除启动的黑色cmd窗口方法
May 22 Python
python后端接收前端回传的文件方法
Jan 02 Python
python全栈要学什么 python全栈学习路线
Jun 28 Python
Python中的类与类型示例详解
Jul 10 Python
python GUI库图形界面开发之PyQt5窗口控件QWidget详细使用方法
Feb 26 Python
零基础小白多久能学会python
Jun 22 Python
浅析NumPy 切片和索引
Sep 02 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 Smarty生成EXCEL文档的代码
2008/08/23 PHP
php 删除数组元素
2009/01/16 PHP
PHP数组函数知识汇总
2016/05/12 PHP
php 截取中英文混合字符串的方法
2018/05/31 PHP
利用jQuery的deferred对象实现异步按顺序加载JS文件
2013/03/17 Javascript
JavaScript判断访问的来源是手机还是电脑,用的哪种浏览器
2013/12/12 Javascript
jquery单行文字向上滚动效果示例
2014/03/06 Javascript
判断在css加载完毕后执行后续代码示例
2014/09/03 Javascript
JavaScript设计模式经典之工厂模式
2016/02/24 Javascript
jquery弹出框插件jquery.ui.dialog用法分析
2016/08/20 Javascript
JS 实现计算器详解及实例代码(一)
2017/01/08 Javascript
canvas实现图片根据滑块放大缩小效果
2017/02/24 Javascript
利用imgareaselect辅助后台实现图片上传裁剪
2017/03/02 Javascript
JS数组去重常用方法实例小结【4种方法】
2018/05/28 Javascript
Angular resolve基础用法详解
2018/10/03 Javascript
微信小程序 授权登录详解(附完整源码)
2019/08/23 Javascript
vue element-ui实现input输入框金额数字添加千分位
2019/12/29 Javascript
使用python统计文件行数示例分享
2014/02/21 Python
python实现的登录和操作开心网脚本分享
2014/07/09 Python
tensorflow中next_batch的具体使用
2018/02/02 Python
使用50行Python代码从零开始实现一个AI平衡小游戏
2018/11/21 Python
解决Django中多条件查询的问题
2019/07/18 Python
Django中modelform组件实例用法总结
2020/02/10 Python
tensorflow多维张量计算实例
2020/02/11 Python
python实现连连看游戏
2020/02/14 Python
python读取多层嵌套文件夹中的文件实例
2020/02/27 Python
英国领先的在线旅游和休闲零售商:lastminute.com
2019/01/23 全球购物
意大利买卖二手奢侈品网站:LAMPOO
2020/06/03 全球购物
巴西最大的巴士票务门户:Quero Passagem
2020/11/21 全球购物
医学院毕业生自荐信
2013/11/08 职场文书
中青班党性分析材料
2014/02/16 职场文书
售房委托书
2014/08/30 职场文书
2014年小学工作总结
2014/11/26 职场文书
2014年个人工作总结模板
2014/12/15 职场文书
Django+Celery实现定时任务的示例
2021/06/23 Python
win10如何更改appdata文件夹的默认位置?
2022/07/15 数码科技