Load Testing - Node.js vs Single Threaded Python Web Server vs PHP5+Apache2.2
Load testing is generally used to test performance of any product by putting it under extreme conditions and one can know about expected performance in worst scenario. In case of web applications, this is acheived by with writing a bot script or using some automated tools like Seleneium.
So, I got curious about testing the relative performace of a webserver written in Node.js, a single threaded Python web server and one application written in PHP and hosted on Apache 2.2 web server. Task of web application was to get exactly 5 key-value pairs via GET request and insert it into MongoDB database. For example - if one url like http://127.0.0.1:8000/?key_1=value1&key_2=value2&key_3=value_3&key_4=value_4&key_5=value_5 is called, then one document like {key_1:value_1, key_2:value_2, key_3:value_3, key_4:value_4, key_5:value_5} will be inserted in the existing collection of some MongoDB database.
For this testing I wrote a Python script which works something like this -
- A set of urls arerequested parallely to web server inside the python test script.
- It waits for response for each url until time out limit is reached.
- If there is any URLError like "connection timed out" or any HTTPError, we count all of them
- Above 3 steps are repeated in a for loop say for a given no. of times.
- Above 4 steps are repeated by varying count of urls in the set for parallel request.
Results
Click here to see the image in new window.

Analysis
As you can see in the screenshot that
- node.js is vey quick in handling requests even if only instance of node is running. This is because of intelligent architecture of node and event based handling.
- PHP5+Apache 2.2 is not as fast as node but performance is acceptable. In apache, many child processes are created under one parent process which leads to good performance.
- In case of single threaded Python server, requests are handled after one another. Due to this as no. of parallel requests increases we get very bad performance and lot of requests face "Conncetion timed out" error.
This means, web applications written in node.js can handle very large no. of requests at a time (as they one node instance can handle as many as 10000 requests at a time), PHP5+Apache can be a good option if you are not expecting very high traffic on you website. On the other hand, single threaded Python servers should never be used in production systems but they help a lot in while developing any web application using Python based frameworks like Django, Web2py or Pylons.
Thanks for reading till last line, please leave a comment.
run.py
#! /usr/bin/env python
import random
import urllib
import urllib2
import string
import datetime
import time
from threading import Thread, enumerate
URIS_COUNT = 500 #total no. of urls to be accessed in parallel
FORLOOP_COUNT = 100 #total no. of loops in which prallel request will be sent
TIME_OUT = 5.0 #timeout in seconds
UPDATE_INTERVAL = 0.01 #update interval for checking threads
BASE_URL = 'http://127.0.0.1:8000/' #base url of server, remember trailing slash is required
errors = {} #this dictionary is used to store count of all sort of HTTPError and URLError
def rand():
return ''.join(random.choice(string.ascii_letters + string.digits) for x in range(20))
def dict_to_query_string(d):
return '&'.join([k+'='+urllib.quote(str(v)) for (k,v) in d.items()])
class URLThread(Thread):
def __init__(self,url):
super(URLThread, self).__init__()
self.url = url
self.response = None
def run(self):
req = urllib2.Request(self.url)
try:
self.request = urllib2.urlopen(req)
#self.response = self.request.read()
except urllib2.HTTPError, e:
try:
errors['HTTPError Code - '+str(e.code)] += 1
except KeyError:
errors['HTTPError Code - '+str(e.code)] = 1
except urllib2.URLError, e:
try:
errors['URLError Reason - '+str(e.reason)] += 1
except KeyError:
errors['URLError Reason - '+str(e.reason)] = 1
else:
pass
uris = []
for k in range(URIS_COUNT):
values = {}
for j in range(5):
values[rand()]= rand()
uris.append(BASE_URL+'?'+dict_to_query_string(values))
def multi_get(uris,timeout=TIME_OUT):
def alive_count(lst):
alive = map(lambda x : 1 if x.isAlive() else 0, lst)
return reduce(lambda a,b : a + b, alive)
threads = [ URLThread(url) for url in uris ]
for thread in threads:
thread.start()
while alive_count(threads) > 0 and timeout > 0.0:
timeout = timeout - UPDATE_INTERVAL
time.sleep(UPDATE_INTERVAL)
return
#return [ (x.url, x.response) for x in threads ]
start_time = datetime.datetime.now()
for i in range(FORLOOP_COUNT):
print 'Step no. - %d'%(i+1)
multi_get(uris,timeout=100.0)
end_time = datetime.datetime.now()
td = end_time - start_time
seconds = td.days*24*3600 + td.seconds + float(td.microseconds/10**6)
print '**********************Finished********************'
print 'Total time taken = %f seconds'%seconds
print errors
print '**************************************************'
example.py
#! /usr/bin/env python
import datetime
import sys
import BaseHTTPServer
from urlparse import urlparse
from pymongo import Connection
connection = Connection('localhost', 27017)
db = connection.mydb
collection = db.mycollection
class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def do_HEAD(s):
s.send_response(200)
s.send_header('Content-type', 'text/plain')
s.end_headers()
def do_GET(s):
s.do_HEAD()
qs = urlparse(s.path).query
params = dict([part.split('=') for part in qs.split('&')])
if not len(params)==5:
print 'Query string should have exactly 5 key value pairs.'
s.wfile.write('Failure')
else:
print 'Query String has been saved successfully'
collection.insert(params)
s.wfile.write('Success')
server_class = BaseHTTPServer.HTTPServer
handler_class = MyHandler
server_address = ('', 8000)
httpd = server_class(server_address, handler_class)
print 'Server Starts'
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
print 'Server Stops'
example.js
var http = require('http');
var url = require('url');
var mongo = require('mongodb')
db = new mongo.Db('mydb', new mongo.Server('localhost', 27017, {}), {});
db.open(function() {
db.collection('mycollection', function(err, collection){
http.createServer(function (request, response) {
var query = url.parse(request.url, true)['query']
var count = 0;
for (var key in query){
count += 1;
}
response.writeHead(200, {'Content-Type': 'text/plain'});
if (count>5 || count<5){
console.log('Query string should have exactly 5 key value pairs.')
response.end('Failure');
}else {
collection.insert(query, function(){
console.log('Query String has been saved successfully.');
});
response.end('Success');
};
}).listen(8000);
});
});
console.log('Server running at http://127.0.0.1:8000/');
example.php
<?php
try {
$m = new Mongo('localhost:27017', array('persist'=>'x')); //creating new mongo connection.
$db = $m->selectDB('mydb');
} catch (MongoConnectionException $e) {
echo '<p>Couldn\'t connect to mongodb, is the "mongod" process running?</p>';
exit();
}
$collection = $db->selectCollection('mycollection');
$query = $_GET;
if (count($query)==5){
$collection->insert($query);
echo 'Success';
} else {
echo 'Failure - Query string should have exactly 5 key-value pairs.';
exit();
}
?>
30 Jul, 2011
