Skip to content

Instantly share code, notes, and snippets.

@whiteinge
Last active October 18, 2016 02:22
Show Gist options
  • Select an option

  • Save whiteinge/9489f0dccd55b61665d5 to your computer and use it in GitHub Desktop.

Select an option

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!
# Only required on Python 2
futures==3.0.3
tornado==4.3
#!/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()
@whiteinge

whiteinge commented Jan 19, 2016

Copy link
Copy Markdown
Author

Using Apache Bench:

GET requests:

ab -n 1000 -c 100 http://localhost:8080/

POST requests:

echo foo > post_data
ab -n 1000 -c 100 -p post_data http://localhost:8080/

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.

echo {1..100} | xargs -d' ' -P100 -n1 -I@ sh -c 'curl -NsS localhost:8080'

@whiteinge

Copy link
Copy Markdown
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