Python aiohttp百万并发极限测试实例分析


Posted in Python onOctober 26, 2019

本文实例讲述了Python aiohttp百万并发极限测试。分享给大家供大家参考,具体如下:

本文将测试python aiohttp的极限,同时测试其性能表现,以分钟发起请求数作为指标。大家都知道,当应用到网络操作时,异步的代码表现更优秀,但是验证这个事情,同时搞明白异步到底有多大的优势以及为什么会有这样的优势仍然是一件有趣的事情。为了验证,我将发起1000000请求,用aiohttp客户端。aiohttp每分钟能够发起多少请求?你能预料到哪些异常情况以及崩溃会发生,当你用比较粗糙的脚本去发起如此大量的请求?面对如此大量的请求,哪些主要的陷阱是你需要去思考的?

初识 asyncio/aiohttp

异步编程并不简单。相比平常的同步编程,你需要付出更多的努力在使用回调函数,以事件以及事件处理器的模式进行思考。同时也是因为asyncio相对较新,相关的教程以及博客还很少的缘故。官方文档非常简陋,只有最基本的范例。在我写本文的时候,Stack Overflow上面,只有410个与asyncio相关的话题(相比之下,twisted相关的有2585)。有个别关于asyncio的不错的博客以及文章,比如这个、这个、这个,或者还有这个以及这个。

简单起见,我们先从基础开始 —— 简单HTTP hello world —— 发起GET请求,同时获取一个单独的HTTP响应。

同步模式,你这么做:

import requests
def hello()
   return requests.get("http://httpbin.org/get")
print(hello())

接着我们使用aiohttp:

#!/usr/local/bin/python3.5
import asyncio
from aiohttp import ClientSession
async def hello():
  async with ClientSession() as session:
    async with session.get("http://httpbin.org/headers") as response:
      response = await response.read()
      print(response)
loop = asyncio.get_event_loop()
loop.run_until_complete(hello())

好吧,看上去仅仅一个简单的任务,我写了很多的代码……那里有“async def”、“async with”、“await”—— 看上去让人迷惑,让我们尝试弄懂它们。

你使用async以及await关键字将函数异步化。在hello()中实际上有两个异步操作:首先异步获取相应,然后异步读取响应的内容。

Aiohttp推荐使用ClientSession作为主要的接口发起请求。ClientSession允许在多个请求之间保存cookie以及相关对象信息。Session(会话)在使用完毕之后需要关闭,关闭Session是另一个异步操作,所以每次你都需要使用async with关键字。

一旦你建立了客户端session,你可以用它发起请求。这里是又一个异步操作的开始。上下文管理器的with语句可以保证在处理session的时候,总是能正确的关闭它。

要让你的程序正常的跑起来,你需要将他们加入事件循环中。所以你需要创建一个asyncio loop的实例, 然后将任务加入其中。

看起来有些困难,但是只要你花点时间进行思考与理解,就会有所体会,其实并没有那么复杂。

访问多个链接

现在我们来做些更有意思的事情,顺序访问多个链接。

同步方式如下:

for url in urls:
  print(requests.get(url).text)

很简单。不过异步方式却没有这么容易。所以任何时候你都需要思考,你的处境是否有必要用到异步。如果你的app在同步模式工作的很好,也许你并不需要将之迁移到异步方式。如果你确实需要异步方式,这里会给你一些启示。我们的异步函数hello()还是保持原样,不过我们需要将之包装在asyncio的Future对象中,然后将Future对象列表作为任务传递给事件循环。

loop = asyncio.get_event_loop()
tasks = [] # I'm using test server localhost, but you can use any url
url = "http://localhost:8080/{}"
for i in range(5):
  task = asyncio.ensure_future(hello(url.format(i)))
  tasks.append(task)
loop.run_until_complete(asyncio.wait(tasks))

现在假设我们想获取所有的响应,并将他们保存在同一个列表中。目前,我们没有保存响应内容,仅仅只是打印了他们。让我们返回他们,将之存储在一个列表当中,最后再打印出来。

为了达到这个目的,我们需要修改一下代码:

