7

NOTE: I have solved most of the problem, but am still encountering an issue with catching the disconnects as noted towards the bottom of this post in the Update section.

NOTE 2: As requested I have posted a more complete view of my setup. See the heading at the bottom of this post.

I am trying to set up a load balancer in Apache but it is not working for socket.io. My Apache code looks like this:

<VirtualHost *:80>
        ServerAdmin webmaster@example.com
        ServerName jpl.example.com

        ProxyRequests off

        Header add Set-Cookie "ROUTEID=.%{BALANCER_WORKER_ROUTE}e; path=/" env=BALANCER_ROUTE_CHANGED
        <Proxy "balancer://mycluster">
                BalancerMember "http://203.0.113.22:3000" route=1
                BalancerMember "http://203.0.113.23:3000" route=2
        </Proxy>

        ProxyPass "/test/" "balancer://mycluster/"
        ProxyPassReverse "/test/" "balancer://mycluster/"    

</VirtualHost>

Problems with socket.io

The issue I am facing is that on the backend I have a node.js server that uses socket.io connections for long polling in both subdir1/index.html and subdir2.index.html. Unfortunately, socket.io likes to be only running from the root directory:

http://203.0.113.22:3000/socket.io/

It is unable to find it if I try running it from:

http://jpl.example.com/test/socket.io

The start of my index.js file on the server looks like this:

// Setup basic express server
var express = require('express');
var app = express();
var server = require('http').createServer(app);
var io = require('socket.io')(server);

Part of my /subdir1/index.html (also being loaded from the server) originally looked like this:

