I have a WSGI server which uses paste.webserver to handle HTTP/1.1
requests. In the environment I'm in the documented limitations to
paste.webserver's 1.1 support are fine.
I ran a benchmark and found that using keep-alive to make multiple
requests is a lot slower than making the same number of requests using
a new connection each time, by about 2 orders of magnitude (10
requests per second vs 1000 request per second)
Here's my test code:
from paste import httpserver
def echo_app(environ, start_response):
n = 10000
start_response("200 Ok", [("Content-Type", "text/plain"),
("Content-Length", str(n))])
return ["*" * n]
httpserver.serve(echo_app, protocol_version="HTTP/1.1")
To verify that it returns 10000 characters,
% curl
http://localhost:8080/ > /dev/null
% Total % Received % Xferd Average Speed Time Time
Time Current
Dload Upload Total Spent
Left Speed
100 10000 100 10000 0 0 92838 0 --:--:-- --:--:--
--:--:-- 1953k
I'll use httperf to benchmark with 100 different connections:
% ./httperf --server localhost --port 8080 --num-conn 100
httperf --client=0/1 --server=localhost --port=8080 --uri=/ --send-
buffer=4096 --recv-buffer=16384 --num-conns=100 --num-calls=1
...
Total: connections 100 requests 100 replies 100 test-duration 0.076 s
Connection rate: 1320.4 conn/s (0.8 ms/conn, <=1 concurrent
connections)
Connection time [ms]: min 0.6 avg 0.8 max 1.9 median 0.5 stddev 0.2
Connection time [ms]: connect 0.1
Connection length [replies/conn]: 1.000
Request rate: 1320.4 req/s (0.8 ms/req)
Request size [B]: 62.0
...
Now I'll do the same test with 100 calls on the same connection
% ./httperf --server localhost --port 8080 --num-call 100
httperf --client=0/1 --server=localhost --port=8080 --uri=/ --send-
buffer=4096 --recv-buffer=16384 --num-conns=1 --num-calls=100
...
Total: connections 1 requests 100 replies 100 test-duration 8.939 s
Connection rate: 0.1 conn/s (8939.5 ms/conn, <=1 concurrent
connections)
Connection time [ms]: min 8939.5 avg 8939.5 max 8939.5 median 8939.5
stddev 0.0
Connection time [ms]: connect 0.1
Connection length [replies/conn]: 100.000
Request rate: 11.2 req/s (89.4 ms/req)
...
That is, I can do 1320 requests per second if I create a new
connection each time, and I get about 11.2 request per second if I
reuse the connection.
Those numbers were with Python 2.5 . Using Python 2.6 they are 1080
and 11.4 request/sec, which is effectively the same. (These numbers
are repeatable to within about 15%.)
I'm using paste.httpserver behind flup's Apache-style preforking
server, which is different than that multi-threading version here. To
get good profiling numbers in the face of threads, and to show that
it's not a threading problem, I've rewritten the code to be
from paste import httpserver
class MyHandler(httpserver.WSGIHandler):
sys_version = None
server_version = "MyServer/0.0"
protocol_version = "HTTP/1.1"
def log_request(self, *args, **kwargs):
pass
def echo_app(environ, start_response):
n = 10000
start_response("200 Ok", [("Content-Type", "text/plain"),
("Content-Length", str(n))])
return ["*" * n]
# WSGIServerBase is single-threaded
server = httpserver.WSGIServerBase(echo_app, ("localhost", 8080),
MyHandler)
server.handle_request()
Running this as
% python2.6 -m cProfile -o paste.prof paste_slowdown.py
and hitting it with
httperf --client=0/1 --server=localhost --port=8080 --uri=/ --send-
buffer=4096 --recv-buffer=16384 --num-conns=1 --num-calls=500
(that reports: Request rate: 11.2 req/s (89.4 ms/req) )
gives me profile numbers of
>>> p=pstats.Stats("paste.prof")
>>> p.strip_dirs().sort_stats("cumulative").print_stats()
Sun Nov 22 21:31:57 2009 paste.prof
109749 function calls in 46.570 CPU seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno
(function)
1 0.000 0.000 46.571 46.571 {execfile}
1 0.001 0.001 46.570 46.570 paste_slowdown.py:2
(<module>)
1 0.000 0.000 46.115 46.115 SocketServer.py:250
(handle_request)
1 0.000 0.000 44.675 44.675 SocketServer.py:268
(_handle_request_noblock)
1 0.000 0.000 44.675 44.675 SocketServer.py:301
(process_request)
1 0.000 0.000 44.675 44.675 SocketServer.py:318
(finish_request)
1 0.000 0.000 44.675 44.675 SocketServer.py:609
(__init__)
1 0.000 0.000 44.675 44.675 httpserver.py:456
(handle)
1 0.001 0.001 44.675 44.675 BaseHTTPServer.py:325
(handle)
501 0.006 0.000 44.674 0.089 httpserver.py:440
(handle_one_request)
2001 0.020 0.000 44.383 0.022 socket.py:373(readline)
501 44.354 0.089 44.354 0.089 {method 'recv' of
'_socket.socket' objects}
1 1.440 1.440 1.440 1.440 {select.select}
It looks like most of the time is spent in recv, but I can't figure
out why that would be the case here, with HTTP/1.1 and not with the
HTTP/1.0 call.
I then composed an HTTP/1.1 request by hand, which looks like
GET / HTTP/1.1
Location: localhost
Connection: Keep-Alive
Content-Length: 0
GET / HTTP/1.1
Location: localhost
Connection: Keep-Alive
Content-Length: 0
... repeat 97 more times, to have 99 keep-alives in total ...
GET / HTTP/1.1
Location: localhost
Connection: Close
Content-Length: 0
then send it with netcat, like this
nc localhost 8080 < ~/src/send_to_paste.txt
That took 0.03 seconds to run, my server instrumentation shows
echo_app was called 100 times, and netcat returned with the expected
number of bytes for 100 responses.
This made me think that httperf was the problem, but then I tried
using ApacheBench and got even worse results - it timed out!
This test does 100 requests, each with its own connection
% ab -n 100 localhost:8080/
This is ApacheBench, Version 1.3d <$Revision: 1.73 $> apache-1.3
...
Requests per second: 1111.11 [#/sec] (mean)
...
This test does 100 requests, reusing the same connection
% ab -n 100 -k localhost:8080/
This is ApacheBench, Version 1.3d <$Revision: 1.73 $> apache-1.3
Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd,
http://www.zeustech.net/
Copyright (c) 2006 The Apache Software Foundation,
http://www.apache.org/
Benchmarking localhost (be patient)...
Server timed out
: Operation now in progress
That makes me think there's something going on inside of paste. But I
can't figure it out.
Any thoughts?
Andrew
da...@dalkescientific.com