Node.js load balancer proof of concept
by Pascal Opitz on May 10 2010, 10:32
Down with a cold, the only bit I managed was a bit more experimenting with node.js this weekend. Here's a little example of node.js acting as a very basic load balancer with fault tolerance.
I did create a repo on github, feel free to contribute or fork if you find this useful.
Project Files
$ ls -h
README cluster.conf.json
balancer.js testserver.js
Load Balancer
var sys = require('sys'),
http = require('http'),
fs = require('fs');
var LoadBalancer = new function() {
var _self = this;
var _server;
var _cluster = [];
var _active = [];
var _port = 8888;
var _checkInterval = 10000;
var _checkTimeout = [];
var _updateActives = function() {
_active = [];
for(var i=0; i<_cluster.length; i++) {
if(_cluster[i].active) {
_active[_active.length] = _cluster[i];
}
}
};
var _loadCluster = function(callback) {
fs.readFile('./cluster.conf.json', function (err, data) {
if (err) throw err;
_cluster = JSON.parse(data.toString().replace('\n', ''));
callback();
});
};
var _checkCluster = function() {
for(var i=0; i<_cluster.length; i++) {
_clusterNodeCheck(_cluster[i]);
}
};
var _clusterNodeCheck = function(node) {
var client = http.createClient(parseInt(node.port, 10), node.host);
var request = client.request('GET', '/is_up', {"host" : node.host});
request.addListener('response', function (response) {
if(response.statusCode == 200) {
response.addListener('data', function(data) {
if(data == 'ok') {
node.active = true;
} else {
node.active = false;
}
});
} else {
node.active = false;
}
});
request.addListener('error', function (err) {
node.active = false;
});
client.addListener('error', function (err) {
node.active = false;
});
request.end();
setTimeout(_updateActives, 200);
_checkTimeout[node.host + ':' + node.port] = setTimeout(function() {
_clusterNodeCheck(node);
}, _checkInterval);
};
var _requestHandler = function(request, response) {
if(_active.length == 0) {
response.writeHead(500, {'Content-Type': 'text/html'});
response.write('no server active');
response.end();
} else {
var index = Math.floor(Math.random()*_active.length);
var node = _active[index];
var proxy_headers = request.headers;
var proxy_client = http.createClient(parseInt(node.port, 10), node.host);
var proxy_request = proxy_client.request(request.method, request.url, proxy_headers);
proxy_request.addListener("response", function (proxy_response) {
response.writeHeader(proxy_response.statusCode, proxy_response.headers);
proxy_response.addListener("data", function (chunk) {
response.write(chunk);
});
proxy_response.addListener("end", function () {
response.end();
});
});
proxy_client.addListener("error", function (error) {
for(var i=0; i<_cluster.length; i++) {
if(node.host == _cluster[i].host && node.port == _cluster[i].port) {
sys.puts('error, deactivating: '+node.host+':'+node.port);
_cluster[i].active = false;
_updateActives();
}
clearTimeout(_checkTimeout[_cluster[i].host + ':' + _cluster[i].port]);
_clusterNodeCheck(_cluster[i]);
}
setTimeout(function() {
_requestHandler(request, response);
}, 200);
});
proxy_request.end();
}
};
var _run = function() {
_loadCluster(_checkCluster);
_server = http.createServer().
addListener('request', _requestHandler)
.listen(_port);
sys.puts('Listening to port ' + _port);
};
_run();
};
Config file
[{
"host" : "127.0.0.1",
"port" : "8200"
},
{
"host" : "127.0.0.1",
"port" : "8300"
}]
Test Server
var sys = require('sys'),
http = require('http');
var _port;
function checkUsage() {
if(process.argv.length != 3) {
sys.puts('usage: node testserver.js <port>');
process.exit(1);
}
var port = parseInt(process.argv[2], 10);
if('NaN' == port.toString()) {
sys.puts('usage: node testserver.js <port>');
process.exit(1);
}
_port = port;
};
var TestServer = function() {
var _self = this;
var _server;
var _routes = {
'/' : function(request, response) {
response.writeHead(200, {'Content-Type': 'text/html'});
response.write('hello world\n');
response.end();
},
'/is_up' : function(request, response) {
response.writeHead(200, {'Content-Type': 'text/plain'});
response.write('ok');
response.end();
},
}
var _requestHandler = function(request, response) {
sys.puts('Request '+request.url+' from '+request.connection.remoteAddress+' to '+request.headers.host);
if(_routes[request.url] === undefined) {
response.writeHead(404, {'Content-Type': 'text/plain'});
response.write('not found\n');
response.end();
} else {
_routes[request.url].call(this, request, response);
}
};
var _run = function() {
_server = http.createServer().
addListener('request', _requestHandler)
.listen(_port);
sys.puts('Listening to port ' + _port);
};
_run();
};
checkUsage();
server = new TestServer();
Readme
A simple load balancer in node.js
Start testservers in seperate shells:
node testserver.js 8200
node testserver.js 8300
Start load balancer:
node balancer.js
Request to balancer:
curl http://127.0.0.1:8888
Comments
Pascal,
You've definitely outlined the core components to a node.js load balancer, but I think there are some edge cases in your reverse proxy code that you might not catch until you run in a large scale production environment.
We've seen these issues at Nodejitsu and have an HTTP reverse proxy library that has been battle hardened through use in our production environment. Check it out, commits are welcome :)
http://github.com/nodejitsu/node-http-proxy
by Charlie Robbins on November 9 2010, 01:33 #
Charlie, that's great work! The above example was just a little proof of concept, not meant to be production ready. It would be interesting to find out more. Could you tell us more about the edge cases?
by Pascal Opitz on November 9 2010, 05:32 #
by Lukas Vlcek on October 5 2010, 06:58 #
by Zac on January 28 2011, 04:55 #