Enterprise-backend-API's met NestJS · Les

Realtime schalen met een Redis Pub/Sub-adapter

Synchroniseer socketstatus tussen meerdere instances met de Redis IoAdapter voor horizontale schaalbaarheid.

Les 4 van 413 stappen

Realtime schalen met een Redis Pub/Sub-adapter is een gratis Enterprise-backend-API's met NestJS-les op CoddyKit. Dit is les 4 van 4. Je kunt de volledige les hieronder gratis lezen en daarna in de browser praktisch oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject Enterprise-backend-API's met NestJS. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus Enterprise-backend-API's met NestJS bevat in totaal 4 lessen.

Het probleem met meerdere instanties

Een enkele NestJS WebSocket-gateway bewaart elke verbonden socket in het geheugen van één Node-proces. Zodra je horizontaal schaalt achter een load balancer, gaat die aanname niet meer op.

  • Client A maakt verbinding met instantie 1.
  • Client B maakt verbinding met instantie 2.
  • Wanneer instantie 1 naar een ruimte uitzendt, krijgt instantie 2 daar niets van mee — dus mist client B het bericht.

Om dit op te lossen hebben we een gedeelde berichtenbus nodig waarmee elke instantie gebeurtenissen naar alle andere instanties kan uitzenden. Redis Pub/Sub is de gebruikelijke keuze en Socket.IO levert hiervoor precies een adapter.

Hoe de Redis-adapter werkt

De @socket.io/redis-adapter vervangt de adapter in het geheugen van Socket.IO. Telkens wanneer je code server.to(room).emit(...) aanroept, publiceert de adapter die gebeurtenis naar een Redis-kanaal in plaats van deze alleen lokaal af te leveren.

  • Elke instantie abonneert zich op dezelfde Redis-kanalen.
  • Een uitzending op instantie 1 wordt naar Redis gepubliceerd. Daarna ontvangt elke geabonneerde instantie (waaronder instantie 2) de uitzending en levert deze af bij de eigen lokale sockets.
  • Redis is alleen een doorgeefluik — socketverbindingen blijven op elk knooppunt bestaan; er wordt geen socketstatus opgeslagen in Redis.

Dit betekent dat lidmaatschap van ruimtes, uitzendingen en zelfs server.emit() correct door het hele cluster worden verspreid.

De onderdelen installeren

Je hebt drie pakketten nodig: een Redis-client (ioredis of de officiële redis-client), de Redis-adapter van Socket.IO en Socket.IO zelf (dat al wordt meegeleverd door @nestjs/platform-socket.io).

De adapter heeft twee Redis-verbindingen nodig: één pubClient voor publiceren en één subClient voor abonneren. Een verbinding in abonnementsmodus kan geen normale opdrachten uitvoeren. Daarom moeten ze gescheiden zijn.

// package.json dependencies (excerpt)
{
  "dependencies": {
    "@nestjs/platform-socket.io": "^11.0.0",
    "@nestjs/websockets": "^11.0.0",
    "@socket.io/redis-adapter": "^8.3.0",
    "socket.io": "^4.7.0",
    "ioredis": "^5.4.0"
  }
}

Een aangepaste IoAdapter

NestJS verpakt Socket.IO achter een IoAdapter-klasse. Om de Redis-adapter te injecteren, maak je een subklasse van IoAdapter en overschrijf je createIOServer, zodat elke nameserver de Redis-adapter krijgt via server.adapter(...).

  • connectToRedis() maakt de gedupliceerde publicatie- en abonnementsclients één keer aan tijdens het opstarten.
  • createIOServer() koppelt de adapter die is gemaakt met createAdapter(pubClient, subClient).

Dit is het centrale onderdeel van de hele les.

// redis-io.adapter.ts
import { IoAdapter } from '@nestjs/platform-socket.io';
import { ServerOptions } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { Redis } from 'ioredis';

export class RedisIoAdapter extends IoAdapter {
  private adapterConstructor: ReturnType<typeof createAdapter>;

  async connectToRedis(url: string): Promise<void> {
    const pubClient = new Redis(url);
    const subClient = pubClient.duplicate();
    this.adapterConstructor = createAdapter(pubClient, subClient);
  }

  createIOServer(port: number, options?: ServerOptions): any {
    const server = super.createIOServer(port, options);
    server.adapter(this.adapterConstructor);
    return server;
  }
}

Dit koppelen tijdens het opstarten

