Skip to content

Instantly share code, notes, and snippets.

@nottrobin
Last active August 12, 2019 21:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nottrobin/39c9390f78c602eb4ddfe91d066d8817 to your computer and use it in GitHub Desktop.
Save nottrobin/39c9390f78c602eb4ddfe91d066d8817 to your computer and use it in GitHub Desktop.
Compare time for concurrent requests in aiohttp vs grequests
#! /usr/bin/env python3
import asyncio
import grequests
from aiohttp import ClientSession
from timeit import default_timer
# aiohttp concurrent requests code from:
# https://gist.github.com/Den1al/2ede0c38fa4bc486d1791d86bcf9034e
# Except that I've removed the timeout stuff he added
# (oddly, removing the timeout code seemed to make a big difference to aiohttp's performance)
async def fetch_all(urls: list):
""" Fetch all URLs """
tasks = []
async with ClientSession() as session:
for url in urls:
tasks.append(asyncio.ensure_future(fetch(url, session)))
await asyncio.gather(*tasks)
async def fetch(url: str, session: object):
""" Fetch a single URL """
async with session.get(url) as response:
return await response.read()
def main():
urls = [
'https://twitter.com',
'https://9to5mac.com',
'https://amazon.com',
'https://ubuntu.com',
'https://development.robinwinslow.co.uk',
'https://github.com',
'https://canonical.com',
'https://microk8s.io',
'https://mir-server.io',
]
start_time = default_timer()
loop = asyncio.get_event_loop()
total_future = asyncio.ensure_future(fetch_all(urls))
loop.run_until_complete(total_future)
total_elapsed = default_timer() - start_time
gstart_time = default_timer()
request_set = []
for url in urls:
request_set.append(grequests.get(url))
grequests.map(request_set)
gtotal_elapsed = default_timer() - gstart_time
print(
f'asyncio time: {total_elapsed:5.2f}\n'
f'grequests time: {gtotal_elapsed:5.2f}'
)
if __name__ == '__main__':
main()
# If I run this a few times, we can see that both perform similarly,
# with aiohttp maybe having the slight edge
$ ./concurrent_requests.py
asyncio time: 0.60
grequests time: 0.63
$ ./concurrent_requests.py
asyncio time: 0.54
grequests time: 0.63
$ ./concurrent_requests.py
asyncio time: 0.53
grequests time: 1.40
$ ./concurrent_requests.py
asyncio time: 0.59
grequests time: 0.61
$ ./concurrent_requests.py
asyncio time: 0.57
grequests time: 0.62
$ ./concurrent_requests.py
asyncio time: 0.61
grequests time: 0.64
$ ./concurrent_requests.py
asyncio time: 0.55
grequests time: 0.66
$ ./concurrent_requests.py
asyncio time: 0.55
grequests time: 0.70
$ ./concurrent_requests.py
asyncio time: 0.57
grequests time: 0.61
$ ./concurrent_requests.py
asyncio time: 0.54
grequests time: 0.72
$ ./concurrent_requests.py
asyncio time: 0.54
grequests time: 0.63
$ ./concurrent_requests.py
asyncio time: 0.58
grequests time: 0.68
$ ./concurrent_requests.py
asyncio time: 0.67
grequests time: 0.66
#! /usr/bin/env python3
"""
Let's try making grequests in a Flask app
"""
import grequests
from timeit import default_timer
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
urls = [
"https://twitter.com",
"https://9to5mac.com",
"https://amazon.com",
"https://ubuntu.com",
"https://ubuntu.com/blog",
"https://development.robinwinslow.co.uk",
"https://github.com",
"https://canonical.com",
"https://microk8s.io",
"https://mir-server.io",
]
gstart_time = default_timer()
rs = (grequests.get(u) for u in urls)
grequests.map(rs)
gtotal_elapsed = default_timer() - gstart_time
return (
f"grequests time: {gtotal_elapsed:5.2f}",
{"content-type": "text/plain"},
)
if __name__ == "__main__":
app.run()
# This seems to work quite well, and yields similar times:
$ ./concurrent_grequests_app.py &
[1] 21741
* Serving Flask app "concurrent_grequests_app" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
$ curl localhost:5000
127.0.0.1 - - [09/Aug/2019 12:33:33] "GET / HTTP/1.1" 200 -
grequests time: 0.71
$ curl localhost:5000
127.0.0.1 - - [09/Aug/2019 12:33:36] "GET / HTTP/1.1" 200 -
grequests time: 0.61
$ curl localhost:5000
127.0.0.1 - - [09/Aug/2019 12:33:38] "GET / HTTP/1.1" 200 -
grequests time: 0.65
$ curl localhost:5000
127.0.0.1 - - [09/Aug/2019 12:33:39] "GET / HTTP/1.1" 200 -
grequests time: 0.70
$ curl localhost:5000
127.0.0.1 - - [09/Aug/2019 12:33:41] "GET / HTTP/1.1" 200 -
grequests time: 0.72
$ curl localhost:5000
127.0.0.1 - - [09/Aug/2019 12:33:42] "GET / HTTP/1.1" 200 -
grequests time: 0.62
$ curl localhost:5000
127.0.0.1 - - [09/Aug/2019 12:33:43] "GET / HTTP/1.1" 200 -
grequests time: 0.63
$ curl localhost:5000
127.0.0.1 - - [09/Aug/2019 12:33:44] "GET / HTTP/1.1" 200 -
grequests time: 0.63
$ curl localhost:5000
127.0.0.1 - - [09/Aug/2019 12:33:46] "GET / HTTP/1.1" 200 -
grequests time: 1.22
$ curl localhost:5000
127.0.0.1 - - [09/Aug/2019 12:33:49] "GET / HTTP/1.1" 200 -
grequests time: 0.61
$ curl localhost:5000
127.0.0.1 - - [09/Aug/2019 12:33:50] "GET / HTTP/1.1" 200 -
grequests time: 0.75
#! /usr/bin/env python3
"""
Now let's try making concurrent aiohttp requests in a Flask app
"""
#! /usr/bin/env python3
import asyncio
from timeit import default_timer
from flask import Flask
app = Flask(__name__)
# aiohttp concurrent requests code from:
# https://gist.github.com/Den1al/2ede0c38fa4bc486d1791d86bcf9034e
# Except that I've removed the timeout stuff he added
async def fetch_all(urls: list):
""" Fetch all URLs """
tasks = []
async with ClientSession() as session:
for url in urls:
tasks.append(asyncio.ensure_future(fetch(url, session)))
await asyncio.gather(*tasks)
async def fetch(url: str, session: object):
""" Fetch a single URL """
async with session.get(url) as response:
return await response.read()
@app.route("/")
def index():
urls = [
'https://twitter.com',
'https://9to5mac.com',
'https://amazon.com',
'https://ubuntu.com',
'https://development.robinwinslow.co.uk',
'https://github.com',
'https://canonical.com',
'https://microk8s.io',
'https://mir-server.io',
]
start_time = default_timer()
loop = asyncio.get_event_loop()
total_future = asyncio.ensure_future(fetch_all(urls))
loop.run_until_complete(total_future)
total_elapsed = default_timer() - start_time
return (
f"aiohttp time: {total_elapsed:5.2f}",
{"content-type": "text/plain"},
)
if __name__ == "__main__":
app.run()
# But now we get a threading error:
$ ./concurrent_aiohttp_app.py &
[1] 6314
* Serving Flask app "concurrent_aiohttp_app" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
$ curl localhost:5000
[2019-08-09 12:37:33,276] ERROR in app: Exception on / [GET]
Traceback (most recent call last):
File "/home/robin/Projects/canonicalwebteam.blog/concurrentcy_env/lib/python3.6/site-packages/flask/app.py", line 2446, in wsgi_app
response = self.full_dispatch_request()
File "/home/robin/Projects/canonicalwebteam.blog/concurrentcy_env/lib/python3.6/site-packages/flask/app.py", line 1951, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/home/robin/Projects/canonicalwebteam.blog/concurrentcy_env/lib/python3.6/site-packages/flask/app.py", line 1820, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "/home/robin/Projects/canonicalwebteam.blog/concurrentcy_env/lib/python3.6/site-packages/flask/_compat.py", line 39, in reraise
raise value
File "/home/robin/Projects/canonicalwebteam.blog/concurrentcy_env/lib/python3.6/site-packages/flask/app.py", line 1949, in full_dispatch_request
rv = self.dispatch_request()
File "/home/robin/Projects/canonicalwebteam.blog/concurrentcy_env/lib/python3.6/site-packages/flask/app.py", line 1935, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "./concurrent_aiohttp_app.py", line 47, in index
loop = asyncio.get_event_loop()
File "/usr/lib/python3.6/asyncio/events.py", line 694, in get_event_loop
return get_event_loop_policy().get_event_loop()
File "/usr/lib/python3.6/asyncio/events.py", line 602, in get_event_loop
% threading.current_thread().name)
RuntimeError: There is no current event loop in thread 'Thread-1'.
127.0.0.1 - - [09/Aug/2019 12:37:33] "GET / HTTP/1.1" 500 -
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>

