Node.js http-proxy drops websocket requests
Asked Answered
C

3

23

Okay, I've spent over a week trying to figure this out to no avail, so if anyone has a clue, you are a hero. This isn't going to be an easy question to answer, unless I am being a dunce.

I am using node-http-proxy to proxy sticky sessions to 16 node.js workers running on different ports.

I use Socket.IO's Web Sockets to handle a bunch of different types of requests, and use traditional requests as well.

When I switched my server over to proxying via node-http-proxy, a new problem crept up in that sometimes, my Socket.IO session cannot establish a connection.

I literally can't stably reproduce it for the life of me, with the only way to turn it on being to throw a lot of traffic from multiple clients to the server.

If I reload the user's browser, it can then sometimes re-connect, and sometimes not.

Sticky Sessions

I have to proxy sticky sessions as my app authenticates on a per-worker basis, and so it routes a request based on its Connect.SID cookie (I am using connect/express).

Okay, some code

This is my proxy.js file that runs in node and routes to each of the workers:

var http = require('http');
var httpProxy = require('http-proxy');

// What ports the proxy is routing to.
var data = {
  proxyPort: 8888,
  currentPort: 8850,
  portStart: 8850,
  portEnd: 8865,
};

// Just gives the next port number.
nextPort = function() {
  var next = data.currentPort++;
  next = (next > data.portEnd) ? data.portStart : next;
  data.currentPort = next;
  return data.currentPort;
};

// A hash of Connect.SIDs for sticky sessions.
data.routes = {}

var svr = httpProxy.createServer(function (req, res, proxy) {

  var port = false;

  // parseCookies is just a little function
  // that... parses cookies.
  var cookies = parseCookies(req);  

  // If there is an SID passed from the browser.
  if (cookies['connect.sid'] !== undefined) {

    var ip = req.connection.remoteAddress;

    if (data.routes[cookies['connect.sid']] !== undefined) {

      // If there is already a route assigned to this SID,
      // make that route's port the assigned port.
      port = data.routes[cookies['connect.sid']].port;
    } else {

      // If there isn't a route for this SID,
      // create the route object and log its
      // assigned port.
      port = data.currentPort;
      data.routes[cookies['connect.sid']] = {
        port: port,
      }

      nextPort();
    }

  } else {

    // Otherwise assign a random port, it will/
    // pick up a connect SID on the next go.
    // This doesn't really happen.
    port = nextPort();
  }

  // Now that we have the chosen port, 
  // proxy the request.
  proxy.proxyRequest(req, res, {
    host: '127.0.0.1',
    port: port
  });
}).listen(data.proxyPort);

// Now we handle WebSocket requests.
// Basically, I feed off of the above route
// logic and try to route my WebSocket to the
// same server regular requests are going to.
svr.on('upgrade', function (req, socket, head) {

  var cookies = parseCookies(req);  
  var port = false;

  // Make sure there is a Connect.SID,
  if (cookies['connect.sid'] != undefined) {

    // Make sure there is a route...
    if (data.routes[cookies['connect.sid']] !== undefined) {

      // Assign the appropriate port.
      port = data.routes[cookies['connect.sid']].port;
    } else {

      // this has never, ever happened, i've been logging it.
    }
  } else {

    // this has never, ever happened, i've been logging it.
  };

  if (port === false) {

    // this has never happened...
  };

  // So now route the WebSocket to the same port
  // as the regular requests are getting.
  svr.proxy.proxyWebSocketRequest(req, socket, head, {
    host: 'localhost',
    port: port
  });

});

Client Side / The Phenomena

Socket connects like so:

var socket = io.connect('http://whatever:8888');

After about 10 seconds on logging on, I get this error back on this listener, which doesn't help much.

socket.on('error', function (data) {
  // this is what gets triggered. ->
  // Firefox can't establish a connection to the server at ws://whatever:8888/socket.io/1/websocket/Nnx08nYaZkLY2N479KX0.
});

The Socket.IO GET request that the browser sends never comes back - it just hangs in pending, even after the error comes back, so it looks like a timeout error. The server never responds.

Server Side - A Worker

This is how a worker receives a socket request. Pretty simple. All workers have the same code, so you think one of them would get the request and acknowledge it...

app.sio.socketio.sockets.on('connection', function (socket) {
  // works... some of the time! all of my workers run this
  // exact same process.
});

Summary

That's a lot of data, and I doubt anyone is willing to confront it, but i'm totally stumped, don't know where to check next, log next, whatever, to solve it. I've tried everything I know to see what the problem is, to no avail.

