Cours NestJS · Module 9: Déploiement et infrastructure

Mise à l'échelle et microservices - bâtir un réseau de légions

6 min de lecture
Dans cette leçon3

Commandant des légions ! Un seul fort avec un seul processus Node.js sert toute la province, et quand le trafic augmente, le processeur atteint cent pour cent et le fort tombe. Le consul Caesar.js va t'apprendre à bâtir, à partir d'un seul fort romain, un réseau de provinces. La mise à l'échelle et les microservices sont des techniques pour construire des applications qui grandissent avec le trafic et la complexité métier.

Horizontal vs Vertical Scaling

La mise à l'échelle verticale, c'est une machine plus grosse : plus de cœurs et de mémoire pour le même serveur. La mise à l'échelle horizontale, c'est davantage d'instances derrière un load balancer. Node.js exécute JavaScript dans un seul thread, donc pour utiliser tous les cœurs d'une machine, le module cluster lance plusieurs processus qui partagent un port :

1// cluster.mjs - plusieurs processus Node.js sur une seule machine
2import cluster from 'node:cluster';
3import { availableParallelism } from 'node:os';
4
5const numCPUs = availableParallelism();
6
7if (cluster.isPrimary) {
8  console.log(`Primary ${process.pid} is running`);
9
10  // Fork workers
11  for (let i = 0; i < numCPUs; i++) {
12    cluster.fork();
13  }
14
15  cluster.on('exit', (worker, code, signal) => {
16    console.log(`Worker ${worker.process.pid} died`);
17    cluster.fork(); // Restart worker
18  });
19} else {
20  // Workers can share any TCP port
21  import('./dist/main.js').then(() => {
22    console.log(`Worker ${process.pid} started`);
23  });
24}

cluster.isPrimary a remplacé isMaster, obsolète depuis Node.js 16, et availableParallelism() est aujourd'hui la façon recommandée de compter les cœurs. Le processus principal distribue les connexions aux workers à tour de rôle : dans un test, six requêtes ont atteint six processus différents. Les workers ne partagent pas la mémoire, donc les sessions et les caches doivent vivre hors du processus, par ex. dans Redis : la documentation de Node.js déconseille explicitement de les garder dans des objets en mémoire.

Dans des conteneurs, je te recommande un processus par conteneur et une mise à l'échelle du nombre de réplicas, plutôt que cluster à l'intérieur : l'orchestrateur surveille les redémarrages mieux qu'une boucle de fork().

Microservices Architecture

Les microservices découpent une application en petits services déployés indépendamment, comme des légions avec leurs propres commandants. Ils ont un prix : chaque frontière est une connexion réseau qui peut échouer et un déploiement de plus à surveiller, donc une petite équipe est souvent mieux servie par un monolithe bien découpé. Dans NestJS, le client d'un autre microservice s'enregistre avec ClientsModule :

1// app.module.ts - les clients des microservices
2import { Module } from '@nestjs/common';
3import { ClientsModule, Transport } from '@nestjs/microservices';
4
5@Module({
6  imports: [
7    ClientsModule.register([
8      { name: 'TRIBUTE_SERVICE', transport: Transport.TCP, options: { host: 'tributes', port: 4001 } },
9      { name: 'LEGION_SERVICE', transport: Transport.TCP, options: { host: 'legions', port: 4002 } },
10    ]),
11  ],
12  controllers: [UserController],
13  providers: [UserService],
14})
15export class AppModule {}

Chaque client a un nom, un transport (ici TCP) et une adresse. Le nom sert ensuite de jeton d'injection :

1// User microservice - une passerelle HTTP qui interroge les autres microservices
2import { Controller, Get, Inject, Param } from '@nestjs/common';
3import { ClientProxy } from '@nestjs/microservices';
4import { firstValueFrom } from 'rxjs';
5
6@Controller('users')
7export class UserController {
8  constructor(
9    private userService: UserService,
10    @Inject('TRIBUTE_SERVICE') private tributeService: ClientProxy,
11    @Inject('LEGION_SERVICE') private legionService: ClientProxy,
12  ) {}
13
14  @Get(':id/tributes')
15  async getUserTributes(@Param('id') userId: string) {
16    const user = await this.userService.findById(userId);
17    const tributes = await firstValueFrom(
18      this.tributeService.send('get_user_tributes', { userId }),
19    );
20
21    return { user, tributes };
22  }
23}
24
25// Message patterns
26export const USER_PATTERNS = {
27  GET_USER: 'get_user',
28  CREATE_USER: 'create_user',
29  UPDATE_USER: 'update_user',
30};

ClientProxy.send() envoie un message et attend la réponse, en renvoyant un Observable que firstValueFrom() de RxJS transforme en Promise. La première version utilisait .toPromise(), obsolète dans RxJS 7, et n'injectait pas userService. Le contrôleur n'est pas vraiment un microservice des utilisateurs, mais une passerelle HTTP qui interroge d'autres services.

De l'autre côté, c'est un contrôleur avec @MessagePattern qui répond :

1// tribute.controller.ts - côté microservice des tributs
2import { Controller } from '@nestjs/common';
3import { MessagePattern, Payload } from '@nestjs/microservices';
4
5@Controller()
6export class TributeController {
7  @MessagePattern('get_user_tributes')
8  getUserTributes(@Payload() data: { userId: string }) {
9    return [{ userId: data.userId, amount: 1000, province: 'Gallia' }];
10  }
11}

Le motif get_user_tributes doit correspondre des deux côtés. Un tel service démarre avec NestFactory.createMicroservice() et le même transport. send() est le modèle requête-réponse ; les notifications sans réponse utilisent emit() et @EventPattern.