<script src="/socket.io/socket.io.js"></script>
<script>
    var socket = io.connect();
    socket.on('notification', function (data) {

But I was now getting an error when accessing it through the proxy. The error was:

http://jpl.example.com/socket.io/socket.io.js 404 (Not Found)

I have tried changing it to this:

<script src="/test/socket.io/socket.io.js"></script>
<script src="http://code.jquery.com/jquery-latest.min.js"></script>
<script>
    var refresh_value = 0;
    var refresh_time = 0;
    //var socket = io.connect();
    var socket = io.connect('http://example.com/', {path: "/test/"});
    socket.on('notification', function (data) {

It no longer gives me an error, but there is no indication that it is communicating with the socket.

What am I doing wrong here and how can I get this to work?

Update

I have now mostly solved the problem with using:

var socket = io.connect('http://example.com/', {path: "/test/socket.io"});

instead of:

var socket = io.connect('http://example.com/', {path: "/test/"});

Final problem:

Things are now working but I am still experiencing the following issue:

It takes about a minute before it detects that a client has actually closed a page. Without a Proxy and Apache load balancer I do not have this issue. I have tried various things such as setting KeepAlive to "no" and modifying the VirtualHost at the top of this page with the following:

        <Proxy "balancer://mycluster">
                BalancerMember "http://203.0.113.22:3000" route=1 max=128 ttl=300 retry=60 connectiontimeout=5 timeout=300 ping=2
                BalancerMember "http://203.0.113.23:3000" route=2 max=128 ttl=300 retry=60 connectiontimeout=5 timeout=300 ping=2
        </Proxy>

But it still takes about a minute before it recognizes that a client has left the page. What can I do to solve this problem?

A more complete view of my setup

As requested, to help diagnose the problem I am posting a more complete view of my setup. I have eliminated as much as I thought I could while providing as much detail as I could:

My current Apache file:

<VirtualHost *:80>
    # Admin email, Server Name (domain name), and any aliases
    ServerAdmin webmaster@example.com
    ServerName jpl.example.com

    ProxyRequests off

    Header add Set-Cookie "ROUTEID=.%{BALANCER_WORKER_ROUTE}e; path=/" env=BALANCER_ROUTE_CHANGED
    <Proxy "balancer://mycluster">
        BalancerMember "http://203.0.113.22:3000" route=1 keepalive=On smax=1 connectiontimeout=10 retry=600 timeout=900 ttl=900
        BalancerMember "http://203.0.113.23:3000" route=2 keepalive=On smax=1 connectiontimeout=10 retry=600 timeout=900 ttl=900
        ProxySet stickysession=ROUTEID
    </Proxy>

    <Proxy "balancer://myws">
        BalancerMember "ws://203.0.113.22:3000" route=1 keepalive=On smax=1 connectiontimeout=10 retry=600 timeout=900 ttl=900
        BalancerMember "ws://203.0.113.23:3000" route=2 keepalive=On smax=1 connectiontimeout=10 retry=600 timeout=900 ttl=900
        ProxySet stickysession=ROUTEID
    </Proxy>

    RewriteEngine On
    RewriteCond %{REQUEST_URI}  ^/test/socket.io                [NC]
    RewriteCond %{QUERY_STRING} transport=websocket        [NC]
    RewriteRule /(.*)           balancer://myws/$1 [P,L]

    ProxyPass "/test/" "balancer://mycluster/"
    ProxyPassReverse "/test/" "balancer://mycluster/"

</VirtualHost>

On each of those servers I have a node installation. The main index.js looks like this:

/************ Set Variables ******************/

// Setup basic express server
var express = require('express');
var app = express();
var server = require('http').createServer(app);
var io = require('socket.io')(server);
var port = process.env.PORT || 3000;
var fs                  = require('fs'),
    mysql               = require('mysql'),
    connectionsArray    = [],
    connection          = mysql.createConnection({
        host        : 'localhost',
        user        : 'myusername',
        password    : 'mypassword',
        database    : 'mydatabase',
        port        : 3306
    }),
    POLLING_INTERVAL = 5000;


server.listen(port, function () {
        console.log("-----------------------------------");
        console.log('Server listening at port %d', port);
});

// Routing
app.use(express.static(__dirname + "/public"));


/*********  Connect to DB ******************/
connection.connect(function(err) {
        if (err == null){
                console.log("Connected to Database!");
        }
        else {
                console.log( err );
                process.exit();
        }
});



/***********************  Looping *********************/

var pollingLoop = function () {

        var query = connection.query('SELECT * FROM spec_table'),
        specs = [];

        query
        .on('error', function(err) {
                console.log( err );
                updateSockets( err );
        })

        .on('result', function( spec ) {
                specs.push( spec );
        })

        .on('end',function(){
                pollingLoop2(specs);
        });

};

var pollingLoop2 = function (specs) {

        // Make the database query
        var query = connection.query('SELECT * FROM info_table'),
        infos = [];

        // set up the query listeners
        query
        .on('error', function(err) {
                console.log( err );
                updateSockets( err );
        })

        .on('result', function( info ) {
                infos.push( info );
        })

        .on('end',function(){
                if(connectionsArray.length) {
                        setTimeout( pollingLoop, POLLING_INTERVAL );
                        updateSockets({specs:specs, infos:infos});
                }
        });

};

/***************  Create new websocket ****************/
//This is where I can tell who connected and who disconnected.

io.sockets.on('connection', function ( socket ) {

        var socketId = socket.id;

        var clientIp = socket.request.connection.remoteAddress;

        var time = new Date();
        console.log(time);
        console.log("\033[32mJOINED\033[0m: "+ clientIp + " (Socket ID: " + socketId + ")");

        // start the polling loop only if at least there is one user connected
        if (!connectionsArray.length) {
                pollingLoop();
        }

        socket.on('disconnect', function () {
                var socketIndex = connectionsArray.indexOf( socket );

                var time = new Date();
                console.log(time);
                console.log("\033[31mLEFT\033[0m: "+ clientIp + " (Socket ID: " + socketId + ")");

                if (socketIndex >= 0) {
                        connectionsArray.splice( socketIndex, 1 );
                }
                console.log('    Number of connections: ' + connectionsArray.length);
        });

        connectionsArray.push( socket );
        console.log('    Number of connections: ' + connectionsArray.length);

});


/********* Function updateSockets ************/

var updateSockets = function ( data ) {

        connectionsArray.forEach(function( tmpSocket ){
                tmpSocket.volatile.emit( 'notification' , data );
        });

};

Finally, in my public/dir1/index.html file I have something that looks like this:

//HTML code here
<script src="/test/socket.io/socket.io.js"></script>
<script>
    var socket = io.connect('', {path: "/test/socket.io"});
    socket.on('notification', function (data) {
            $.each(data.specs,function(index,spec){
                    //Other js code here
            })
    })
</script>
//More HTML code here

With this particular setup the connection works, but it takes over a minute before I can detect that a page is closed. Also, with this set up there is an error logged to the console:

WebSocket connection to 'ws://jpl.example.com/test/socket.io/?EIO=3&transport=websocket&sid=QE5aCExz3nAGBYcZAAAA' failed: Connection closed before receiving a handshake response
ws @ socket.io.js:5325

What am I doing wrong and how can I fix my code so that I can detect disconnects the moment they occur?

Note: It works just fine if I do not use a subdirectory /test/.

Please also note: this is only a subdirectory appearing in the URL. It does not exist in the file system anywhere.

Also, I am open to tips and suggestions if you notice areas in my code that I could be writing better.

kojow7
  • 10,308
  • 17
  • 80
  • 135
  • Do you have a minimal git repo handy for testing this? Would be fast to resolve your issue – Tarun Lalwani Mar 27 '18 at 03:33
  • No unfortunately I do not, and am not familiar enough with git to get it set up in time. – kojow7 Mar 27 '18 at 03:36
  • No worries, just share the client and server files, so I use same code as yours – Tarun Lalwani Mar 27 '18 at 04:38
  • Well, I have now gotten it working without using a subdirectory. I will just use that for my demo tomorrow and then go about getting a minimal version of my setup to post here. Thank you for your help! – kojow7 Mar 27 '18 at 05:09
  • @TarunLalwani I have now posted a more complete version of the files. Please let me know if this is enough for you. – kojow7 Mar 31 '18 at 02:40
  • Is `route=1` a typo while posting? Because the 2nd one should have `route=2` – Tarun Lalwani Mar 31 '18 at 05:59
  • Ah, yes, it is a typo. My original script has both 1 and 2. I changed my real IP address when posting it on S.O. and inadvertently changed the route number as well. – kojow7 Mar 31 '18 at 20:35
  • Can you check in network tab that your socket is switching to longpolling instead of websocket mode? Because I just did the setup and one thing I observed was that it not connecting in websocket mode and rather longpolling mode – Tarun Lalwani Apr 01 '18 at 08:19
  • @TarunLalwani I am not quite sure I am understanding. What networking tab do you mean? And what should I be checking for? As far as I understand it is supposed to be using longpolling mode because it needs to push updated data to the client every few seconds. Though to be honest I do not completely understand the differences between websocket and long polling. – kojow7 Apr 01 '18 at 22:20
  • what you mean by this line RewriteRule /(.*) balancer://myws/$1 [P,L] if you aim is to redirect a URI /test/socket.io with query string transport=websocket to balancer://myws/test/socket.io?transport=websocket your rules are not correct – Mohammed Elhag Apr 01 '18 at 22:52
  • @MohammedElhag Should that not work if my RewriteCond is specifically checking for any lines that match `^/test/socket.io` ? – kojow7 Apr 01 '18 at 23:04
  • @kojow7 it capture all URI start with /test/socket.io and has quert string transport=websocket , but the rule after that will remove /test in new uri that why i asked you , give what you expected to be after balancer://myws/ – Mohammed Elhag Apr 01 '18 at 23:07
  • @MohammedElhag So, you are suggesting I change it to this: `RewriteRule /test/(.*) balancer://myws/test/$1 [P,L]` ? – kojow7 Apr 01 '18 at 23:14
  • @kojow7 if that your target , change the last line to RewriteRule ^(.*)$ balancer://myws/$1 [P,L] – Mohammed Elhag Apr 01 '18 at 23:18
  • @kojow7 clear browser cache first then test – Mohammed Elhag Apr 01 '18 at 23:19
  • @MohammedElhag That seems to be not much different than my original one except that you add the `$`. – kojow7 Apr 01 '18 at 23:23
  • @kojow7 your original is match after test see what you wrote RewriteRule then /(.*) and in /test/something , rule will match something , in RewriiteRule don't start with / to match URI but in RewriteCond you could match by slash so in RewriteRule ^(.*)$ here you will match /test/something – Mohammed Elhag Apr 01 '18 at 23:28

3 Answers3

3

So after some hit and trials, I was able to get a config which works fine. The changes required

Base Path on Server

You need to use the base path on server as well to make this smooth

var io = require('socket.io')(server, { path: '/test/socket.io'});

And then below is the updated Apache config I used

<VirtualHost *:8090>
    # Admin email, Server Name (domain name), and any aliases
    ServerAdmin webmaster@example.com

    ProxyRequests off

   #Header add Set-Cookie "ROUTEID=.%{BALANCER_WORKER_ROUTE}e; path=/" env=BALANCER_ROUTE_CHANGED
   Header add Set-Cookie "SERVERID=sticky.%{BALANCER_WORKER_ROUTE}e; path=/" env=BALANCER_ROUTE_CHANGED

    <Proxy "balancer://mycluster">
        BalancerMember "http://127.0.0.1:3001" route=1 keepalive=On smax=1 connectiontimeout=10 retry=600 timeout=900 ttl=900
        BalancerMember "http://127.0.0.1:3000" route=2 keepalive=On smax=1 connectiontimeout=10 retry=600 timeout=900 ttl=900
        ProxySet stickysession=SERVERID
    </Proxy>

    <Proxy "balancer://myws">
        BalancerMember "ws://127.0.0.1:3001" route=1 keepalive=On smax=1 connectiontimeout=10 retry=600 timeout=900 ttl=900
        BalancerMember "ws://127.0.0.1:3000" route=2 keepalive=On smax=1 connectiontimeout=10 retry=600 timeout=900 ttl=900
        ProxySet stickysession=SERVERID
    </Proxy>

    RewriteEngine On
    RewriteCond %{HTTP:Upgrade} =websocket [NC]
    RewriteRule /(.*) balancer://myws/$1 [P,L]

    RewriteCond %{HTTP:Upgrade} !=websocket [NC]
    RewriteRule /(.*)                balancer://mycluster/$1 [P,L]
    ProxyTimeout 3
</VirtualHost>

And now the disconnects are immediate

Disconnect

Tarun Lalwani
  • 142,312
  • 9
  • 204
  • 265
  • I have implemented your changes and unfortunately it does not work. I was trying to get it to work for a virtual subdirectory of "test" (by virtual I mean that it does not exist on the file system, only in the URL). If I go to http://jpl.example.com/test/dir1/index.html it does not work because the "/test/" was removed from apache config file in your example. I already had it working without the /test/ in the URL. Also, do I not need to use ProxyPass and ProxyReverse to properly implement a load balancer? Thank you for your help! – kojow7 Apr 01 '18 at 22:47
  • No, you don't need to us ProxyPass in this case, i found that using that creates a problem and for hosting the files in virtual `/test`, you can change the static inclusion to `app.use('/test', express.static(__dirname + "/public"));` – Tarun Lalwani Apr 02 '18 at 05:26
  • That works!!! Now the question, what problem was using ProxyPass causing? – kojow7 Apr 02 '18 at 14:09
  • I think the import bit is that when `Upgrade` is set to `websocket`, we should be using `ws` and for all other request `http`. So may be `ProxyPass` combo may also work as along as we can satisfy that condition. But I focused more on getting a config that works, rather getting all possible combinations that work – Tarun Lalwani Apr 02 '18 at 14:13
  • The following page: https://httpd.apache.org/docs/2.4/rewrite/avoid.html says "Note that whether you use RewriteRule or ProxyPass, you'll still need to use the ProxyPassReverse directive to catch redirects issued from the back-end server." Does that mean I'll run into issues by not having ProxyPassReverse? – kojow7 Apr 02 '18 at 15:04
  • That is only if you do a redirect and and since we incorporated the `/test` path in our static as well as `socket.io` it shouldn't happen. But you can still add it to be safe as it won't create a problem – Tarun Lalwani Apr 02 '18 at 15:14
  • Thank you for all your help! – kojow7 Apr 03 '18 at 16:04
1

Im not that familiar with Apaches mod_proxy, but I think your issue is related to your paths.

I setup a little test to see if I could help (and have a play), in my test I will proxy both HTTP and ws traffic to a single backend. Which is what your doing plus websockets.

Servers (LXD Containers):

  • 10.158.250.99 is the proxy.
  • 10.158.250.137 is the node.

First, enable the apache mods on the proxy:

sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod proxy_wstunnel
sudo a2enmod proxy_balancer
sudo a2enmod lbmethod_byrequests

Then change 000-default.conf:

sudo nano /etc/apache2/sites-available/000-default.conf

This is what I used after clearing out the comments:

<VirtualHost *:80>
    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/html

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined

    <Proxy balancer://mycluster>
        BalancerMember http://10.158.250.137:7779
    </Proxy> 

    ProxyPreserveHost On

    # web proxy - forwards to mycluser nodes
    ProxyPass /test/ balancer://mycluster/
    ProxyPassReverse /test/ balancer://mycluster/

    # ws proxy - forwards to web socket server
    ProxyPass /ws/  "ws://10.158.250.137:7778"

</VirtualHost>

What the above config is doing:

  • Visit the proxy http://10.158.250.99 it will show default Apache page.
  • Visit the proxy http://10.158.250.99/test/ it will forward the HTTP request to http://10.158.250.137:7779.
  • Visit the proxy http://10.158.250.99/ws and it will make a websocket connection to ws://10.158.250.137:7778 and tunnel it though.

So for my app im using phptty as it uses HTTP and WS, its uses xterm.js frontend which connects to websocket http://10.158.250.99/ws to give a tty in the browser.

Here Is a screen of it all working, using my LXDui electron app to control it all.

enter image description here

So check your settings against what I have tried and see if it's different, its always good to experiment abit to see how things work before trying to apply them to your idea.

Hope it helps.

Lawrence Cherone
  • 46,049
  • 7
  • 62
  • 106
  • Thank you for your response. I am using socket.io rather than websockets. To be honest, I am not quite sure I understand what the difference is. But, I am not using the ws protocol. In your example, it would seem that the ws address is not being load balanced. In my scenario I have a node.js app which utilizes socket.io. This entire node.js app and socket.io library is duplicated on at least two servers. So I need the entire app including socket.io to be load balanced. – kojow7 Mar 25 '18 at 05:14
  • On a side note what did you use to make this gif file? – Tarun Lalwani Apr 02 '18 at 23:08
  • @LawrenceCherone, thanks. Seems like it is not for Mac. – Tarun Lalwani Apr 03 '18 at 09:00
0

I think your delay problem to detect the client has closed the page comes from your default kernel tcp keepalive configuration of your proxy apache node. I think in your system, if you check the value of sys.net.ipv4.tcp_keepalive_time, you may have the value 60 that should be the 60 seconds waited before the first keepalive packet is sent to detect if the client has closed the connection. From your problem details, mod_proxy looks to have an issue because it seems to not forward the RST packet that you correctly manage without the mod_proxy module. Without solving that forward RST packet issue on mod_proxy, you may only be able to reduce the delay by decreasing the parameter tcp_keepalive_time in example to 5, to wait up to 5 second before to start to check if the tcp connection is closed. Check also the number of failed keepalive probes parameters before to state the connection has been closed, it could also impact the total delay. This is tcp_keepalive_probes parameter.