UPDATE

Okay, I am fairly certain that the problem is in this statement on the node-http-proxy github homepage:

node-http-proxy is <= 0.8.x compatible, if you're looking for a >= 0.10 compatible version please check caronte

I am running Node.js v0.10.13, and the phenomena is exactly as some have commented in github issues on this subject: it just drops websocket connections randomly.

I've tried to implement caronte, the 'newer' fork, but it is not at all documented and I have tried my hardest to piece together their docs in a workable solution, but I can't get it forwarding websockets, my Socket.IO downgrades to polling.

Are there any other ideas on how to get this implemented and working? node-http-proxy has 8200 downloads yesterday! Sure someone is using a Node build from this year and proxying websockets....

What I am look for exactly

I want to accomplish a proxy server (preferrably Node) that proxies to multiple node.js workers, and which routes the requests via sticky sessions based on a browser cookie. This proxy would need to stably support traditional requests as well as web sockets.

Or...

I don't mind accomplishing the above via clustered node workers, if that works. My only real requirement is maintaining sticky sessions based on a cookie in the request header.

If there is a better way to accomplish the above than what I am trying, I am all for it.

Crosscheck answered 4/12, 2013 at 6:21 Comment(14)
What version of Node are you using? Looks like it might be related to this: github.com/nodejitsu/node-http-proxy/pull/402 - There is a pull request with a fix, but apparently that doesn't resolve all of the issues.Salaried
v0.10.13 - Is that too old?Crosscheck
Honestly not too sure where things are at with it. It mentions in the comments that they are still having problems with 0.10.13.Salaried
Could you explain exactly what you're trying to accomplish with this? Maybe there's a better way to do things.Febrifacient
@Febrifacient Sure. I want to accomplish a proxy server (preferrably Node) that proxies to multiple node.js workers, and which routes the requests via sticky sessions based on a browser cookie. This proxy would need to stably support traditional requests as well as web sockets.Crosscheck
Since the objective of the proxying appears to be scaling, is there a specific reason you want to proxy instead of clustering?Indonesia
@Indonesia I don't mind changing my strategy to clustering - you are right that I am proxying for scaling purposes. I simply couldn't figure out how to achieve sticky sessions with clustering, i.e. how does the master process route to the right forked worker based on the request's session id cookie. Sticky sessions is my only real requirement that's holding me up from 1000 solutions. Do you know how to achieve this?Crosscheck
Sorry for asking but why do you need sticky sessions in the first place? If it's for authentication, it is usually possible to solve that by using a redis store.Stepmother
@jtblin No need for apologies! The only reasons I didn't already toast sticky sessions and set up redis as you mentioned is 1) I am on a Windows environment and redis isn't as production ready (but it does work), and 2) I don't know how to handle a particular websocket problem created by losing sticky sessions: When something in my app updates, it runs a pub/sub with all other browser instances of a user's session. For example, if a user had 5 browser windows open, they would all be authenticated through the same cookie, but would hold 5 different web socket connections to the server.Crosscheck
When you do something, say hit 'change user' in my app, the Node worker looks up all sockets assigned to that session id, loops through them and publishes the message to each browser window, instantly 'logging out' all windows. Websocket connections are living, breathing things, not something you can throw in a redis hashmap. I don't know how to distribute instant pub/sub communications between Node workers without getting into some complicated hack. I could be totally wrong though - I am sure there is a standard way to do this. I just don't know it. I'm quite agnostic on finding a solution.Crosscheck
Pub/sub systems go far beyond redis. Though I feel redis is a great fit because its so versatile. You mention that if the user has multiple windows open each window/tab would have a unique websocket connection. I feel like the philosophy of realtime web applications is kind of to reuse the single page as much as possible to reduce redundant data and update the page in realtime. Consider offering your services to be easily viewed on a single page. Maybe consider making the change on how your front-end works?Febrifacient
All in all, shouldn't your UI/UX design not require multiple windows open? But rather reuse the same single page?Febrifacient
@Febrifacient That is unfortunately one thing I can't do, but perhaps you are misunderstand why I am doing that. Having multiple windows isn't a liability on my part, its a major feature of my application. My SPA supports 'apps' and 'applets', portal-like, I guess, but not so damn rigid. Any app or applet can be moved all around the thing, or opened as its own window, popped out, you name it, and its all URL routed, so its quite flexible. The popped out versions only load that code necessary to run, not entire SPA payload. Think Gmail chat, but with all pieces flexible like that.Crosscheck
I have not done anything exactly the same as you talk about, but I have done some similar things with bouncy.Ingeminate
P
4