De adapter moet voordat de app begint te luisteren met Redis zijn verbonden en worden geregistreerd met app.useWebSocketAdapter(). Doe dit in main.ts.

  • Maak RedisIoAdapter aan met de app-instantie.
  • Gebruik await voor de Redis-verbinding, zodat een fout het opstarten netjes afbreekt.
  • Registreer de adapter en roep daarna app.listen() aan.
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { RedisIoAdapter } from './redis-io.adapter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const redisIoAdapter = new RedisIoAdapter(app);
  await redisIoAdapter.connectToRedis(
    process.env.REDIS_URL ?? 'redis://localhost:6379',
  );
  app.useWebSocketAdapter(redisIoAdapter);

  await app.listen(3000);
}
bootstrap();

De gateway blijft ongewijzigd

Het mooie van deze aanpak: je @WebSocketGateway-code verandert helemaal niet. Je blijft server.to(room).emit() gebruiken en de adapter verspreidt de uitzending transparant over alle instanties.

  • Voeg zoals gewoonlijk een client toe aan een ruimte met client.join(room).
  • Zend uit met this.server.to(room).emit(event, payload).

Dezelfde gateway werkt nu zowel met 1 als met 50 replica's.

// chat.gateway.ts
import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  MessageBody,
  ConnectedSocket,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';

@WebSocketGateway({ cors: { origin: '*' } })
export class ChatGateway {
  @WebSocketServer() server: Server;

  @SubscribeMessage('joinRoom')
  onJoin(@ConnectedSocket() client: Socket, @MessageBody() room: string) {
    client.join(room);
    return { joined: room };
  }

  @SubscribeMessage('sendMessage')
  onMessage(@MessageBody() data: { room: string; text: string }) {
    // Fans out to every instance thanks to the Redis adapter
    this.server.to(data.room).emit('message', data.text);
  }
}

Sticky sessions versus polling

De Redis-adapter lost het uitzenden op, maar niet de HTTP-handshake met lang pollen. Bij meerdere instanties verstuurt het polling-transport van Socket.IO verschillende HTTP-aanvragen tijdens de upgrade. Als die aanvragen bij verschillende instanties terechtkomen, mislukt de handshake.

  • Optie A: Schakel sticky sessions in op de load balancer, zodat een client altijd dezelfde instantie bereikt.
  • Optie B: Dwing alleen het WebSocket-transport af en sla polling volledig over.

De meeste productieopstellingen schakelen sticky sessions in op de ingress- of load-balancerlaag.

// Force WebSocket-only to sidestep multi-request polling handshakes
@WebSocketGateway({
  transports: ['websocket'],
  cors: { origin: '*' },
})
export class ChatGateway {}

De berichtenbus netjes opnieuw verbinden

Als Redis uitvalt, kan de adapter geen gebeurtenissen tussen instanties doorgeven. Met ioredis krijg je standaard automatisch opnieuw verbinden, maar je moet fouten loggen en monitoren, zodat je weet wanneer uitzendingen minder betrouwbaar zijn.

  • Voeg listeners toe aan zowel de publicatie- als de abonnementsclient.
  • Met een retryStrategy kun je de back-off begrenzen en voorkomen dat Redis wordt overspoeld met nieuwe pogingen.
// redis-io.adapter.ts (hardened connect)
async connectToRedis(url: string): Promise<void> {
  const pubClient = new Redis(url, {
    retryStrategy: (times) => Math.min(times * 100, 3000),
  });
  const subClient = pubClient.duplicate();

  for (const client of [pubClient, subClient]) {
    client.on('error', (err) => console.error('Redis adapter error', err));
    client.on('reconnecting', () => console.warn('Redis adapter reconnecting'));
  }

  this.adapterConstructor = createAdapter(pubClient, subClient);
}

Redeneren over de kosten van verspreiding

Elke uitzending tussen instanties wordt Redis-verkeer. Als je begrijpt hoe de verspreiding werkt, kun je Redis beter dimensioneren. Een eenvoudige berekening zonder afhankelijkheden maakt het schaalgedrag concreet: als elke uitzending N instanties moet bereiken, geeft Redis ongeveer één publicatie door die door N−1 andere instanties wordt verwerkt.

Het onderstaande fragment is een eenvoudig TypeScript-programma dat het aantal gepubliceerde berichten per seconde in een cluster schat — er is geen framework of Redis nodig om het uit te voeren.

// Estimate Redis pub/sub relay load for a cluster
function relayLoad(
  instances: number,
  broadcastsPerSecPerInstance: number,
): { publishes: number; deliveries: number } {
  const publishes = instances * broadcastsPerSecPerInstance;
  // each publish is consumed by the other (instances - 1) nodes
  const deliveries = publishes * (instances - 1);
  return { publishes, deliveries };
}

const scenarios = [
  { instances: 2, rate: 100 },
  { instances: 10, rate: 100 },
  { instances: 50, rate: 100 },
];

for (const s of scenarios) {
  const { publishes, deliveries } = relayLoad(s.instances, s.rate);
  console.log(
    `${s.instances} instances -> ${publishes} publishes/s, ${deliveries} deliveries/s`,
  );
}

