Sticky Session on Heroku - node.js

We have have a NodeJS application running with SocketIO and clustering on heroku. To get SocketIO working we use the redis-adapter like discussed here: https://socket.io/docs/using-multiple-nodes/.
Then we've implemented sticky sessions like shown in the sticky session documentation here: https://github.com/elad/node-cluster-socket.io.
Turns out that when we deploy to Heroku, the connection.remoteAddress in:
// Create the outside facing server listening on our port.
var server = net.createServer({ pauseOnConnect: true }, function(connection) {
// We received a connection and need to pass it to the appropriate
// worker. Get the worker for this connection's source IP and pass
// it the connection.
var index = worker_index(connection.remoteAddress, num_processes);
var worker = workers[index];
worker.send('sticky-session:connection', connection);
}).listen(port);
is actually the IP address of some heroku routing server and NOT the client IP. I've seen that the request header "x-forwarded-for" could be used to get the client IP, but when we pause the connection in this way, we don't even have the headers yet?

We searched all over for a solution, but apparently there's no good solutions.
Here are some of the better suggestions:
https://github.com/indutny/sticky-session/issues/6
https://github.com/indutny/sticky-session/pull/45
None of them seemed good performance wise and therefore we ended up changing SocketIO communication to Websockets only. This eliminates the need for sticky sessions all together.

Related

How to use socket.io-redis with multiple servers?