It looks to me like aiohttp and grequests both perform similarly for making concurrent HTTP requests. It may be possible to tweak aiohttp to be slightly quicker, but grequests is quite a lot more straightforward to use.

aiohttp doesn't appear to work trivially with Flask, because Flask is a synchronous rather than an async framework. I encountered the same error that other people have mentioned when trying to use async code in Flask.

I did find https://github.com/Hardtack/Flask-aiohttp/, but it's "Experimental". I tried to use it to solve this problem, but the documentation doesn't work out of the box at all, so I ultimately gave up.

I think this means we should simply use grequests for now, unless we seriously want to consider switching to using an async HTTP framework, which seems quite radical.

"""
This Flask app compares 3 options:
- Consecutive requests with requests
- Concurrent requests with grequests
- Concurrent requests with aiohttp
It then runs each scenario 6 times and averages them
"""
from flask import Flask
import grequests
import requests
import aiohttp
import asyncio
import time
app = Flask(__name__)
urls = [
"https://admin.insights.ubuntu.com/2018/11/09/how-to-harness-big-data-business-value",
"https://admin.insights.ubuntu.com/2018/11/21/ubuntu-security-compliance",
"https://admin.insights.ubuntu.com/2019/02/11/understanding-containerised-workloads-for-telco",
"https://admin.insights.ubuntu.com/2019/08/06/ubuntu-server-development-summary-06-august-2019/",
"https://admin.insights.ubuntu.com/2019/08/07/creating-a-ros-2-cli-command-and-verb",
"https://admin.insights.ubuntu.com/2019/08/08/slow-snap-trace-exec-to-the-rescue/",
"https://admin.insights.ubuntu.com/2019/08/09/enhanced-livepatch-desktop-integration-available-with-ubuntu-18-04-3-lts",
"https://admin.insights.ubuntu.com/2019/08/12/issue-2019-08-12-the-kubeflow-machine-learning-toolkit/",
"https://admin.insights.ubuntu.com/2019/08/12/julia-and-jeff-discover-the-ease-of-snaps-at-the-snapcraft-summit",
]
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def fetch_all():
async with aiohttp.ClientSession() as session:
queue = []
for url in urls:
queue.append(fetch(session, url))
return await asyncio.gather(*queue)
def requests_time():
start = time.time()
for url in urls:
requests.get(url)
return time.time() - start
def grequests_time():
queue = []
for url in urls:
queue.append(grequests.get(url))
start = time.time()
grequests.map(queue)
return time.time() - start
def aiohttp_time():
loop = asyncio.new_event_loop()
async_start = time.time()
loop.run_until_complete(fetch_all())
loop.close()
return time.time() - async_start
@app.route("/fetch")
def fetch_urls():
average_requests = (
requests_time()
+ requests_time()
+ requests_time()
+ requests_time()
+ requests_time()
+ requests_time()
) / 6
average_aiohttp = (
aiohttp_time()
+ aiohttp_time()
+ aiohttp_time()
+ aiohttp_time()
+ aiohttp_time()
+ aiohttp_time()
) / 6
average_grequests = (
grequests_time()
+ grequests_time()
+ grequests_time()
+ grequests_time()
+ grequests_time()
+ grequests_time()
) / 6
return (
f"Requests: {average_requests}\naiohttp: {average_aiohttp}\ngrequests: {average_grequests}",
{"content-type": "text/plain"},
)
5 times with grequests first:
Requests: 6.331809600194295
grequests: 1.0391256014506023
aiohttp: 0.9352189699808756
Requests: 9.039314468701681
grequests: 3.3131030797958374
aiohttp: 1.2752037048339844
Requests: 8.931830803553263
grequests: 1.1890474160512288
aiohttp: 0.9405452807744344
Requests: 7.337289134661357
grequests: 0.8972634871800741
aiohttp: 0.9521408875783285
Requests: 6.777968366940816
grequests: 1.8296988407770793
aiohttp: 0.9191683928171793
5 times with aiohttp first:
Requests: 7.394062479337056
aiohttp: 0.906674305597941
grequests: 0.8309899171193441
Requests: 6.248002092043559
aiohttp: 1.6101177136103313
grequests: 1.4184436003367107
Requests: 5.999986092249553
aiohttp: 1.1145278215408325
grequests: 0.8567223151524862
Requests: 6.519141475359599
aiohttp: 0.7730963627497355
grequests: 0.8148366212844849
Requests: 6.2029545704523725
aiohttp: 0.7818789482116699
grequests: 0.9571941693623861
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment