Last active
October 18, 2016 02:22
-
-
Save whiteinge/9489f0dccd55b61665d5 to your computer and use it in GitHub Desktop.
Tornado worker threads plus SSE streams plus reading/writing to sqlite; this is a benchmarking POC and not useful in production!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Only required on Python 2 | |
| futures==3.0.3 | |
| tornado==4.3 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python | |
| import time | |
| import os | |
| import signal | |
| import sqlite3 | |
| import threading | |
| import tornado | |
| import tornado.concurrent | |
| import tornado.gen | |
| import tornado.httpserver | |
| import tornado.ioloop | |
| import tornado.queues | |
| import tornado.web | |
| from tornado.iostream import StreamClosedError | |
| from concurrent.futures import ThreadPoolExecutor | |
| DB_FILE = 'data.db' | |
| MAX_WORKERS = 10 | |
| def init_db(db_file): | |
| ''' | |
| Delete and recreate an empty db | |
| ''' | |
| if os.path.exists(db_file): | |
| os.remove(db_file) | |
| conn = sqlite3.connect(db_file) | |
| c = conn.cursor() | |
| c.execute('CREATE TABLE data (data text)') | |
| c.execute('INSERT INTO data VALUES ("init")') | |
| conn.commit() | |
| conn.close() | |
| class MainHandler(tornado.web.RequestHandler): | |
| ''' | |
| Read and write to an sqlite db which blocks on IO without blocking the | |
| coroutines that process HTTP requests | |
| ''' | |
| # This is an unbounded queue. For prod will be preferable to wrap this | |
| # using tornado.queues to get visibility into queue size and return a 502 | |
| # when overloaded instead of overflowing the queue. | |
| executor = ThreadPoolExecutor(max_workers=MAX_WORKERS) | |
| _pool_conns = {} | |
| def initialize(self, db_file): | |
| self.db_file = db_file | |
| def get_conn(self): | |
| ''' | |
| ThreadPoolExecutor reuses existing threads so keep one DB connection | |
| open for each thread. (Don't confuse with sharing a single connection | |
| between multiple threads which is bad.) | |
| ''' | |
| thread_id = threading.current_thread().ident | |
| return self._pool_conns.setdefault(thread_id, | |
| sqlite3.connect(self.db_file)) | |
| @tornado.concurrent.run_on_executor | |
| def read_query(self, query, *args): | |
| conn = self.get_conn() | |
| c = conn.cursor() | |
| ret = c.execute(query, args).fetchall() | |
| return ret | |
| @tornado.concurrent.run_on_executor | |
| def append_query(self, query, *args): | |
| # Writes inside transactions are limited by disk rotation. | |
| # https://www.sqlite.org/faq.html#q19 | |
| conn = self.get_conn() | |
| c = conn.cursor() | |
| ret = c.execute(query, args).fetchall() | |
| conn.commit() | |
| return ret | |
| @tornado.gen.coroutine | |
| def get(self): | |
| ret = yield self.read_query( | |
| 'SELECT * FROM data ORDER BY ROWID DESC LIMIT 1;') | |
| self.write(ret[0][0]) | |
| @tornado.gen.coroutine | |
| def post(self): | |
| data = self.request.body | |
| res = yield self.append_query('INSERT INTO data VALUES(?)', data) | |
| self.write('Success') | |
| class StreamHandler(MainHandler): | |
| @tornado.gen.coroutine | |
| def get(self): | |
| self.set_header('content-type', 'text/event-stream') | |
| self.set_header('cache-control', 'no-cache') | |
| latest_pk = 0 | |
| while True: | |
| # Prod shouldn't query the db every half-second, obviously. (The | |
| # POST method should message the class after a successful write | |
| # instead maybe.) But since we're stress-testing how the SSE stream | |
| # scales during db reads and writes, this will make for a more | |
| # interesting test. | |
| ret = yield self.read_query( | |
| 'SELECT ROWID, data FROM data WHERE ROWID > ?', | |
| latest_pk) | |
| if ret: | |
| latest_pk, res = ret[0] | |
| yield self.format_stream(res) | |
| else: | |
| yield tornado.gen.sleep(0.5) | |
| @tornado.gen.coroutine | |
| def format_stream(self, data): | |
| try: | |
| self.write('data: {0}\n\n'.format(data)) | |
| yield self.flush() | |
| except StreamClosedError: | |
| pass | |
| def main(): | |
| init_db(DB_FILE) | |
| app = tornado.web.Application([ | |
| (r'/', MainHandler, dict(db_file=DB_FILE)), | |
| (r'/stream', StreamHandler, dict(db_file=DB_FILE)), | |
| ], debug=True) | |
| server = tornado.httpserver.HTTPServer(app) | |
| server.listen(8080) | |
| signal.signal(signal.SIGINT, | |
| lambda x, y: tornado.ioloop.IOLoop.instance().stop()) | |
| tornado.ioloop.IOLoop.instance().start() | |
| if __name__ == '__main__': | |
| main() |
Author
Author
Results (with the persistent worker connections added in 4ad3a03):
GETs
% ab -n 1000 -c 100 http://localhost:8080/
Concurrency Level: 100
Time taken for tests: 1.216 seconds
Complete requests: 1000
Failed requests: 0
Total transferred: 196000 bytes
HTML transferred: 4000 bytes
Requests per second: 822.49 [#/sec] (mean)
Time per request: 121.583 [ms] (mean)
Time per request: 1.216 [ms] (mean, across all concurrent requests)
Transfer rate: 157.43 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 1.0 0 5
Processing: 7 118 24.6 115 192
Waiting: 7 118 24.6 115 192
Total: 11 119 24.5 115 192
Percentage of the requests served within a certain time (ms)
50% 115
66% 125
75% 133
80% 142
90% 154
95% 168
98% 175
99% 179
100% 192 (longest request)
POSTs
% ab -n 1000 -c 100 -p post_data http://localhost:8080/
Concurrency Level: 100
Time taken for tests: 4.945 seconds
Complete requests: 1000
Failed requests: 0
Total transferred: 149000 bytes
Total body sent: 132000
HTML transferred: 7000 bytes
Requests per second: 202.21 [#/sec] (mean)
Time per request: 494.540 [ms] (mean)
Time per request: 4.945 [ms] (mean, across all concurrent requests)
Transfer rate: 29.42 [Kbytes/sec] received
26.07 kb/s sent
55.49 kb/s total
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.8 0 4
Processing: 83 462 194.8 440 2733
Waiting: 82 462 194.7 440 2733
Total: 86 463 194.5 440 2734
Percentage of the requests served within a certain time (ms)
50% 440
66% 465
75% 475
80% 484
90% 517
95% 606
98% 992
99% 1638
100% 2734 (longest request)
GETs with 100 SSE listeners:
% ab -n 1000 -c 100 http://localhost:8080/
Concurrency Level: 100
Time taken for tests: 1.305 seconds
Complete requests: 1000
Failed requests: 0
Total transferred: 196000 bytes
HTML transferred: 4000 bytes
Requests per second: 766.01 [#/sec] (mean)
Time per request: 130.547 [ms] (mean)
Time per request: 1.305 [ms] (mean, across all concurrent requests)
Transfer rate: 146.62 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.9 0 5
Processing: 55 128 29.6 122 192
Waiting: 55 128 29.6 122 192
Total: 55 128 29.4 122 192
Percentage of the requests served within a certain time (ms)
50% 122
66% 140
75% 144
80% 157
90% 175
95% 183
98% 188
99% 189
100% 192 (longest request)
POSTs with 100 SSE listeners:
% ab -n 1000 -c 100 -p post_data http://localhost:8080/
Concurrency Level: 100
Time taken for tests: 9.791 seconds
Complete requests: 1000
Failed requests: 0
Total transferred: 149000 bytes
Total body sent: 132000
HTML transferred: 7000 bytes
Requests per second: 102.13 [#/sec] (mean)
Time per request: 979.140 [ms] (mean)
Time per request: 9.791 [ms] (mean, across all concurrent requests)
Transfer rate: 14.86 [Kbytes/sec] received
13.17 kb/s sent
28.03 kb/s total
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 1 1.2 0 6
Processing: 72 822 332.4 836 3894
Waiting: 72 822 332.4 836 3894
Total: 78 823 331.9 836 3895
Percentage of the requests served within a certain time (ms)
50% 836
66% 910
75% 943
80% 964
90% 1020
95% 1127
98% 1613
99% 2126
100% 3895 (longest request)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Using Apache Bench:
GET requests:
POST requests:
Spin up 100 processes with one SSE listener each This triggers a DB read every half-second per listener. Then re-run the above benchmarks.