├── .gitignore ├── Readme.md ├── benchmarks ├── Readme.md ├── current-benchmark-no-proxy.html └── current-benchmark-reads-to-slaves.html ├── bin └── redis-proxy ├── config ├── config.json └── configuration.md ├── docs └── Typical_Setup.jpg ├── lib ├── logging.js ├── redis_command.js ├── redis_proxy.js └── server.js ├── package.json ├── server.js └── test ├── lib └── redis_proxy_spec.js ├── mocha.opts └── testy.js /.gitignore: -------------------------------------------------------------------------------- 1 | dump.rdb 2 | npm-debug.log 3 | *.log 4 | /node_modules -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | Redis-proxy 2 | ============= 3 | 4 | It's like haproxy except for redis. 5 | 6 | *This is a dead project, please look at twemproxy or redis sentinal* 7 | Why RedisProxy? 8 | 9 | Typically for every redis server we setup, we have a backup server setup as a slave of the main server. 10 | 11 | If the Active Redis crashes or goes down for maintenance, we want the application to seamlessly use (read/write) data from the backup server. This can easily be done by making the backup server 'slaveof no one' 12 | But the problem is once the backup takes over as active new writes will go there. Now if the "original master" comes back up it will be out of sync with the "current master", so it has to be slaved to the "current master" before it is ready to start take requests. 13 | 14 | This is solved by redis-proxy, which proxies the active redis. 15 | It is also smart enough to issue slave of commands to machines that start up and make masters slave of no one. It also can optionally provide for reads-write splitting. i.e. Reads going to slaves, and Writes only going to master. 16 | 17 | This reduces the common redis slave master replication dance that needs to be done when bad stuff happens or maintenance of the servers are needed 18 | 19 | Features 20 | ============ 21 | 22 | * Server Monitoring (to track masters and slaves) 23 | * Automatic slave upgrade on master failure 24 | * Connection Pooling 25 | * Supports Pipelining 26 | * Honors Existing Master Slave Configurations( ie. if the masters and slaves are already setup then it will maintain the same configuration, instead of largescale movement of data) 27 | * Read write Splitting. It is available for testing, but not published to npm. 28 | * Turn it on by adding mode: "readsToSlaves" in the configuration(default is allToMaster) 29 | * If there are multiple slaves, a round robin strategy is used. 30 | * It lets standard non mutating commands go to the slaves( instead of the master). This will almost always improve the throughput of the cluster of Redis servers. 31 | * Mutating commands always go to Master. 32 | * Unknown commands (Renamed via the redis config) will go to the Master. 33 | 34 | 35 | ![Typical setup!](http://github.com/sreeix/redis-proxy/raw/master/docs/Typical_Setup.jpg) 36 | 37 | Image courtesy Marlon Li([Marlon Li](https://github.com/atrun)) represents how it is being or can be used. 38 | 39 | 40 | Configuration 41 | ============== 42 | 43 | The redis-proxy uses the config file to figure out the details of the redis cluster, The configurations are documented in the [Config settings](https://github.com/sreeix/redis-proxy/blob/master/config/configuration.md) 44 | 45 | Disclaimer 46 | ============= 47 | 48 | We are in the process of testing it, it works for simple commands, but i have not tested and validated it against the whole set of redis commands. It is likely that commands like Pub sub don't work correctly(or at all). 49 | 50 | Install 51 | ========= 52 | 53 | From NPM 54 | --------- 55 | Node.js and NPM are prerequistes. [Here is the link to install both.](https://github.com/joyent/node/wiki/Installation) 56 | 57 | 58 | * `npm install -g redis-proxy` 59 | 60 | * `redis-proxy ` 61 | 62 | 63 | From Source 64 | ------------- 65 | 66 | * `git clone git@github.com:sreeix/redis-proxy.git` 67 | 68 | * `npm install` 69 | 70 | * `Modify the config/config.json` 71 | 72 | * `npm start` 73 | 74 | Scenarios 75 | ============ 76 | 77 | The standard scenario is each redis has a backup redis. 78 | 79 | * R1 backed by R2 80 | * R1 is slave of no one. 81 | * R2 is slave of R1 82 | * R1 Goes down 83 | * We issue Slave of no one to R2 84 | * Make R2 the active redis 85 | * R1 Comes up. 86 | * We issue Slave of R2 to R1 87 | * R2 is still the active server 88 | * R2 Goes down 89 | * We make R1 Slave of no one 90 | * R1 is now the active redis. 91 | 92 | 93 | If Both of them go down together, We just return errors and wait for one of them to come back on. 94 | 95 | There can be only one master in the system. This is true even if multiple redis-proxies are monitoring the cluster. 96 | 97 | There can be multiple slaves. Each will become slave of the master, and on master doing down, one of the slave it randomly picked as master. 98 | 99 | 100 | Redis-Proxy Stability 101 | ================== 102 | 103 | Redis proxy can become a single point of failure, If it goes down your redis servers will become inaccessible. There are 2 possible setups 104 | 105 | * Using Nodemon/Forever to keep the redis proxy up all the time 106 | 107 | * Have a backup redis-proxy on Elastic IP or Virtual IP and switch manually or using keepalived. 108 | Note, however, that this is not a distributed system (like redis-sentinel). The choice of the redis server to be used is consistent. i.e if a proxy picks up the fact that master is down, then it would promote the highest slave (by runid) to master consistently. 109 | 110 | 111 | Limitations / TODOS 112 | =================== 113 | 114 | * Benchmarks show about 3x drop in performance(Investigating it and will post a fix soon) 115 | 116 | * No support for Monitoring & pub/sub commands( There is no reason why this can't be supported) 117 | 118 | * Would be nice to have a small ui for showing errors and status of redis servers. 119 | 120 | * It currently only works as master/slave mode. And it's highly unlikely that there could be a switch to sharded mode. 121 | 122 | * No downtime adding and removing slaves 123 | 124 | 125 | -------------------------------------------------------------------------------- /benchmarks/Readme.md: -------------------------------------------------------------------------------- 1 | The results of running `redis-benchmark -c 40 -n 100000` 2 | 3 | Both the proxy and the redis server were running locally on a 13'' mac book pro. 4 | 5 | * new-results-straight.txt : The results from executing it against redis-server 6 | * new-results-with-proxy.txt : The results from executing it agains the redis-proxy 7 | 8 | In general we see a 3 times reduced throughput. 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /benchmarks/current-benchmark-no-proxy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 |
CommandTotal RequestsTotal Time TakenRequests per secondMax Time taken
PING (inline)100000.28 seconds35587.196ms
PING100000.20 seconds49751.240ms
MSET (10 keys)100000.44 seconds22675.746ms
SET100000.36 seconds28089.894ms
GET100000.21 seconds47846.890ms
INCR100000.34 seconds29069.770ms
LPUSH100000.35 seconds28490.031ms
LPOP100000.35 seconds28653.290ms
SADD100000.25 seconds40322.581ms
SPOP100000.29 seconds34602.070ms
LPUSH (again, in order to bench LRANGE)100000.34 seconds29498.530ms
LRANGE (first 100 elements)100000.28 seconds35087.720ms
LRANGE (first 300 elements)100000.43 seconds23474.187ms
LRANGE (first 450 elements)100000.45 seconds22075.051ms
LRANGE (first 600 elements)100000.61 seconds16286.641ms
141 | 142 | -------------------------------------------------------------------------------- /benchmarks/current-benchmark-reads-to-slaves.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 |
CommandTotal RequestsTotal Time TakenRequests per secondMax Time taken
PING (inline)100000.99 seconds10090.828ms
PING100000.97 seconds10298.663ms
MSET (10 keys)100001.08 seconds9302.3310ms
SET100000.98 seconds10162.602ms
GET100000.96 seconds10460.252ms
INCR100001.00 seconds10010.012ms
LPUSH100000.99 seconds10080.6511ms
LPOP100000.98 seconds10256.4110ms
SADD100000.99 seconds10101.013ms
SPOP100000.98 seconds10214.506ms
LPUSH (again, in order to bench LRANGE)100001.01 seconds9910.804ms
LRANGE (first 100 elements)100000.96 seconds10395.018ms
LRANGE (first 300 elements)100000.99 seconds10141.992ms
LRANGE (first 450 elements)100001.03 seconds9680.5410ms
LRANGE (first 600 elements)100001.05 seconds9505.702ms
141 | 142 | -------------------------------------------------------------------------------- /bin/redis-proxy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../server.js'); -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "servers": [{ 3 | "host": "localhost" 4 | ,"port": 6379 5 | } 6 | , { 7 | "host": "localhost" 8 | , "port": 6389 9 | } 10 | ] 11 | ,"mode": "readsToSlaves" 12 | ,"listen_port": 9999 13 | ,"bind_address": "0.0.0.0" 14 | ,"check_period": 5000 15 | ,"pool_size": 50 16 | , "debug": false 17 | , "slave_balance": "roundrobin" 18 | , "loggers":[{ "filename": "redis-proxy.log", "level":"silly" } ] 19 | } -------------------------------------------------------------------------------- /config/configuration.md: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============== 3 | 4 | Following options are available 5 | 6 | 7 | * servers -> An Array of Server Information. This should form the default "cluster". host defaults to localhost and port to 6379. 8 | * mode -> can be one `readsToSlaves` or `allToMaster` defaults to allToMaster 9 | * listen_port -> The port on which to listen for redis requests defaults to 9999 10 | * bind_address -> The address to bind on the local machine, defaults to 127.0.0.1, set this to "0.0.0.0" for it to be accessible from other servers. 11 | * check_period -> Ping the redis servers every milliseconds, defaults to 5000 milliseconds 12 | * pool_size -> Size of the connection pools held to the redis server defaults to 50. Currently connection pool cannot be turned off. 13 | * debug -> Put a lot of debugging information, defaults to false. It does put a lot of debugging information. Always goes to the stdout. 14 | * slave_balance -> how to balance between slaves when using `readsToSlaves` mode. Defaults to round robin and the only one supported. 15 | * loggers -> An array of additional logs that go to the file. An array can be specified, and will support all the options of the [`winston.transports.File`](https://github.com/flatiron/winston/blob/master/docs/transports.md#file-transport) class. 16 | 17 | ` 18 | { 19 | "servers": [{ 20 | "host": "localhost" 21 | ,"port": 6379 22 | } 23 | , { 24 | "host": "localhost" 25 | , "port": 6389 26 | } 27 | ] 28 | ,"mode": "readsToSlaves" 29 | ,"listen_port": 9999 30 | ,"bind_address": "0.0.0.0" 31 | ,"check_period": 5000 32 | ,"pool_size": 50 33 | , "debug": false 34 | , "slave_balance": "roundrobin" 35 | , "loggers":[{ "filename": "redis-proxy.log", "level":"silly" } ] 36 | } 37 | ` -------------------------------------------------------------------------------- /docs/Typical_Setup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sreeix/redis-proxy/9dddb73de102c066b76c63a117110b0cf71419b3/docs/Typical_Setup.jpg -------------------------------------------------------------------------------- /lib/logging.js: -------------------------------------------------------------------------------- 1 | var winston = require('winston'), 2 | _ = require('underscore'); 3 | 4 | var newLogger = function (debug, loggers) { 5 | var transports = [new (winston.transports.Console)({ "colorize" : true, "level" : debug ? 'info': "error", "silent" : false, "handleExceptions" : false })]; 6 | if (loggers){ 7 | _.each(loggers, function (logger) { 8 | transports.push(new (winston.transports.File)(logger)); 9 | }); 10 | } 11 | return new (winston.Logger)({"exitOnError" : false, "transports" : transports}); 12 | }; 13 | var logInstance = newLogger(false); 14 | 15 | var logging = module.exports = { 16 | logger : logInstance, 17 | setupLogging : function (debug, loggers) { 18 | return logging.logger = newLogger(debug, loggers); 19 | } 20 | }; -------------------------------------------------------------------------------- /lib/redis_command.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | 3 | // info is a tricky command to send to slave, so till we figure out how to handle info, we send it it master 4 | var readOnlyCommands = ['smembers', 'hlen', 'hmget', 'srandmember', 'hvals', 'randomkey', 'strlen', 5 | 'dbsize', 'keys', 'ttl', 'lindex', 'type', 'llen', 'dump', 'scard', 'echo', 'lrange', 6 | 'zcount', 'exists', 'sdiff', 'zrange', 'mget', 'zrank', 'get', 'getbit', 'getrange', 7 | 'zrevrange', 'zrevrangebyscore', 'hexists', 'object', 'sinter', 'zrevrank', 'hget', 8 | 'zscore', 'hgetall', 'sismember']; 9 | var connectionCommands = ['auth', 'select']; 10 | 11 | module.exports = redisCommand = { 12 | readOnly: function(command){ 13 | return _.include(readOnlyCommands, command.split("\r\n")[2]); 14 | } 15 | , connection: function(command){ 16 | return _.include(connectionCommands, command.split("\r\n")[2]); 17 | } 18 | }; -------------------------------------------------------------------------------- /lib/redis_proxy.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var redisCommand = require('./redis_command'); 3 | var util = require('util'); 4 | var Server = require('./server'); 5 | var logger = require('./logging').logger; 6 | 7 | // Acts as a proxy for a set of redis servers 8 | // first redis becomes the master and then on failure we move to the next one in the list 9 | // When redis becomes the master it executes a slave of no one, and all other redises will 10 | // automatically become slave of this master. 11 | // this is also true when the redis dies and another redis takes over. 12 | // This should also monitor coming up of redis servers and slave them as appropriate. 13 | 14 | var RedisProxy = module.exports = function(o){ 15 | var self = this; 16 | // redis.debug_mode = o.debug || false; 17 | 18 | this._active = null; 19 | this._activeSlaves = []; 20 | this.slaveIndex = 0; 21 | 22 | this.options = _.defaults(o, {listen_port: 6379, softErrorCount: 5, pool_size: 10, mode: "allToMaster"}); 23 | if(o.servers && o.servers.size === 0) 24 | throw new Error("Expected to have at least one redis to proxy. Can't start"); 25 | 26 | this.sendCommand = this[this.options.mode]; 27 | logger.info("Using the "+ this.options.mode +" mode."); 28 | var onDown = function onDown(){ 29 | if(_.isEqual(self._active.options, this.options)){ 30 | logger.error("Main server down PANIC"); 31 | logger.info("finding next active server."); 32 | self.nextActive(); 33 | } else { 34 | self._activeSlaves = _.without(self._activeSlaves, this); 35 | } 36 | }; 37 | var onUp = function onUp() { 38 | logger.debug("We have a server that went up"); 39 | if(!self.active && this.client.server_info.role === 'master'){ 40 | self._active = this; 41 | logger.info("setting up the active "+ self._active.options.host + ":" + self._active.options.port); 42 | self.readyup(this); 43 | } else { 44 | // this is slightly tricky. The active has not been selected yet, in such a case we can slave this redis server. 45 | // But when the master is selected the remaining redises will be slaved correctly. via the `readyup` method 46 | if(self._active) this.slave(self._active); 47 | if(! _.include(self._activeSlaves, this)) self._activeSlaves.push(this); 48 | } 49 | }; 50 | 51 | this.allServers = _.map(o.servers, function(server){ 52 | return new Server(_.defaults(server, {pool_size: self.options.pool_size, softErrorCount: self.options.softErrorCount})) 53 | .on('up', onUp) 54 | .on('down', onDown); 55 | }); 56 | }; 57 | 58 | RedisProxy.prototype.readyup = function(active){ 59 | logger.info("Creating the pool for active server"+ active.options.port); 60 | var self = this; 61 | active.setMaster(); 62 | _.each(this.allServers, function(s){ 63 | if(!_.isEqual(s, active) && !_.include(self._activeSlaves, this) && s.isUp()){ 64 | s.slave(active); 65 | self._activeSlaves.push(s); 66 | } 67 | }); 68 | }; 69 | 70 | RedisProxy.prototype.nextActive = function() { 71 | this._active = _.chain(this.allServers).select(function(server) { 72 | return server.isUp() && server.client.server_info["slave-priority"] !== "0"; 73 | }).sortBy(function (server) { 74 | return server.client.server_info["runid"] ; // no slave priority right now, so just use runid 75 | }).first().value(); 76 | 77 | if(this._active){ 78 | this.readyup(this.active); 79 | logger.info("Setting up as active "+ this.active.options.host +" : " + this.active.options.port); 80 | } else { 81 | logger.error("No redis available"); 82 | } 83 | }; 84 | 85 | Object.defineProperty(RedisProxy.prototype, 'active', { 86 | get: function() { return this._active;} 87 | }); 88 | 89 | // balancing strategies 90 | RedisProxy.prototype.readsToSlaves = function(command, id, callback) { 91 | var serverToSend = null; 92 | if(!this.active){ 93 | return callback(new Error("Expected to have atleast one redis to proxy")); 94 | } 95 | if(redisCommand.readOnly(command)) { 96 | logger.info('Read only command'); 97 | serverToSend = (this.nextSlave() || this.active) 98 | } else { 99 | logger.info('mutating command'); 100 | serverToSend = this.active; 101 | } 102 | logger.info('server:'+ serverToSend.toString()); 103 | return serverToSend.sendCommand(command, id, callback); 104 | }; 105 | 106 | RedisProxy.prototype.allToMaster = function(command, id, callback) { 107 | if(this._active){ 108 | this._active.sendCommand(command, id, callback); 109 | }else{ 110 | return callback(new Error("Expected to have atleast one redis to proxy")); 111 | } 112 | }; 113 | 114 | RedisProxy.prototype.nextSlave = function() { 115 | var slave = this._activeSlaves[this.slaveIndex]; 116 | this.slaveIndex = (this.slaveIndex + 1) % this._activeSlaves.length; 117 | return slave; 118 | }; 119 | 120 | RedisProxy.prototype.quit = function(id) { 121 | if(this.active) this.active.close(id); 122 | }; 123 | 124 | RedisProxy.prototype.watch = function(){ 125 | var self = this; 126 | setInterval(function(){ 127 | _.each(self.allServers, function(server){ 128 | logger.info("Pinging "+ server.options.host +":"+ server.options.port); 129 | server.ping(); 130 | }); 131 | }, this.options.check_period); 132 | }; -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'), 2 | net = require('net'); 3 | 4 | var redis = require('redis'); 5 | 6 | var Pool = require('connection_pool'); 7 | 8 | var EventEmitter = require('events').EventEmitter; 9 | var util = require('util'); 10 | var logger = require('./logging').logger; 11 | 12 | var Server = module.exports = function(serverInfo){ 13 | var self = this; 14 | this.status = 'down'; 15 | this.errorCount = 0; 16 | this.options = _.defaults(serverInfo, {softErrorCount: 5}); 17 | }; 18 | // Following events are fired as appropriate 19 | // up: when the server comes up. 20 | // down: when an existing server goes down. 21 | // slave: when the server becomes a slave of another server( Needs to be fleshed) 22 | // master: When a server becomes master. 23 | 24 | util.inherits(Server, EventEmitter); 25 | 26 | Server.prototype._attachHandlers = function(client){ 27 | var self = this; 28 | client.on('ready', function(data){ 29 | self.up(); 30 | }); 31 | client.on('error', function(data){ 32 | logger.error("error happened " + data); 33 | self._incrErrorCount(); 34 | }); 35 | return client; 36 | }; 37 | 38 | Server.prototype.up = function(){ 39 | if( this.status !== 'up'){ 40 | this.status = 'up'; 41 | this.errorCount = 0; 42 | if(_.isNull(this.connections) || _.isEmpty(this.connections) ){ 43 | this.connections = this._createConnections(); 44 | } 45 | this.emit('up'); 46 | } 47 | return this; 48 | }; 49 | 50 | Server.prototype.setMaster = function(){ 51 | this._master(); 52 | }; 53 | 54 | Server.prototype.sendCommand = function(command, id, cb){ 55 | var self = this; 56 | this.connections.take(id, function(err, conn){ 57 | if(err){ 58 | self._incrErrorCount(); 59 | return cb(err); 60 | } 61 | logger.debug('sending command to redis '+ command); 62 | conn.write(command); 63 | return cb(null, conn); 64 | }); 65 | }; 66 | 67 | Server.prototype.close = function(id){ 68 | this.connections.close(id); 69 | }; 70 | 71 | Server.prototype._setupControlClient = function(serverInfo){ 72 | try{ 73 | this.client = redis.createClient(serverInfo.port, serverInfo.host); 74 | this._attachHandlers(this.client); 75 | } catch(err) { 76 | logger.error(err); 77 | this.down(); 78 | // Its ok... we will bring this guy up some point in time 79 | } 80 | }; 81 | 82 | Server.prototype._createConnections = function(){ 83 | var self = this; 84 | return new Pool({ 85 | create: function(cb){ 86 | try { 87 | var client = net.connect({port: self.options.port, host: self.options.host}); 88 | // self._attachHandlers(client); 89 | return cb(null, client); 90 | } catch(err) { 91 | self._incrErrorCount(); 92 | logger.error('Creating Connection to redis server failed with error '+ err); 93 | return cb(err); 94 | } 95 | } 96 | , maxSize: this.options.pool_size 97 | , startSize: this.options.pool_size 98 | , delayCreation: false 99 | }); 100 | }; 101 | 102 | Server.prototype.slave = function(server){ 103 | var self = this; 104 | logger.info('Marking '+ this.host + ':' + this.port + ' as slave of '+ server.host+': '+ server.port); 105 | this.client.slaveof(server.client.host, server.client.port, function(err, message){ 106 | if(err){ 107 | return logger.error(err); 108 | } 109 | self.emit('slave'); 110 | logger.info(message); 111 | }); 112 | }; 113 | 114 | Server.prototype._master = function (){ 115 | var self = this; 116 | logger.info(this.options.host+":"+this.options.port+ " is slave of no one"); 117 | this.client.slaveof('no', 'one', function(err, message){ 118 | if(err){ 119 | return logger.error(err); 120 | } 121 | return self.emit('master'); 122 | }); 123 | }; 124 | 125 | Server.prototype._incrErrorCount = function(){ 126 | this.errorCount++; 127 | if(this.errorCount > this.options.softErrorCount){ 128 | this.down(); 129 | } 130 | }; 131 | 132 | Server.prototype.ping = function(){ 133 | var self = this; 134 | if(!this.client){ 135 | this._setupControlClient(this.options); 136 | } else { 137 | this.client.ping(function(err, data){ 138 | if(err){ 139 | logger.error(err); 140 | return self.down(); 141 | } 142 | self.up(); 143 | }); 144 | } 145 | }; 146 | 147 | Server.prototype.isUp = function(){ 148 | return this.status === 'up'; 149 | }; 150 | 151 | Server.prototype.down = function(){ 152 | if( this.status !== 'down'){ 153 | this.status = 'down'; 154 | this.emit('down'); 155 | this._clearConnections(); 156 | } 157 | return this; 158 | }; 159 | 160 | Server.prototype._clearConnections = function clearConnections(){ 161 | this.connections.closeAll(); 162 | this.connections = null; 163 | }; 164 | Server.prototype.toString = function () { 165 | return this.host+":"+ this.port; 166 | } 167 | 168 | Object.defineProperty(Server.prototype, 'host', { 169 | get: function() {return this.options.host;} 170 | }); 171 | 172 | Object.defineProperty(Server.prototype, 'port', { 173 | get: function() {return this.options.port;} 174 | }); 175 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Sreekanth " 3 | , "name": "redis-proxy" 4 | , "description": "Proxy for redis servers, to manage redis clusters. Now supports read write splits" 5 | , "version": "0.0.7" 6 | , "repository": { 7 | "type": "git" 8 | , "url": "git@github.com:sreeix/redis-proxy.git" 9 | } 10 | , "bin": { 11 | "redis-proxy": "./bin/redis-proxy" 12 | } 13 | , "main": "server.js" 14 | , "preferGlobal": true 15 | , "engines": { "node": ">= 0.4.7" } 16 | , "node-version": ">0.4.7" 17 | , "dependencies": { 18 | "jade": "0.20.0" 19 | , "express": "2.5.1" 20 | , "underscore": "1.4.4" 21 | , "async": "0.1.15" 22 | , "winston": "0.6.2" 23 | , "hiredis": "" 24 | , "connection_pool": "0.0.2" 25 | , "node-redis-raw": "0.1.1" 26 | , "redis": "0.8.2" 27 | } 28 | , "devDependencies": { 29 | "mocha": "0.0.8" 30 | , "should": "0.3.2" 31 | , "jshint":"0.5.5" 32 | , "metrics":">=0.1.5" 33 | } 34 | , "scripts": { 35 | "start": "node server.js" 36 | , "console": "node" 37 | , "test": "mocha --reporter dot --require should --ui bdd test/lib/*.js" 38 | , "lint": " find . -not -path '*node_modules*' -and -name '*.js' -print0 | xargs -0 node_modules/.bin/jshint" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var net = require('net'); 2 | var fs = require('fs'); 3 | var util = require('util'); 4 | var logging = require('./lib/logging'), logger = logging.logger; 5 | 6 | var configFile = process.argv[2] || "config/config.json"; 7 | logger.info('using '+ configFile + ' as configuration source'); 8 | var config = JSON.parse(fs.readFileSync(configFile)); 9 | logger = logging.setupLogging(config.debug, config.loggers); 10 | 11 | var RedisProxy = require('./lib/redis_proxy'); 12 | var redis_proxy = new RedisProxy(config); 13 | var bindAddress = config.bind_address || "127.0.0.1", 14 | listenPort = config.listen_port || 9999; 15 | 16 | 17 | var server = net.createServer(function (socket) { 18 | var id = socket.remoteAddress+':'+socket.remotePort 19 | logger.debug('client connected ' + id); 20 | socket.on('end', function() { 21 | logger.info('client disconnected'); 22 | // Hack to get the connection identifier, so that we can release the connection 23 | // the usual socket.remoteAddress, socket.remotePort don't seem to work after connection has ended. 24 | if(this._peername){ 25 | redis_proxy.quit(this._peername.address+':'+this._peername.port); 26 | } 27 | }); 28 | 29 | socket.on('data', function(data) { 30 | var command = data.toString('utf8'), id = socket.remoteAddress+':'+socket.remotePort; 31 | redis_proxy.sendCommand(command, id, function(err, response) { 32 | if( response) response.unpipe(); 33 | if(err){ 34 | return socket.write("-ERR Error Happened "+ err); 35 | } 36 | response.pipe(socket); 37 | }) 38 | }); 39 | }); 40 | 41 | redis_proxy.watch(); 42 | 43 | server.listen(listenPort, bindAddress); 44 | logger.info("Redis proxy is listening on " +bindAddress+" : " + listenPort); 45 | -------------------------------------------------------------------------------- /test/lib/redis_proxy_spec.js: -------------------------------------------------------------------------------- 1 | var testy = require('../testy'); 2 | var _ = require('underscore'); 3 | var RedisProxy = require('../../lib/redis_proxy'); 4 | 5 | describe('RedisProxy', function() { 6 | 7 | it('raises exception when no rediss available', function() { 8 | new RedisProxy({}).should.throw(); 9 | }); 10 | 11 | it('should proxy existing redis', function() { 12 | var fake = testy.createRedis(6389); 13 | new RedisProxy({"servers": [{"host": "localhost","port": 6389}]}).should.not.throw(); 14 | fake.close(); 15 | }); 16 | 17 | it('should not fail if both redis is down', function() { 18 | new RedisProxy({"servers": [{"host": "localhost","port": 6389}]}).should.not.throw(); 19 | }); 20 | 21 | it('should use first working redis', function() { 22 | var fake = testy.createRedis(6389); 23 | var proxy = new RedisProxy({"servers": [{"host": "localhost","port": 6389}, {"host": "localhost","port": 6399}]}); 24 | proxy.active.client.port.should.equal(6389); 25 | fake.close(); 26 | }); 27 | 28 | it('should switch to backup if first fails', function() { 29 | var fake = testy.createRedis(6399); 30 | var proxy = new RedisProxy({"servers": [{"host": "localhost","port": 6389}, {"host": "localhost","port": 6399}]}); 31 | proxy.active.client.port.should.equal(6399); 32 | fake.close(); 33 | }); 34 | }); -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require should 2 | --reporter dot 3 | --ui bdd 4 | --growl 5 | 6 | -------------------------------------------------------------------------------- /test/testy.js: -------------------------------------------------------------------------------- 1 | var net = require('net'); 2 | 3 | exports.createRedis = function(port){ 4 | var server = net.createServer(function (socket) { 5 | socket.on('end', function() { 6 | console.log('disconnect'); 7 | }); 8 | socket.on('data', function(data) { 9 | console.log("DATA: "+data); 10 | }); 11 | }); 12 | server.listen(port); 13 | return server; 14 | } 15 | --------------------------------------------------------------------------------