Upstream get stuck in disabled state for websocket load balancing

Oleg Pisklov nginx-forum at forum.nginx.org
Fri Jul 9 13:13:37 UTC 2021


My current nginx configuration looks like:

worker_processes 1;
error_log /dev/stdout debug;
events { worker_connections 1024; }
http {
  upstream back {
      server backend1 max_fails=1 fail_timeout=10;
      server backend2 max_fails=1 fail_timeout=10;
  }
  server {
      listen 80;
      location / {
          proxy_pass          http://back;
          proxy_http_version  1.1;
          proxy_set_header    Host $host;
          proxy_set_header    Upgrade $http_upgrade;
          proxy_set_header    Connection "Upgrade";
          proxy_read_timeout  3600s;
          proxy_send_timeout  3600s;
      }
  }
}

nginx version: nginx/1.21.1

Backend is a simple websocket server, which accepts incoming connection and
then does nothing with it.

---

# Test scenario:

1. Run nginx and backend1 server, backend2 should stay down.
2. Run several websocket clients
   Some of them try to connect to backend2 upstream, and nginx writes
("connect() failed (111: Connection refused) while connecting to upstream"
and "upstream server temporarily disabled while connecting to upstream") to
log, which is expected.
3. Run backend2 and wait some time (to outwait fail_timeout).
4. Close websocket connections on backend1 side and wait for clients to
reconnect.

Then a strange thing happens. I expect that new websocket connections will
be distributed evenly among two backends. But most of them land on backend1,
as if backend2 is still disabled. Sometimes there is one client that
connects to backend2, but it's the only one.
Further attempts to close connections on server side show the same picture.
I found that setting max_fails=0 solves the problem with distribution.

Is this correct behavior? If so, how to assure proper distribution of
websocket connection while using max_fails in such scenarios? Is there any
documentation for it?

---

Client/server code and docker-compose file used to reproduce this behavior
are below. Websocket clients can be disconnected by a command inside backend
container: ps aux | grep server.js | awk '{print $2}' | xargs kill -sHUP


# server.js

const WebSocket = require('ws');
const Url = require('url');
const Process = require('process');

console.log("Starting Node.js websocket server");
const wss = new WebSocket.Server({ port: 80 });

wss.on('connection', function connection(ws, request) {
  const uid = Url.parse(request.url, true).query.uid;

  ws.on('message', function incoming(message) {
    console.log('Received: %s', message);
  });

  ws.on('close', function close() {
    console.log('Disconnected: %s', uid)
  });

});

Process.on('SIGHUP', () => {
  console.log('Received SIGHUP');
  for (const client of wss.clients) client.terminate();
});

---

# client.js

const WebSocket = require('ws');
const UUID = require('uuid')

function client(uid) {
    const ws = new WebSocket('ws://balancer/?uid=' + uid);
    ws.on('open', function open() {
      ws.send(JSON.stringify({'id': uid}));
    });
    ws.on('close', function close() {
      setTimeout(client, 2, uid)
    });
}

for (let i = 0; i < 100; i++){
    uid = UUID.v4();
    client(uid)
}

--- 

# Dockerfile.node

FROM node
WORKDIR app
RUN npm install -y ws uuid
COPY *.js /app
CMD node server.js

---

# docker-compose.yaml

version: '3.4'
services:
  balancer:
    image: nginx:latest
    depends_on:
      - backend1
      - backend2
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
  backend1:
    build:
      dockerfile: Dockerfile.node
      context: .
  backend2:
    build:
      dockerfile: Dockerfile.node
      context: .
    command: bash -c "sleep 30 && node server.js"
  clients:
    build:
      dockerfile: Dockerfile.node
      context: .
    depends_on:
      - balancer
    command: node client.js

Posted at Nginx Forum: https://forum.nginx.org/read.php?2,292014,292014#msg-292014



More information about the nginx mailing list