构建高效的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实现strcmp函数功能示例
Mar 25 Python
python的tkinter布局之简单的聊天窗口实现方法
Sep 03 Python
Python学习思维导图(必看篇)
Jun 26 Python
python使用tkinter实现简单计算器
Jan 30 Python
python实现批量图片格式转换
Jun 16 Python
selenium3+python3环境搭建教程图解
Dec 07 Python
Python 脚本拉取 Docker 镜像问题
Nov 10 Python
python GUI库图形界面开发之PyQt5访问系统剪切板QClipboard类详细使用方法与实例
Feb 27 Python
Pycharm中切换pytorch的环境和配置的教程详解
Mar 13 Python
Pycharm插件(Grep Console)自定义规则输出颜色日志的方法
May 27 Python
tensorflow 2.0模式下训练的模型转成 tf1.x 版本的pb模型实例
Jun 22 Python
Pandas数据结构之Series的使用
Mar 31 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调用Java对象的方法
2006/10/09 PHP
php图像处理函数大全(推荐收藏)
2013/07/11 PHP
PHP实现递归复制整个文件夹的类实例
2015/08/03 PHP
WordPress中给文章添加自定义字段及后台编辑功能区域
2015/12/19 PHP
PHP实现动态执行代码的方法
2016/03/25 PHP
CI框架整合smarty步骤详解
2016/05/19 PHP
利用PHP访问MySql数据库的逻辑操作以及增删改查的实例讲解
2017/08/30 PHP
一组JS创建和操作表格的函数集合
2009/05/07 Javascript
JavaScript 类的定义和引用 JavaScript高级培训 自定义对象
2010/04/27 Javascript
js弹出层包含flash 不能关闭隐藏的2种处理方法
2013/06/17 Javascript
JSON格式的键盘编码对照表
2015/01/29 Javascript
移动端H5开发 Turn.js实现很棒的翻书效果
2016/06/20 Javascript
jQuery.ajax 跨域请求webapi设置headers的解决方案
2016/08/08 Javascript
jQuery easyui刷新当前tabs的方法
2016/09/23 Javascript
js数组操作方法总结(必看篇)
2016/11/22 Javascript
JavaScript正则表达式校验与递归函数实际应用实例解析
2017/08/04 Javascript
详解vue2.0 使用动态组件实现 Tab 标签页切换效果(vue-cli)
2017/08/30 Javascript
vue 使用Jade模板写html,stylus写css的方法
2018/02/23 Javascript
JS实现头条新闻的经典轮播图效果示例
2019/01/30 Javascript
使用Angular自定义字段校验指令的方法示例
2019/02/01 Javascript
一文快速详解前端框架 Vue 最强大的功能
2019/05/21 Javascript
vue路由教程之静态路由
2019/09/03 Javascript
Centos7 安装Node.js10以上版本的方法步骤
2019/10/15 Javascript
vue 导航菜单刷新状态不消失,显示对应的路由界面操作
2020/08/06 Javascript
在vue中实现某一些路由页面隐藏导航栏的功能操作
2020/09/21 Javascript
[01:07:02]DOTA2-DPC中国联赛 正赛 iG vs PSG.LGD BO3 第三场 2月26日
2021/03/11 DOTA
python的多重继承的理解
2017/08/06 Python
python实现猜单词小游戏
2020/05/22 Python
解决Python plt.savefig 保存图片时一片空白的问题
2019/01/10 Python
微信浏览器左上角返回按钮拦截功能
2017/11/21 HTML / CSS
英国领先的在线高尔夫设备零售商:Golfgeardirect
2020/12/11 全球购物
数学检讨书1000字
2014/02/24 职场文书
文明和谐家庭事迹材料
2014/05/18 职场文书
义和团口号
2014/06/17 职场文书
幼儿园六一活动总结
2014/08/27 职场文书
企业安全生产规章制度
2015/08/06 职场文书