构建高效的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通过PIL获取图片主要颜色并和颜色库进行对比的方法
Mar 19 Python
Python运用于数据分析的简单教程
Mar 27 Python
python实现域名系统(DNS)正向查询的方法
Apr 19 Python
Python 数据结构之队列的实现
Jan 22 Python
详解Python实现多进程异步事件驱动引擎
Aug 25 Python
Python快速查找list中相同部分的方法
Jun 27 Python
pymongo中聚合查询的使用方法
Mar 22 Python
python机器学习包mlxtend的安装和配置详解
Aug 21 Python
Python实现打印实心和空心菱形
Nov 23 Python
matlab中二维插值函数interp2的使用详解
Apr 22 Python
Python requests.post方法中data与json参数区别详解
Apr 30 Python
使用已经得到的keras模型识别自己手写的数字方式
Jun 29 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+DBM的同学录程序(3)
2006/10/09 PHP
php实现word转html的方法
2016/01/22 PHP
使用PHP连接数据库_实现用户数据的增删改查的整体操作示例
2017/09/01 PHP
ie 调试javascript的工具
2009/04/29 Javascript
GWT中复制到剪贴板 js+flash实现复制 兼容性比较好
2010/03/07 Javascript
Javascript 中的 && 和 || 使用小结
2010/04/25 Javascript
轻量级 JS ToolTip提示效果
2010/07/20 Javascript
extjs render 用法介绍
2013/09/11 Javascript
event对象获取方法总结在google浏览器下测试
2013/11/03 Javascript
判断某个字符在一个字符串中是否存在的js代码
2014/02/28 Javascript
jquery查找tr td 示例模拟
2014/05/08 Javascript
JavaScript中length属性的使用方法
2015/06/05 Javascript
分享网页检测摇一摇实例代码
2016/01/14 Javascript
JavaScript实现99乘法表及隔行变色实例代码
2016/02/24 Javascript
JavaScript中的toString()和toLocaleString()方法的区别
2017/02/15 Javascript
Bootstrap标签页(Tab)插件使用方法
2017/03/21 Javascript
React Native基础入门之初步使用Flexbox布局
2018/07/02 Javascript
NodeJS 实现多语言的示例代码
2018/09/11 NodeJs
JS中超越现实的匿名函数用法实例分析
2019/06/21 Javascript
ant design vue 表格table 默认勾选几项的操作
2020/10/31 Javascript
vue打包通过image-webpack-loader插件对图片压缩优化操作
2020/11/12 Javascript
[02:36]DOTA2-DPC中国联赛 正赛 PSG.LGD vs Magma 选手采访
2021/03/11 DOTA
python用字典统计单词或汉字词个数示例
2014/04/22 Python
Python自动化运维_文件内容差异对比分析
2017/12/13 Python
pytorch中torch.max和Tensor.view函数用法详解
2020/01/03 Python
Python操作Sqlite正确实现方法解析
2020/02/05 Python
python GUI库图形界面开发之PyQt5简单绘图板实例与代码分析
2020/03/08 Python
pycharm永久激活超详细教程
2020/10/29 Python
英国健身超市:Fitness Superstore
2019/06/17 全球购物
JSF面试题:如何管量web层中的Bean,用什么标签。如何通过jsp页面与Bean绑定在一起进行处理?
2012/10/05 面试题
授权委托书范文
2014/07/31 职场文书
学生党员批评与自我批评
2014/10/15 职场文书
2015年收银工作总结范文
2015/04/01 职场文书
幼儿园小班教师随笔
2015/08/14 职场文书
JavaScript 定时器详情
2021/11/11 Javascript
mysql函数之截取字符串的实现
2022/08/14 MySQL