Déployer sans interruption

Plusieurs instances permettent de mettre à jour l'application sans coupure. Le déploiement blue-green maintient deux environnements identiques et bascule le trafic de l'ancien vers le nouveau : il offre zéro interruption et un rollback instantané en basculant de nouveau, au prix d'une infrastructure double. Un déploiement sans interruption se déroule ainsi :

  1. prépare la nouvelle version dans un environnement parallèle,
  2. lance les health checks et les smoke tests,
  3. bascule le trafic vers la nouvelle version,
  4. arrête l'ancienne version après vérification.

Le déploiement canary redirige progressivement le trafic vers la nouvelle version, en commençant par un petit pourcentage d'utilisateurs :

  1. déploie la nouvelle version pour un petit pourcentage d'utilisateurs,
  2. surveille les métriques et les erreurs,
  3. augmente progressivement le trafic,
  4. fais un déploiement complet ou un rollback.

Dans Kubernetes, ce sont les sondes qui décident si une instance reçoit du trafic :

1# deployment.yaml - extrait : stratégie de déploiement et sondes de santé
2spec:
3  replicas: 4
4  strategy:
5    type: RollingUpdate
6    rollingUpdate:
7      maxSurge: 1        # au plus 1 pod supplémentaire pendant le déploiement
8      maxUnavailable: 0  # un ancien pod ne disparaît qu'une fois le nouveau prêt
9  template:
10    spec:
11      containers:
12        - name: api
13          image: registry.imperium.rome/api:2.4.0
14          readinessProbe:        # doit-il recevoir du trafic ?
15            httpGet:
16              path: /health/ready
17              port: 3000
18            initialDelaySeconds: 5
19            periodSeconds: 10
20            timeoutSeconds: 2
21            successThreshold: 2  # deux vérifications réussies de suite
22            failureThreshold: 3  # trois échecs = le pod sort du trafic
23          livenessProbe:         # le processus est-il vivant ?
24            httpGet:
25              path: /health/live
26              port: 3000
27            periodSeconds: 10
28            failureThreshold: 3  # trois échecs = redémarrage du conteneur

La sonde readiness retire un pod du trafic quand il n'est pas prêt, et la sonde liveness redémarre un conteneur qui ne répond plus. Un successThreshold supérieur à 1 n'est autorisé que pour la readiness ; par défaut, periodSeconds vaut 10 et failureThreshold vaut 3. maxUnavailable: 0 garantit qu'un rolling update ne réduit jamais le nombre de pods prêts.

Dans la dernière leçon du module, nous construirons en code le blue-green, le rolling update et les feature flags.

Retiens : la force de l'Empire ne résidait pas dans un seul grand fort, mais dans un réseau de légions capables de se remplacer mutuellement -, construis ton application de la même façon.

Code de cette leçon : src/scaling.ts
1// Mise à l'échelle et microservices - bâtir une flotte de navires
2
3// 1. Vertical vs Horizontal Scaling
4// Vertical: bigger server (more CPU/RAM)
5// Horizontal: plus d'instances (load balancing)
6
7// 2. NestJS Microservices
8import { Controller } from '@nestjs/common';
9// import { MessagePattern, EventPattern } from '@nestjs/microservices';
10
11// Microservice - TributeService
12// @Controller()
13// class TributeController {
14//   // Request-Response pattern
15//   @MessagePattern({ cmd: 'get_tribute' })
16//   getTribute(data: { provinceId: string }) {
17//     return { province: data.provinceId, amount: 1000 };
18//   }
19//
20//   // Event pattern (fire and forget)
21//   @EventPattern('tribute_collected')
22//   handleTributeCollected(data: { province: string; amount: number }) {
23//     console.log(`Tribute collected from ${data.province}`);
24//   }
25// }
26
27// 3. Communication entre les services
28// TCP Transport (default):
29// const app = await NestFactory.createMicroservice(AppModule, {
30//   transport: Transport.TCP,
31//   options: { host: '0.0.0.0', port: 3001 },
32// });
33
34// Redis Transport:
35// const app = await NestFactory.createMicroservice(AppModule, {
36//   transport: Transport.REDIS,
37//   options: { host: 'localhost', port: 6379 },
38// });
39
40// 4. Docker Compose pour les microservices
41const dockerCompose = `
42services:
43  api-gateway:
44    build: ./gateway
45    ports: ['3000:3000']
46    depends_on: [tribute-service, legion-service]
47
48  tribute-service:
49    build: ./tribute-service
50    ports: ['3001:3001']
51
52  legion-service:
53    build: ./legion-service
54    ports: ['3002:3002']
55
56  redis:
57    image: redis:7-alpine
58    ports: ['6379:6379']
59
60  mongodb:
61    image: mongo:7
62    ports: ['27017:27017']
63    volumes: [mongo-data:/data/db]
64
65volumes:
66  mongo-data:
67`;
68
69// 5. Wzorce architektury
70// - API Gateway: un point d'entrée unique
71// - Service Discovery: découverte automatique des services
72// - Circuit Breaker: protection against cascading failures
73// - Saga Pattern: transactions distribuées
74// - CQRS: séparation de la lecture et de l'écriture
75

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. La stratégie de déploiement blue-green offre :

  2. 2. Le déploiement canary consiste à :

Tâches pratiques dans le jeu

  • Éditeur de code

    Écris une configuration de déploiement avec une readiness probe et une liveness probe

  • Tri vertical

    Ordonne les étapes du processus de déploiement canary :

  • Tri vertical

    Ordonne les étapes d'un déploiement sans interruption :