Python asyncio vs nginx performance
While I was playing with Python asyncio I got interested in how well it performs serving data over TLS compared to Nginx. So I implemented a small HTTPS server with asyncio:
import asyncio import ssl import random import string import uvloop content_20k = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(20 * 1024)) resp_lines = [ 'HTTP/1.1 200 OK', 'Connection: close', '', '' ] resp_20k = bytes('\r\n'.join(resp_lines) + content_20k, 'latin-1') class HttpServerProtocol(asyncio.Protocol): def __init__(self) -> None: super().__init__() self._transport = None def request_received(self) -> None: self._transport.write(bytes(resp_20k)) self._transport.close() def data_received(self, data: bytes) -> None: # Let's assume request is complete. self.request_received() def connection_made(self, transport) -> None: self._transport = transport def close(self): self._transport.close() async def start_https_server(loop): tls_ctx = make_tls_context() return await loop.create_server( lambda: asyncio.sslproto.SSLProtocol( loop, HttpServerProtocol(), tls_ctx, None, server_side=True), '0.0.0.0', 8082, backlog=65535 ) def make_tls_context(): ctx = ssl.SSLContext(ssl.PROTOCOL_TLS) ctx.load_cert_chain( certfile='example.com.crt', keyfile='https.key.pem', ) return ctx def main(): asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) loop = asyncio.new_event_loop() loop.run_until_complete(start_https_server(loop)) loop.run_forever() loop.close() if __name__ == '__main__': main()
All it does is simply serves a static 20k content over TLS. I configure nginx to do the same.
Then I ran a small benchmark with httpmeter. I am doing one request per connection as I am mainly interested in TLS performance. Here's the results for asyncio based server:
$ httpmeter -c 1000 -n 3000 -H 'Connection: close' https://192.168.2.222:8082/ Concurrency Level: 1000 Completed Requests: 3000 Requests Per Second: 544.186120 [#/sec] (mean) Request Durations: [min: 0.147766, avg: 1.016474, max: 1.847824] seconds Document Length: [min: 20480, avg: 20480.000000, max: 20480] bytes Status codes: 200 3000
And here's for nginx:
$ httpmeter -c 1000 -n 3000 -H 'Connection: close' https://192.168.2.222:8443/20k Concurrency Level: 1000 Completed Requests: 3000 Requests Per Second: 1210.957884 [#/sec] (mean) Request Durations: [min: 0.068251, avg: 0.454704, max: 0.834079] seconds Document Length: [min: 20480, avg: 20480.000000, max: 20480] bytes Status codes: 200 3000
Both nginx and python server loaded up to 100% single CPU core.
Basically nginx performed 2x better than asyncio.
I profiled the asyncio server:
$ pyenv/bin/python server.py 361637 function calls (361625 primitive calls) in 9.351 seconds Ordered by: internal time ncalls tottime percall cumtime percall filename:lineno(function) 9000 4.770 0.001 4.770 0.001 {method 'do_handshake' of '_ssl._SSLSocket' objects} 1 4.088 4.088 9.349 9.349 {method 'run_forever' of 'uvloop.loop.Loop' objects} 15000 0.076 0.000 4.975 0.000 sslproto.py:169(feed_ssldata) 3000 0.068 0.000 0.068 0.000 {method 'write' of '_ssl._SSLSocket' objects} 9000 0.045 0.000 0.045 0.000 {method 'read' of '_ssl._SSLSocket' objects} 12000 0.033 0.000 0.221 0.000 sslproto.py:624(_process_write_backlog) 3000 0.024 0.000 0.025 0.000 sslproto.py:413(__init__) 9000 0.018 0.000 5.107 0.001 sslproto.py:497(data_received) 3000 0.017 0.000 0.035 0.000 sslproto.py:572(_on_handshake_complete) 3000 0.015 0.000 0.015 0.000 {method '_wrap_bio' of '_ssl._SSLContext' objects} 12000 0.013 0.000 0.013 0.000 {method 'read' of '_ssl.MemoryBIO' objects} 3000 0.012 0.000 0.012 0.000 {method 'shutdown' of '_ssl._SSLSocket' objects} 3000 0.012 0.000 0.091 0.000 sslproto.py:243(feed_appdata) 9000 0.010 0.000 4.782 0.001 ssl.py:681(do_handshake) 3000 0.009 0.000 0.019 0.000 sslproto.py:460(connection_made) 12000 0.008 0.000 0.008 0.000 {method 'write' of 'uvloop.loop.UVStream' objects} 6000 0.007 0.000 0.155 0.000 sslproto.py:555(_write_appdata) 9000 0.007 0.000 0.007 0.000 {method 'write' of '_ssl.MemoryBIO' objects} 3000 0.006 0.000 0.061 0.000 sslproto.py:118(do_handshake) 3000 0.006 0.000 0.168 0.000 server.py:25(request_received) 3000 0.006 0.000 0.008 0.000 sslproto.py:521(eof_received) 48045 0.005 0.000 0.005 0.000 {built-in method builtins.len} 3000 0.005 0.000 0.005 0.000 sslproto.py:68(__init__) 3000 0.005 0.000 0.034 0.000 server.py:43(<lambda>) 9000 0.005 0.000 0.050 0.000 ssl.py:618(read) 9000 0.005 0.000 0.005 0.000 {method 'call_soon' of 'uvloop.loop.Loop' objects} 15018 0.005 0.000 0.005 0.000 {built-in method builtins.getattr} 3000 0.004 0.000 0.009 0.000 weakref.py:155(__setitem__) 3000 0.004 0.000 0.030 0.000 sslproto.py:139(shutdown) 3000 0.004 0.000 0.021 0.000 ssl.py:403(wrap_bio) 3000 0.004 0.000 0.006 0.000 sslproto.py:471(connection_lost) 3000 0.003 0.000 0.114 0.000 sslproto.py:379(write) 3000 0.003 0.000 0.003 0.000 server.py:21(__init__) 3000 0.003 0.000 0.005 0.000 sslproto.py:560(_start_handshake) 3000 0.003 0.000 0.003 0.000 {method 'cipher' of '_ssl._SSLSocket' objects} 3000 0.003 0.000 0.003 0.000 {method 'update' of 'dict' objects} 15028 0.002 0.000 0.002 0.000 {method 'append' of 'list' objects} 3000 0.002 0.000 0.004 0.000 ssl.py:638(getpeercert) 3000 0.002 0.000 0.002 0.000 weakref.py:310(__init__) 3000 0.002 0.000 0.002 0.000 ssl.py:577(__init__) 3000 0.002 0.000 0.048 0.000 sslproto.py:317(close) 3000 0.002 0.000 0.002 0.000 {method 'peer_certificate' of '_ssl._SSLSocket' objects} 3000 0.002 0.000 0.014 0.000 ssl.py:690(unwrap) 9000 0.002 0.000 0.002 0.000 sslproto.py:450(_wakeup_waiter) 3000 0.002 0.000 0.046 0.000 sslproto.py:549(_start_shutdown) 3000 0.001 0.000 0.003 0.000 weakref.py:305(__new__) 3000 0.001 0.000 0.001 0.000 {method 'close' of 'uvloop.loop.UVBaseTransport' objects} 3000 0.001 0.000 0.001 0.000 ssl.py:584(context) 3000 0.001 0.000 0.170 0.000 server.py:29(data_received) 1454 0.001 0.000 0.001 0.000 weakref.py:108(remove) 3002 0.001 0.000 0.001 0.000 {built-in method __new__ of type object at 0x88d200} 3000 0.001 0.000 0.069 0.000 ssl.py:630(write) 3000 0.001 0.000 0.001 0.000 sslproto.py:297(__init__) 9000 0.001 0.000 0.001 0.000 {method 'get_debug' of 'uvloop.loop.Loop' objects} 3000 0.001 0.000 0.004 0.000 ssl.py:661(cipher) 3000 0.001 0.000 0.002 0.000 ssl.py:672(compression) 9000 0.001 0.000 0.001 0.000 {method 'append' of 'collections.deque' objects} 3000 0.001 0.000 0.001 0.000 server.py:33(connection_made) 3018 0.001 0.000 0.001 0.000 {built-in method builtins.hasattr} 3022 0.001 0.000 0.001 0.000 {built-in method builtins.isinstance} 3000 0.001 0.000 0.001 0.000 sslproto.py:95(ssl_object) 3000 0.000 0.000 0.000 0.000 {method 'compression' of '_ssl._SSLSocket' objects} 3000 0.000 0.000 0.000 0.000 protocols.py:94(eof_received) 2 0.000 0.000 0.000 0.000 sre_compile.py:250(_optimize_charset) 3000 0.000 0.000 0.000 0.000 protocols.py:25(connection_lost) 2397 0.000 0.000 0.000 0.000 sslproto.py:332(__del__)
It turns out that most of the time is spent doing TLS handshake. This is weird because I expected TLS to perform similar to nginx because python is only wrapping OpenSSL C implementation.
One of the possible improvements that nginx could have is TLS session resumption. But it's not possible in this case because I used httpmeter which is implemented in python. And by the time I ran benchmarks, python TLS API did not support TLS sessions (https://bugs.python.org/issue19500).
So I need to do more research to better understand nginx over python asyncio TLS performance.
Comments