#!/usr/local/bin/python3.5
import asyncio
from aiohttp import ClientSession
async def fetch(url):
  async with ClientSession() as session:
   async with session.get(url) as response:
     return await response.read()
async def run(loop, r):
  url = "http://localhost:8080/{}"
  tasks = []
  for i in range(r):
    task = asyncio.ensure_future(fetch(url.format(i)))
    tasks.append(task)
    responses = await asyncio.gather(*tasks)
    # you now have all response bodies in this variable
    print(responses)
def print_responses(result):
  print(result)
loop = asyncio.get_event_loop()
future = asyncio.ensure_future(run(loop, 4))
loop.run_until_complete(future)

注意asyncio.gather()的用法,它搜集所有的Future对象,然后等待他们返回。

常见错误

现在我们来模拟真实场景,去调试一些错误,作为演示范例。

看看这个:

# WARNING! BROKEN CODE DO NOT COPY PASTE
async def fetch(url):
  async with ClientSession() as session:
   async with session.get(url) as response:
     return response.read()

如果你对aiohttp或者asyncio不够了解,即使你很熟悉Python,这段代码也不好debug。

上面的代码产生如下输出:

pawel@pawel-VPCEH390X ~/p/l/benchmarker> ./bench.py
[<generator object ClientResponse.read at 0x7fa68d465728>,
 <generator object ClientResponse.read at 0x7fa68cdd9468>,
 <generator object ClientResponse.read at 0x7fa68d4656d0>,
 <generator object ClientResponse.read at 0x7fa68cdd9af0>]

发生了什么?你期待获得响应对象,但是你得到的是一组生成器。怎么会这样?

我之前提到过,response.read()是一个异步操作,这意味着它不会立即返回结果,仅仅返回生成器。这些生成器需要被调用跟运行,但是这并不是默认行为。在Python34中加入的yield from以及Python35中加入的await便是为此而生。它们将迭代这些生成器。以上代码只需要在response.read()前加上await关键字即可修复。如下:

# async operation must be preceded by await
  return await response.read()
  # NOT: return response.read()

我们看看另一个例子。

# WARNING! BROKEN CODE DO NOT COPY PASTE
async def run(loop, r):
  url = "http://localhost:8080/{}"
  tasks = []
  for i in range(r):
    task = asyncio.ensure_future(fetch(url.format(i)))
    tasks.append(task)
    responses = asyncio.gather(*tasks)
    print(responses)

输出结果如下:

pawel@pawel-VPCEH390X ~/p/l/benchmarker> ./bench.py
<_GatheringFuture pending>
Task was destroyed but it is pending!
task: <Task pending coro=<fetch() running at ./bench.py:7>
        wait_for=<Future pending cb=[Task._wakeup()]>
        cb=[gather.<locals>._done_callback(0)()
        at /usr/local/lib/python3.5/asyncio/tasks.py:602]>
Task was destroyed but it is pending!
task: <Task pending coro=<fetch() running at ./bench.py:7>

wait_for=<Future pending cb=[Task._wakeup()]>

cb=[gather.<locals>._done_callback(1)()

at /usr/local/lib/python3.5/asyncio/tasks.py:602]>
Task was destroyed but it is pending!
task: <Task pending coro=<fetch() running at ./bench.py:7>

wait_for=<Future pending cb=[Task._wakeup()]>

cb=[gather.<locals>._done_callback(2)()

at /usr/local/lib/python3.5/asyncio/tasks.py:602]>
Task was destroyed but it is pending!
task: <Task pending coro=<fetch() running at ./bench.py:7>

wait_for=<Future pending cb=[Task._wakeup()]>

cb=[gather.<locals>._done_callback(3)()

at /usr/local/lib/python3.5/asyncio/tasks.py:602]>

发生了什么?查看本地日志,你会发现没有任何请求到达服务器,实际上没有任何请求发生。打印信息首先打印<_Gathering pending>对象,然后警告等待的任务被销毁。又一次的,你忘记了await。

修改

responses = asyncio.gather(*tasks)

responses = await asyncio.gather(*tasks)

即可解决问题。

经验:任何时候,你在等待什么的时候,记得使用await。

同步 vs 异步

重头戏来了。我们来验证异步是否值得(编码麻烦)。看看同步与异步(client)效率上的区别。异步每分钟能够发起多少请求。

为此,我们首先配置一个异步的aiohttp服务器端。这个服务端将获取全部的html文本, 来自Marry Shelley的Frankenstein。在每个响应中,它将添加随机的延时。有的为0,最大值为3s。类似真正的app。有些app的响应延时为固定值,一般而言,每个响应的延时是不同的。

服务器代码如下:

#!/usr/local/bin/python3.5
import asyncio
from datetime import datetime
from aiohttp import web
import random
# set seed to ensure async and sync client get same distribution of delay values
# and tests are fair random.seed(1)
async def hello(request):
  name = request.match_info.get("name", "foo")
  n = datetime.now().isoformat()
  delay = random.randint(0, 3)
  await asyncio.sleep(delay)
  headers = {"content_type": "text/html", "delay": str(delay)}
  # opening file is not async here, so it may block, to improve
  # efficiency of this you can consider using asyncio Executors
  # that will delegate file operation to separate thread or process
  # and improve performance
  # https://docs.python.org/3/library/asyncio-eventloop.html#executor
  # https://pymotw.com/3/asyncio/executors.html
  with open("frank.html", "rb") as html_body:
     print("{}: {} delay: {}".format(n, request.path, delay))
     response = web.Response(body=html_body.read(), headers=headers)
     return response
app = web.Application()
app.router.add_route("GET", "/{name}", hello)
web.run_app(app)

同步客户端代码如下:

import requests
r = 100
url = "http://localhost:8080/{}"
for i in range(r):
  res = requests.get(url.format(i))
 delay = res.headers.get("DELAY")
 d = res.headers.get("DATE")
 print("{}:{} delay {}".format(d, res.url, delay))

在我的机器上,上面的代码耗时2分45s。而异步代码只需要3.48s。

有趣的是,异步代码耗时无限接近最长的延时(server的配置)。如果你观察打印信息,你会发现异步客户端的优势有多么巨大。有的响应为0延迟,有的为3s。同步模式下,客户端会阻塞、等待,你的机器什么都不做。异步客户端不会浪费时间,当有延迟发生时,它将去做其他的事情。在日志中,你也会发现这个现象。首先是0延迟的响应,然后当它们到达后,你将看到1s的延迟,最后是最大延迟的响应。

极限测试

现在我们知道异步表现更好,让我们尝试去找到它的极限,同时尝试让它崩溃。我将发送1000异步请求。我很好奇我的客户端能够处理多少数量的请求。

> time python3 bench.py
2.68user 0.24system 0:07.14elapsed 40%CPU
(0avgtext+0avgdata 53704maxresident)
k 0inputs+0outputs (0major+14156minor)pagefaults 0swaps

1000个请求,花费了7s。相当不错的成绩。然后10K呢?很不幸,失败了:

responses are <_GatheringFuture finished exception=