In general I don't think node is not the most used option as a proxy server, I, for one use nginx as a frontend server for node and it's a really great combination. Here are some instructions to install and use the nginx sticky sessions module.

It's a lightweight frontend server with json like configuration, solid and very well tested.

nginx is also a lot faster if you want to serve static pages, css. It's ideal to configure your caching headers, redirect traffic to multiple servers depending on domain, sticky sessions, compress css and javascript, etc.

You could also consider a pure load balancing open source solution like HAProxy. In any case I don't believe node is the best tool for this, it's better to use it to implement your backend only and put something like nginx in front of it to handle the usual frontend server tasks.

Panicle answered 8/12, 2013 at 20:33 Comment(7)
Thanks for the response. Its funny, right now nginx is exactly what I am looking at. My only problem with this is that ngingx is 100% optimized for linux and my Node.js server is unfortunately running in a windows environment. So to get this working, I am having to run nginx on a Hyper-V VM, which adds complications to an otherwise simple setup. Additionally, I want nginx to be a static file server, but that means I have to put my web directory on the nginx box....Crosscheck
Sounds good, is it for a high volume application? The windows version of nginx might already give a higher throughput than the one needed, or comparable to a unix version running on a VM on top of windows, the only way would be to try it out. If the VM is directly on top of the metal then it will be faster, otherwise it might not be worth it. Hyper-V probably can be configured to have access to a folder on the host operating system, at least other VM's can, in order to more conveniently update the site without having to copy files inside the VM, I am not sure if this could make it slower.Panicle
Yes, I believe it is high volume if you take into account the amount of hardware available. It is an internal Single Page App that is running which will probably download a payload of about 500kb in files on page load once optimized, used by 100 - 200 users. To host this, we have one server per organization with 8 - 16 amd cores. This server is also acting as the database server and this SPA is very data intensive. Does that seem like a relatively heavy load? I have little comparisons, so either it is, or something else is very wrong.Crosscheck
Maybe I just host Node.js on the unix VM as well... It seems the idea of accessing a folder on the raw server from the VM as a via and server that would slow it down....Crosscheck
It should be OK, if you configure nginx to serve the single page app with caching headers with a a long expiration date (days/weeks) it means the clients will only download it once every so days. The users won't be using it all the time either. I don't know if it's feasible but I think such a huge organization for an application with hundreds of users would have a unix box somewhere you could use, or just buy one. It's hard to say, the best is to to measure it early with tools like ab - Apache Benchmark httpd.apache.org/docs/2.2/programs/ab.htmlPanicle
Good points. On the unix box, this setup I described above has already been replicated in about 25 organizations like the above, and so the hardware is already in place. This is new software going in. As there will be another 100 organizations like this going in in the future, I can bring it up to people, but I wouldn't count on implementing it. We are trying to make it as simple click-in and low-maintenance as possible, as the systems admins in these places can't be expected to be the greatest or even know how to log in to a unix machine.Crosscheck
@jhadesdev This worked out for me. Ended up using nginx on linux VM. +250Josie
F
3

I agree with hexacyanide. To me it would make the most sense to queue workers through a service like redis or some kind of Message Query system. Workers would be queued through Redis Pub/Sub functionality by web nodes(which are proxied). Workers would callback upon error, finish, or stream data in realtime with a 'data' event. Maybe check out the library kue. You could also roll your own similar library. RabbitMQ is another system for similar purpose.

I get using socket.io if you're already using that technology, but you need to use tools for their intended purpose. Redis or a MQ system would make the most sense, and pair great with websockets(socket.io) to create realtime, insightful applications.

Session Affinity(sticky sessions) is supported through Elastic LoadBalancer for aws, this supports webSockets. A PaaS provider(Modulus) does this exactly. Theres also satalite which provides sticky sessions for node-http-proxy, however I have no idea if it supports webSockets.

