nestjs / nest

A progressive Node.js framework for building efficient, scalable, and enterprise-grade server-side applications with TypeScript/JavaScript 🚀
https://nestjs.com
MIT License
67.33k stars 7.59k forks source link

Cluster mode and Socketio sticky sessions #14062

Open Bubuioc opened 1 week ago

Bubuioc commented 1 week ago

Is there an existing issue for this?

Current behavior

Please someone. How can i implement sticky session for a Nest app running in cluster mode.The Nest docs have no info about this and Socketio have no docs for nest(.I have posted a question on Socketio github discussions and Nest discord but nothing.I already implemented adapter but the sticky sessions from @socket.io/sticky i can't setup.Maybe someone did this.

Minimum reproduction code

a

Steps to reproduce

No response

Expected behavior

This is main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';
import { Logger, ValidationPipe } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { SocketIOAdapter } from './core/socket/adapters/socket.io.adapter';
import * as cluster from 'cluster';
import * as os from 'os';
const clusterInstance = cluster as unknown as cluster.Cluster;
import { NODE_ENV } from './common/enums/node.env.enum';

async function bootstrap() {
  const logger = new Logger();
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
  app.enableCors({
    origin: '*',
  });

  const socketIOAdapter = new SocketIOAdapter(app);
  app.useWebSocketAdapter(socketIOAdapter);
  const configService: ConfigService = app.get(ConfigService);

  const port: number = configService.get('port');
  const nodeEnv: string = configService.get('NODE_ENV');

  if (nodeEnv !== NODE_ENV.PRODUCTION) {
    const config = new DocumentBuilder()
      .setTitle('Chat application')
      .setDescription('The corporative chat application written in Nest')
      .setVersion('1.0')
      .addBearerAuth({ type: 'http', name: 'accessToken' })
      .build();
    const document = SwaggerModule.createDocument(app, config);
    SwaggerModule.setup('api/docs', app, document);
  }

  await app.listen(port);

  logger.log(
    `Find docs on http://localhost:${port}/api/docs`,
    'NestApplication',
  );
}

const runningOnCluster: boolean =
  Object.values(NODE_ENV)
    .filter((env) => env !== NODE_ENV.LOCAL)
    .findIndex((env) => env === process.env.NODE_ENV) > -1;

if (runningOnCluster) {
  if (clusterInstance.isPrimary) {
    const cpuCount = os.cpus().length;
    for (let i = 0; i < cpuCount; i += 1) {
      clusterInstance.fork();
    }
    clusterInstance.on('online', (worker) => {
      Logger.log(`${worker.process.pid} is online`, 'NestApplication::Cluster');
    });
    clusterInstance.on('exit', ({ process }) => {
      Logger.log(`${process.pid} died`, 'NestApplication::Cluster');
    });
  } else {
    bootstrap();
  }
} else {
  bootstrap();
}

and this is redis streams adatper

import { INestApplicationContext } from '@nestjs/common';
import { IoAdapter } from '@nestjs/platform-socket.io';
import { Server, ServerOptions } from 'socket.io';
import { WsAuthMiddleware } from '../../../core/socket/middlewares/ws.auth.middleware';
import { createAdapter } from '@socket.io/redis-streams-adapter';
import { ConfigService } from '@nestjs/config';
import {
  DEFAULT_NAMESPACE,
  REDIS_CLIENT,
} from 'src/common/constants/constants';
import { WsQueryValidationMiddleware } from '../middlewares/ws.query.validation.middleware';
import { RedisClientType } from 'redis';

export class SocketIOAdapter extends IoAdapter {
  private configService: ConfigService;
  private redisClient: RedisClientType;
  private adapterConstructor: ReturnType<typeof createAdapter>;

  constructor(private app: INestApplicationContext) {
    super(app);
    this.configService = this.app.get(ConfigService);
    this.redisClient = this.app.get(REDIS_CLIENT);
    this._createClient();
  }

  private _createClient(): void {
    const streamName = this.configService.get<string>('redisConfig.streamName');
    this.adapterConstructor = createAdapter(this.redisClient, { streamName });
  }

  createIOServer(port: number, options?: ServerOptions): Server {
    const server: Server = super.createIOServer(port, {
      ...options,
      cors: {
        origin: (_req: any, callback: (arg0: null, arg1: boolean) => void) => {
          callback(null, true);
        },
        credentials: true,
      },
      transports: ['polling', 'websocket'],
      allowUpgrades: true,
    });
    server.adapter(this.adapterConstructor);

    server.of(DEFAULT_NAMESPACE).use(WsAuthMiddleware);
    server.of(DEFAULT_NAMESPACE).use(WsQueryValidationMiddleware);

    return server;
  }
}

Package

Other package

No response

NestJS version

No response

Packages versions

    "@nestjs/websockets": "^10.3.8",
    "@socket.io/redis-streams-adapter": "^0.2.2",
    "@socket.io/sticky": "^1.0.4",

Node.js version

No response

In which operating systems have you tested?

Other

No response

pasha-vuiko commented 5 days ago

I hope it doesn't sound offensive to you, but if you are using the node:cluster not for an experiment, but for a production app, that means you are doing something wrong. You should have scaling outside of an app, with Kubernetes for example. Node:cluster uses the Round Robin algorithm to balance the load between child processes, and it cannot implement something like sticky sessions

Bubuioc commented 4 days ago

I hope it doesn't sound offensive to you, but if you are using the node:cluster not for an experiment, but for a production app, that means you are doing something wrong. You should have scaling outside of an app, with Kubernetes for example. Node:cluster uses the Round Robin algorithm to balance the load between child processes, and it cannot implement something like sticky sessions

I am not a devops and have just some basic skills and knowledge about this(little of docker, github actions and some core priciples of kubernetes).But i have a personal server and i want my node app to run into cluster mode, because it is more efficient and I think is better than running multiple container or pods of same app on one server. PS:About your struggling of offensive thing: I created this issue to get help from people that are better than me, so any idea or any help is welcome, so, thanks a lot for your implication!

pasha-vuiko commented 4 days ago

Okay, look, the scaling of your app should be done outside of it, if we are talking about real world production scenarios. By outside, I mean by increasing the number of Docker containers with your app. Cluster mode is scaling inside the app which is wrong in most scenarios, because:

  1. It is not dynamic.
  2. It is not really efficient, because one of your processes works as a load balancer to redirect a load, so we have a situation when one of the processes is overloaded, and other processes are underloaded.
  3. It lacks advanced features, such as sticky sessions, which you need.

But if you still decide to go with the cluster approach, instead of the containers approach, I recommend you to get familiar with PM2, but I'm not sure if it has the sticky sessions functionality. BTW why do you need the sticky sessions?

Nosfistis commented 4 days ago

I have been using PM2 for a very long time (even before docker) and it does work with sticky sessions under nginx, which are useful for websocket connections.

That being said I do acknowledge that docker clustering is the best way.

Bubuioc commented 3 days ago

I have been using PM2 for a very long time (even before docker) and it does work with sticky sessions under nginx, which are useful for websocket connections.

That being said I do acknowledge that docker clustering is the best way.

Do you listen for each process to a new port?

Nosfistis commented 3 days ago

No, this is the advantage of node cluster, it's all in one port, so the nginx configuration is quite simple.

Bubuioc commented 3 days ago

No, this is the advantage of node cluster, it's all in one port, so the nginx configuration is quite simple.

Can you provide nginx configuration for cluster mode?

Nosfistis commented 3 days ago

Something like

    location ~ ^/socket.io/ {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_cache_bypass $http_upgrade;
    }

I mean, since there is no real clustering, there is only one port, and NodeJS does the "clustering" internally.

Bubuioc commented 2 days ago

I mean, since there

I tried this, isn't working as you said here is one port.

Bubuioc commented 2 days ago

Something like

    location ~ ^/socket.io/ {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_cache_bypass $http_upgrade;
    }

I mean, since there is no real clustering, there is only one port, and NodeJS does the "clustering" internally.

But I resolved it with pm2, I assign for each thread a new port ex:3000-3011 and setup sticky with nginx.