i have following code on two machines
var server = require('http').createServer(app);
io = require('socket.io')(server);
var redisAdapter = require('socket.io-redis');
io.adapter(redisAdaptebr({host: config.redis.host, port: config.redis.port}));
server.listen(config.port, function () {
and I store socket.id of every client connected to these two machines on central db, ID of sockets is being saved and event sending on same server works flawlessly, but when I try to send message to the socket of other server it doesn't work..
subSocket = io.sockets.connected[userSocketID];
subSocket.emit('hello',{a:'b'})
How can i know that redis is wokring good.
How to send message to socket connected on another server.
You can't. Socket.IO requires sticky sessions. The socket must communicate solely with the originating process.
docs
You can have the socket.io servers communicate to each other to pass events around, but the client must continue talking to the process with which it originated.
I'm in a similar issue but I can answer your first question.
you can monitor all the commands processed by redis using that command on the terminal:
redis-cli monitor
http://redis.io/commands/MONITOR
Unfortunately I cannot help you further as I am still having issues even though both server are sending something to redis.

How to check socket is alive (connected) in socket.io with multiple nodes and socket.io-redis

I am using socket.io with multiple nodes, socket.io-redis and nginx. I follow this guide: http://socket.io/docs/using-multiple-nodes/
I am trying to do: At a function (server site), I want to query by socketid that this socket is connected or disconnect
I tried io.of('namespace').connected[socketid], it only work for current process ( it mean that it can check for current process only).
Anyone can help me? Thanks for advance.
How can I check socket is alive (connected) with socketid I tried
namespace.connected[socketid], it only work for current process.
As you said, separate process means that the sockets are only registered on the process that they first connected to. You need to use socket.io-redis to connect all your nodes together, and what you can do is broadcast an event each time a client connects/disconnects, so that each node has an updated real-time list of all the clients.
Check out here
as mentioned above you should use socket.io-redis to get it work on multiple nodes.
var io = require('socket.io')(3000);
var redis = require('socket.io-redis');
io.adapter(redis({ host: 'localhost', port: 6379 }));
I had the same problem and no solution at my convenience. So I made a log of the client to see the different methods and variable that I can use. there is the client.conn.readystate property for the state of the connection "open/closed" and the client.onclose() function to capture the closing of the connection.
const server = require('http').createServer(app);
const io = require('socket.io')(server);
let clients = [];
io.on('connection', (client)=>{
clients.push(client);
console.log(client.conn.readyState);
client.onclose = ()=>{
// do something
console.log(client.conn.readyState);
clients.splice(clients.indexOf(client),1);
}
});
When deploying Socket.IO application on a multi-nodes cluster, that means multiple SocketIO servers, there are two things to take care of:
Using the Redis adapter and Enabling the sticky session feature: when a request comes from a SocketIO client (browser) to your app, it gets associated with a particular session-id, these requests must be kept connecting with the same process (Pod in Kubernetes) that originated their ids.
you can learn more about this from this Medium story (source code available) https://saphidev.medium.com/socketio-redis...

Errors going to 2 dynos on Heroku with socket.io / socket.io-redis / rediscloud / node.js

I have a node.js / socket.io app running on Heroku. I am using socket.io-redis with RedisCloud to allow users who connect to different dynos to communicate, as described here.
From my app.js:
var express = require('express'),
app = express(),
http = require('http'),
server = http.createServer(app),
io = require('socket.io').listen(server),
redis = require('redis'),
ioredis = require('socket.io-redis'),
url = require('url'),
redisURL = url.parse(process.env.REDISCLOUD_URL),
And later in app.js ...
var sub1 = redis.createClient(redisURL.port, redisURL.hostname, {
no_ready_check: true,
return_buffers: true
});
sub1.auth(redisURL.auth.split(":")[1]);
var pub1 = redis.createClient(redisURL.port, redisURL.hostname, {
no_ready_check: true,
return_buffers: true
});
pub1.auth(redisURL.auth.split(":")[1]);
var redisOptions = {
pubClient: pub1,
subClient: sub1,
host: redisURL.hostname,
port: redisURL.port
};
if (io.adapter) {
io.adapter(ioredis(redisOptions));
console.log("mylog: io.adapter found");
}
It is kind of working -- communication is succeeding between dynos.
Three issues that happen with 2 dynos but not with 1 dyno:
1) There is a login prompt which comes up and works reliably with 1 dyno but is hit-and-miss with 2 dynos -- may not come up and may not work if it does come up. It is (or should be) triggered by the io.sockets.on('connection') event.
2) I'm seeing a lot of disconnects in the server log.
3) Also lots of errors in the client console on Chrome, for example:
socket.io.js:5039 WebSocket connection to 'ws://example.mydomain.com/socket.io/?EIO=3&transport=websocket&sid=F8babuJrLI6AYdXZAAAI' failed: Error during WebSocket handshake: Unexpected response code: 503
socket.io.js:2739 POST http://example.mydomain.com/socket.io/?EIO=3&transport=polling&t=1419624845433-63&sid=dkFE9mUbvKfl_fiPAAAJ net::ERR_INCOMPLETE_CHUNKED_ENCODING
socket.io.js:2739 GET http://example.mydomain.com/socket.io/?EIO=3&transport=polling&t=1419624842679-54&sid=Og2ZhJtreOG0wnt8AAAQ 400 (Bad Request)
socket.io.js:3318 WebSocket connection to 'ws://example.mydomain.com/socket.io/?EIO=3&transport=websocket&sid=ITYEPePvxQgs0tcDAAAM' failed: WebSocket is closed before the connection is established.
Any thoughts or suggestions would be welcome.
Yes, like generalhenry said, the issue is that Socket.io requires sticky sessions (meaning that requests from a given user always go to the same dyno), and Heroku doesn't support that.
(It works with 1 dyno because when there's only 1 then all requests go to it.)
https://github.com/Automattic/engine.io/issues/261 has a lot more good info, apparently web sockets don't really require sticky sessions, but long-polling does. It also mentions a couple of potential work-arounds:
Roll back to socket.io version 0.9.17, which tries websockets first
Only use SSL connections which, makes websockets more reliable (because ISP's and corporate proxies and whatnot can't tinker with the connection as easily.)
You might get the best results from combining both of those.
You could also spin up your own load balancer that adds sticky session support, but by that point, you're fighting against Heroku and might be better off on a different host.
RE: your other question about the Node.js cluster module: it wouldn't really help here. It's for using up all of the available CPU cores on a single server/dyno,

How io.adapter works under the hood?

I'm working on 1-1 chat rooms application powered by node.js + express + socket.io.
I am following the article: Socket.IO - Rooms and Namespaces
In the article they demonstrate how to initiate the io.adapter using the module socket.io-redis:
var io = require('socket.io')(3000);
var redis = require('socket.io-redis');
io.adapter(redis({ host: 'localhost', port: 6379 }));
Two questions:
In the docs, They are mentioning two more arguments: pubClient and subClient. Should I supply them? What's the difference?
How the io.adapter behaves? For example, if user A is connected to server A and user B is server B, and they want to "talk" with each other. What's going under the hood?
Thanks.
You do not need to pass your own pubClient/subClient. If you pass host/port, they will be created for you. But, if you want to create them yourself, for any reason (e.g. you want to tweak reconnection timeouts), you create those 2 clients and pass it to adapter.
The adapter broadcasts all emits internally. So, it gives you the cluster feature. E.g. lets suppose that you have chat application, and you have 3 node.js servers behind load balancer (so they share single URL). Lets also assume that 6 different browsers connect to load balancer URL and they are routed to 3 separate node.js processes, 2 users per node.js server. If client #1 sends a message, node.js #1 will do something like io.to('chatroom').emit('msg from user #1'). Without adapter, both server #1 users will receive the emit, but not the remaining 4 users. If you use adapter, however, remaining node.js #2 and node.js #3 will receive info that emit was done and will issue identical emit to their clients - and all 6 users will receive initial message.
I've been struggling with this same issue, but have found an answer that seems to be working for me, at least in my initial testing phases.
I have a clustered application running 8 instances using express, cluster , socket.io , socket.io-redis and NOT sticky-sessions -> because using sticky seemed to cause a ton of bizarre bugs.
what I think is missing from the socket.io docs is this:
io.adapter(redis({ host: 'localhost', port: 6379 })); only supports web sockets ( well at the very least it doesn't support long polling ) , and so the client needs to specify that websockets are the only transport available. As soon as I did that I was able to get it going. So on the client side, I added {transports:['websockets']} to the socket constructor... so instead of this...
var socketio = io.connect( window.location.origin );
use this
var socketio = io.connect( window.location.origin , {transports:['websocket']} );
I haven't been able to find any more documentation from socket.io to support my theory but adding that got it going.
I forked this great chat example that wasn't working and got it working here: https://github.com/squivo/chat-example-cluster so there's finally a working example online :D

Load Balance: Node.js - Socket.io - Redis

I have 3 Servers running NodeJs, and they are related each other with Redis (1 master, 2 slaves).
The issue i'm having is that running the system on a single server works fine, but when I scale it to 3 NodeJS servers, it starts missing messages and the system gets unstable.
My load balancer does not accept sticky sessions. So every time that the requests from the client arrives to it, they can go to a different server.
I'm pointing all the NodeJS servers to the Redis Master.
It looks like socket.io is storing information on each server and it is not being distributed with redis.
I'm using socket.io V9, I'm suspecting that I don't have any handshake code, could this be the reason?
My code to configure socket.io is:
var express = require('express');
var io = require('socket.io');
var redis = require('socket.io/node_modules/redis');
var RedisStore = require('socket.io/lib/stores/redis');
var pub = redis.createClient("a port", "an ip");
var sub = redis.createClient("a port", "an ip");
var client = redis.createClient("a port", "an ip");
var events = require('./modules/eventHandler');
exports.createServer = function createServer() {
var app = express();
var server = app.listen(80);
var socketIO = io.listen(server);
socketIO.configure(function () {
socketIO.set('store', new RedisStore({
redisPub: pub,
redisSub: sub,
redisClient: client
}));
socketIO.set('resource', '/chat/socket.io');
socketIO.set('log level', 0);
socketIO.set('transports', [, 'htmlfile', 'xhr-polling', 'jsonp-polling']);
});
// attach event handlers
events.attachHandlers(socketIO);
// return server instance
return server;
};
Redis only syncs from the master to the slaves. It never syncs from the slaves to the master. So, if you're writing to all 3 of your machines, then the only messages that will wind up synced across all three servers will be the ones hitting the master. This is why it looks like you're missing messages.
More info here.
Read only slave
Since Redis 2.6 slaves support a read-only mode that
is enabled by default. This behavior is controlled by the
slave-read-only option in the redis.conf file, and can be enabled and
disabled at runtime using CONFIG SET.
Read only slaves will reject all
the write commands, so that it is not possible to write to a slave
because of a mistake. This does not mean that the feature is conceived
to expose a slave instance to the internet or more generally to a
network where untrusted clients exist, because administrative commands
like DEBUG or CONFIG are still enabled. However security of read-only
instances can be improved disabling commands in redis.conf using the
rename-command directive.
You may wonder why it is possible to revert
the default and have slave instances that can be target of write
operations. The reason is that while this writes will be discarded if
the slave and the master will resynchronize, or if the slave is
restarted, often there is ephemeral data that is unimportant that can
be stored into slaves. For instance clients may take information about
reachability of master in the slave instance to coordinate a fail over
strategy.
I arrived to this post:
It can be a good idea to have a "proxy" between nodejs servers and the load balancer.
With this approach XHR-Polling can be used in load balancers without Sticky sessions.
Load balancing with node.js using http-proxy
using nodejs-http-proxy i can have custom routing route, ex. by adding a parameter on the "connect url" of socket.io.
Anyone tried this solution before?

Resources