Febrifacient answered 8/12, 2013 at 3:28 Comment(3)
Thank you, those are a lot of good ideas, and I am looking at each of them to see if they are able to work out. One question - you say I need to use tools intended for their purpose - what is it in my above discourse that is improper usage? I am curious. Servide side optimization is not my specialty so I am wondering if what I am doing has something totally hokey going on with it.Crosscheck
Maybe I'm being a bit close minded, but I'm under the assumption that you're assigning worker nodes jobs via websockets through your proxy server. First of all I don't think a proxy is best used as an IPC medium. I also don't feel as though websockets are good for this purpose. As I feel that, in their current state of development, are geared for this purpose. I don't discredit your idea as interesting, and I'm all for pushing technology to innovate, but I don't see the pros of your specific usage of said technologies for your intended use.Febrifacient
All in all, I also agree with jhadesdev's answer, though only partially. A PaaS with node-http-proxy support is great for reasons of flexibility, however you'll get the most benefit from a technology thats specifically geared towards loadbalancing from top to bottom. Node isn't a good load balancer, its great that you can program its specific actions, but there are better solutions. Nginx is a good one, but most people deploy on the cloud nowadays. It wouldn't hurt to touch up on AWS loadbalancing services. You could also deploy on Modulus, they offer session affinity and websockets.Febrifacient
Q
2

I've been looking into something very similar to this myself, with the intent of generating (and destroying) Node.js cluster nodes on the fly.

Disclaimer: I'd still not recommend doing this with Node; nginx is more stable for the sort of design architecture that you're looking for, or even more so, HAProxy (very mature, and easily supports sticky-session proxying). As @tsturzl indicates, there is satellite, but given the low volume of downloads, I'd tread carefully (at least in a production environment).

That said, since you appear to have everything already set up with Node, rebuilding and re-architecting may be more work than it's worth. Therefore, to install the caronte branch with NPM:

  1. Remove your previous http-node-proxy Master installation with npm uninstall node-proxy and/or sudo npm -d uninstall node-proxy

  2. Download the caronte branch .zip and extract it.

  3. Run npm -g install /path/to/node-http-proxy-caronte
  4. In my case, the install linkage was broken, so I had to run sudo npm link http-proxy

I've got it up and running using their basic proxy example -- whether or not this resolves your dropped sessions issue or not, only you will know.

Quesenberry answered 11/12, 2013 at 17:2 Comment(13)
thanks for your answer! I am actually very glad you answered with this as I was looking for someone else who tried caronte. I got the caronte branch installed already (I mention this in my answer above), but I couldn't get it to forward Web Sockets. My requests kept downgrading to long polling. The documentation is very skimpy, so perhaps I didn't understand it, though I did look through the source code and attempt to. Is there any way I could see your working example so perhaps I could see what I did wrong?Crosscheck
To answer your other point on nginx, I do have ngingx up and running now, but this is running on a unix hyper-v vm on my server and I am looking for a cleaner alternative deployment for smaller sites I will be installing this in, so am eager to see caronte work.Crosscheck
I admittedly didn't try with web sockets - I guess I misread and thought you were having trouble installing it. I'll play around some more and see if I can get web sockets forwarding. I agree with you about nginx too -- powerful, but not nearly as lightweight and portable.Quesenberry
Thanks - if you could figure it out it would be tremendously helpful.Crosscheck
Are you able to flesh out your implementation of the client and server? I'm trying to piece together what you've built, but without knowing, I might do something completely different.Quesenberry
Its pretty simple for the purpose of this - client sends traditional xhr requests and websocket requests to a single server on port x, as designated by me in a config directory. The XHR and websockets have to go to the same place. Listening to port X is either nginx, a small node.js file running node-http-proxy or a single instance of one of my workers if I don't want to load balance. My node-http-proxy file is literally the code I pasted above (minus boilerplate) in my question.Crosscheck
Node workers listen on whatever port I say, authenticate request based on connect.sid, and then handle the request, whatever it is, and returns it back upstream. Is there a particular question you had outside of this scope? Just need a clearer idea of what you are asking.Crosscheck
Sorry, I meant your example code -- you have the proxy code but only snippets of client/server. (I'm trying to recreate a sandbox of your environment to get the sockets working, but it'll be easier if I can at least see the wrapping and requires of your client and server)Quesenberry
Unless i'm misunderstanding, I think those snippits are all that are needed to implement what would reproduce the problem. Ignore my whole app.sio.socketio.sockets.on thing - that's confusing - I've just abstracted socket.io on my server and extended Express's router onto it, so server side should literally be the basic code to get socket io working, and client side the same thing. The key to reproducing the problem is in the proxy.Crosscheck
Ahh ok. That makes more sense. Thanks. Once more unto the breach.Quesenberry
let us continue this discussion in chatQuesenberry
Grt - I answered your chat.Crosscheck
Replied to your email, not sure you received it? Lemme know.Quesenberry

© 2022 - 2024 — McMap. All rights reserved.