ClientOSError(24, 'Cannot connect to host localhost:8080 ssl:

False [Can not connect to localhost:8080 [Too many open files]]')>
Traceback (most recent call last):
   File "/home/pawel/.local/lib/python3.5/site-packages/aiohttp/connector.py", line 581, in _create_connection
   File "/usr/local/lib/python3.5/asyncio/base_events.py", line 651, in create_connection
   File "/usr/local/lib/python3.5/asyncio/base_events.py", line 618, in create_connection
   File "/usr/local/lib/python3.5/socket.py", line 134, in __init__ OS
   Error: [Errno 24] Too many open files

这样不大好,貌似我倒在了10K connections problem面前。

traceback显示,open files太多了,可能代表着open sockets太多。为什么叫文件?Sockets(套接字)仅仅是文件描述符,操作系统有数量的限制。多少才叫太多呢?我查看Python源码,然后发现这个值为1024.怎么样绕过这个问题?一个粗暴的办法是增加这个数值,但是听起来并不高明。更好的办法是,加入一些同步机制,限制并发数量。于是我在asyncio.Semaphore()中加入最大任务限制为1000.

修改客户端代码如下:

# modified fetch function with semaphore
import random
import asyncio
from aiohttp import ClientSession
async def fetch(url):
  async with ClientSession() as session:
    async with session.get(url) as response:
     delay = response.headers.get("DELAY")
     date = response.headers.get("DATE")
     print("{}:{} with delay {}".format(date, response.url, delay))
     return await response.read()
async def bound_fetch(sem, url):
  # getter function with semaphore
  async with sem:
   await fetch(url)
  async def run(loop, r):
   url = "http://localhost:8080/{}"
   tasks = []
   # create instance of Semaphore
   sem = asyncio.Semaphore(1000)
   for i in range(r):
     # pass Semaphore to every GET request
     task = asyncio.ensure_future(bound_fetch(sem, url.format(i)))
     tasks.append(task)
     responses = asyncio.gather(*tasks)
await responses number = 10000
loop = asyncio.get_event_loop()
future = asyncio.ensure_future(run(loop, number))
loop.run_until_complete(future)

现在,我们可以处理10k链接了。这花去我们23s,同时返回了一些异常。不过不管怎样,相当不错的表现。

那100K呢?这个任务让我的机器很吃力,不过惊奇的是,它工作的很好。服务器的表现相当稳定,虽然内存占用很高,然后cpu占用一直维持在100%左右。让我觉得有趣的是,服务器占用的cpu明显小于client。这是ps的回显:

pawel@pawel-VPCEH390X ~/p/l/benchmarker> ps ua | grep python
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
pawel     2447 56.3  1.0 216124 64976 pts/9    Sl+  21:26   1:27 /usr/local/bin/python3.5 ./test_server.py
pawel     2527  101  3.5 674732 212076 pts/0   Rl+  21:26   2:30 /usr/local/bin/python3.5 ./bench.py

最终因为某些原因,运行5分钟过后,它崩溃了。它生成了接近100K行的输出,所以很难定位traceback,好像某些响应没有正常关闭。具体原因不太确定。(client or server error)

一段时间的滚动以后,我找到了这个异常,在client日志中。

  File "/usr/local/lib/python3.5/asyncio/futures.py", line 387, in __iter__
      return self.result()  # May raise too.
  File "/usr/local/lib/python3.5/asyncio/futures.py", line 274, in result
       raise self._exception
  File "/usr/local/lib/python3.5/asyncio/selector_events.py", line 411, in _sock_connect
       sock.connect(address) OS
  Error: [Errno 99] Cannot assign requested address

我不太确定这里发生了什么。我初始的猜测是测试服务器挂掉了。一个读者提出:这个异常的发生原因是操作系统的可用端口耗尽。之前我限制了并发连接数最大为1k,可能有些sockets仍然处在closing状态,系统内核无法使用才导致这个问题。

已经很不错了,不是吗?100k耗时5分钟。相当于一分钟20k请求数。

最后我尝试1M连接数。我真怕我的笔记本因为这个爆炸^_^.我特意将延迟降低为0到1s之间。最终耗时52分钟。

1913.06user 1196.09system 52:06.87elapsed 99%CPU
(0avgtext+0avgdata 5194260maxresident)k 265144
inputs+0outputs (18692major+2528207minor)
pagefaults 0swaps

这意味着,我们的客户端每分钟发送了19230次请求。还不错吧?注意客户端的性能被服务器限制了,好像服务器端崩溃了好几次。

最后

如你所见,异步HTTP客户端相当强大。发起1M请求不是那么困难,同时相比同步模式,优势巨大。

我好奇对比其他的语言或者异步框架,其表现如何?可能在以后某个时候,我将对比Twisted Treq跟aiohttp。然后,其他的异步库(其他语言)能够支持到多少并发?比如:某些Java 异步框架?或者C++框架?或者某些Rust HTTP客户端?

希望本文所述对大家Python程序设计有所帮助。

Python 相关文章推荐
Python linecache.getline()读取文件中特定一行的脚本
Sep 06 Python
下载安装setuptool和pip linux安装pip    
Jan 24 Python
Django框架中处理URLconf中特定的URL的方法
Jul 20 Python
python3+PyQt5 使用三种不同的简便项窗口部件显示数据的方法
Jun 17 Python
python在openstreetmap地图上绘制路线图的实现
Jul 11 Python
Python 分发包中添加额外文件的方法
Aug 16 Python
如何使用python进行pdf文件分割
Nov 11 Python
Python爬取365好书中小说代码实例
Feb 28 Python
Django之choices选项和富文本编辑器的使用详解
Apr 01 Python
python代码能做成软件吗
Jul 24 Python
详解Flask开发技巧之异常处理
Jun 15 Python
python数据分析之单因素分析线性拟合及地理编码
Jun 25 Python
python实现淘宝购物系统
Oct 25 #Python
DJANGO-URL反向解析REVERSE实例讲解
Oct 25 #Python
python计算n的阶乘的方法代码
Oct 25 #Python
Python OrderedDict的使用案例解析
Oct 25 #Python
python编写计算器功能
Oct 25 #Python
Python实现快速排序的方法详解
Oct 25 #Python
python实现超市商品销售管理系统
Oct 25 #Python
You might like
php删除数组元素示例分享
2014/02/17 PHP
ThinkPHP中url隐藏入口文件后接收alipay传值的方法
2014/12/09 PHP
CodeIgniter扩展核心类实例详解
2016/01/20 PHP
PHP基于openssl实现的非对称加密操作示例
2019/01/11 PHP
PHP Swoole异步MySQL客户端实现方法示例
2019/10/24 PHP
php的RSA加密解密算法原理与用法分析
2020/01/23 PHP
js实现杯子倒水问题自动求解程序
2013/03/25 Javascript
原生js实现查找/添加/删除/指定元素的class
2013/04/12 Javascript
javascript中字符串的定义示例代码
2013/12/19 Javascript
jquery中append()与appendto()用法分析
2014/11/14 Javascript
简述JavaScript中正则表达式的使用方法
2015/06/15 Javascript
jquery实现适用于门户站的导航下拉菜单效果代码
2015/08/24 Javascript
AngularJS使用angular-formly进行表单验证
2015/12/27 Javascript
让html元素随浏览器的大小自适应垂直居中的实现方法
2016/10/12 Javascript
js中删除数组中的某一元素实例(无下标时)
2017/02/28 Javascript
react 原生实现头像滚动播放的示例
2020/04/21 Javascript
Element Carousel 走马灯的具体实现
2020/07/26 Javascript
让python的Cookie.py模块支持冒号做key的方法
2010/12/28 Python
Python配置文件解析模块ConfigParser使用实例
2015/04/13 Python
Python实现判断一个整数是否为回文数算法示例
2019/03/02 Python
Python基本类型的连接组合和互相转换方式(13种)
2019/12/16 Python
django 读取图片到页面实例
2020/03/27 Python
Python定义函数实现累计求和操作
2020/05/03 Python
Python unittest如何生成HTMLTestRunner模块
2020/09/08 Python
世界各地的当地人的食物体验:Eatwith
2019/07/26 全球购物
俄罗斯第一家篮球店:StreetBall
2020/07/30 全球购物
波兰办公用品和学校用品在线商店:Dlabiura24.pl
2020/11/18 全球购物
金山毒霸系列的笔试题
2013/04/13 面试题
感恩的演讲稿
2014/05/06 职场文书
运动会广播稿150字(9篇)
2014/09/20 职场文书
东京审判观后感
2015/06/01 职场文书
推普标语口号大全
2015/12/26 职场文书
2016教师读书思廉心得体会
2016/01/23 职场文书
Nest.js参数校验和自定义返回数据格式详解
2021/03/29 Javascript
springboot+VUE实现登录注册
2021/05/27 Vue.js
详细谈谈MYSQL中的COLLATE是什么
2021/06/11 MySQL