Specifieke sockets op verschillende knooppunten benaderen

Naast ruimtes ondersteunt de Redis-adapter ook clusterbrede bewerkingen die Socket.IO beschikbaar maakt op het serverobject:

  • server.fetchSockets() geeft socketinformatie van alle instanties terug.
  • server.in(socketId).disconnectSockets() kan een socket verbreken die op een ander knooppunt actief is.
  • server.socketsJoin(room) / socketsLeave(room) verplaatsen sockets in het hele cluster.

Deze bewerkingen zijn asynchroon, omdat ze via Redis heen en weer gaan om externe instanties te bereiken.

// admin.gateway.ts
@SubscribeMessage('kick')
async onKick(@MessageBody() socketId: string) {
  // Works even if that socket is connected to another instance
  const remoteSockets = await this.server.in(socketId).fetchSockets();
  for (const s of remoteSockets) {
    s.emit('kicked', { reason: 'admin action' });
    s.disconnect(true);
  }
}

Redis Pub/Sub versus de Streams-adapter

Socket.IO biedt twee op Redis gebaseerde adapters met verschillende leveringsgaranties:

  • Pub/Sub-adapter (@socket.io/redis-adapter): verzenden en vergeten. Als een instantie kort de verbinding met Redis verliest, mist deze gedurende die periode berichten. De laagste latentie en de eenvoudigste optie.
  • Streams-adapter (@socket.io/redis-streams-adapter): gebruikt Redis Streams, zodat een instantie die opnieuw verbinding maakt gemiste berichten kan afspelen. Iets meer overhead, maar beter geschikt wanneer levering minstens één keer vereist is.

Voor de meeste chat- en meldingenuitzendingen is de Pub/Sub-adapter de standaardaanbeveling. Kies Streams wanneer gemiste uitzendingen tijdens een korte storing onaanvaardbaar zijn.

Korte controle: waarom twee clients?

De Redis-adapter vereist een afzonderlijke pubClient en subClient. Waarom kun je niet één Redis-verbinding voor beide gebruiken?

Samenvatting

Je hebt geleerd hoe je een NestJS-realtimeapp horizontaal schaalt met een Redis Pub/Sub-adapter:

  • De Socket.IO-status in het geheugen wordt niet gedeeld tussen instanties — Redis Pub/Sub stuurt broadcasts door het cluster.
  • Maak een subklasse van IoAdapter, verbind een pubClient en subClient, en koppel createAdapter(...) in createIOServer.
  • Registreer de adapter in main.ts met app.useWebSocketAdapter() vóór listen(); je gatewaycode blijft ongewijzigd.
  • Schakel sticky sessions in (of forceer het WebSocket-transport), zodat de polling-handshake de loadbalancing overleeft.
  • Clusterbrede bewerkingen zoals fetchSockets() en disconnectSockets() maken een heen-en-terugverbinding via Redis.
  • Kies in plaats daarvan de Streams-adapter wanneer je broadcasts wilt kunnen afspelen die tijdens een Redis-storing zijn gemist.
Gratis beginnen

Leer TypeScript met een AI-tutor — gratis

Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.

Cursussen
20
Lessen
76

Veelgestelde vragen

Is de les “Realtime schalen met een Redis Pub/Sub-adapter” gratis?

Ja — de volledige tekst van “Realtime schalen met een Redis Pub/Sub-adapter” kun je hier gratis op het web lezen. Als je interactief wilt oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is, en de rest van de cursus Enterprise-backend-API's met NestJS wilt ontgrendelen, kun je upgraden naar CoddyKit PRO. De cursus Enterprise-backend-API's met NestJS bevat in totaal 4 lessen.

Wat leer ik in “Realtime schalen met een Redis Pub/Sub-adapter”?

Synchroniseer socketstatus tussen meerdere instances met de Redis IoAdapter voor horizontale schaalbaarheid. Je oefent met Enterprise-backend-API's met NestJS door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.

Heb ik ervaring nodig om met Enterprise-backend-API's met NestJS te beginnen?

Ervaring vooraf is niet nodig. Enterprise-backend-API's met NestJS op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 4 van 4.

Hoe lang duurt de les “Realtime schalen met een Redis Pub/Sub-adapter”?

De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.

Kan ik code schrijven en uitvoeren in deze les over Enterprise-backend-API's met NestJS?

Ja. Elke les over Enterprise-backend-API's met NestJS bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.

Alle lessen in deze cursus

  1. WebSocket-gateways met de Socket.IO-adapter
  2. Socketverbindingen authenticeren en beveiligen
  3. Server-Sent Events voor eenrichtingspush
  4. Realtime schalen met een Redis Pub/Sub-adapter
← Terug naar Enterprise-backend-API's met NestJS