Kurs NestJS · Moduł 9: Deployment i infrastruktura
Scaling i Microservices - budowanie sieci legionów
W tej lekcji3
Dowódco legionów! Jeden fort z jednym procesem Node.js obsługuje całą prowincję, a gdy ruch rośnie, procesor dobija do stu procent i fort pada. Konsul Caesar.js nauczy Cię, jak z jednego rzymskiego fortu zbudować sieć prowincji. Scaling i microservices to techniki budowania aplikacji, które rosną razem z ruchem i złożonością biznesową.
Horizontal vs Vertical Scaling
Skalowanie pionowe to większa maszyna: więcej rdzeni i pamięci dla tego samego serwera. Skalowanie poziome to więcej instancji za load balancerem. Node.js wykonuje JavaScript w jednym wątku, więc żeby wykorzystać wszystkie rdzenie jednej maszyny, moduł cluster uruchamia kilka procesów, które dzielą port:
1// cluster.mjs - kilka procesów Node.js na jednej maszynie
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 zastąpiło przestarzałe od Node.js 16 isMaster, a availableParallelism() to zalecany dziś sposób liczenia rdzeni. Proces główny rozdziela połączenia między workery po kolei: w teście sześć żądań trafiło do sześciu różnych procesów. Workery nie dzielą pamięci, więc sesje i cache muszą mieszkać poza procesem, np. w Redisie - dokumentacja Node.js wprost ostrzega przed trzymaniem ich w obiektach w pamięci.
W kontenerach polecam jeden proces na kontener i skalowanie liczby replik, a nie cluster w środku - orkiestrator lepiej pilnuje restartów niż pętla fork().
Microservices Architecture
Mikroserwisy dzielą aplikację na małe, niezależnie wdrażane usługi, jak legiony z własnymi dowódcami. Mają swoją cenę: każda granica to połączenie sieciowe, które może zawieść, i osobne wdrożenie do pilnowania, więc małemu zespołowi często lepiej służy dobrze podzielony monolit. W NestJS klienta innego mikroserwisu rejestruje ClientsModule:
1// app.module.ts - klienci mikroserwisów
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 {}Każdy klient ma nazwę, transport (tu TCP) i adres. Nazwa służy potem jako token wstrzykiwania:
1// User microservice - brama HTTP, która pyta inne mikroserwisy
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() wysyła wiadomość i czeka na odpowiedź, zwracając Observable, który firstValueFrom() z RxJS zamienia w Promise. Pierwsza wersja używała .toPromise(), przestarzałego w RxJS 7, i nie wstrzykiwała userService. Kontroler nie jest w istocie mikroserwisem użytkowników, tylko bramą HTTP, która pyta inne usługi.
Po drugiej stronie odpowiada kontroler z @MessagePattern:
1// tribute.controller.ts - po stronie mikroserwisu tributów
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}Wzorzec get_user_tributes musi się zgadzać po obu stronach. Taki serwis startuje przez NestFactory.createMicroservice() z tym samym transportem. send() to model zapytanie-odpowiedź; do powiadomień bez odpowiedzi służą emit() i @EventPattern.
Wdrażanie bez przestojów
Wiele instancji pozwala aktualizować aplikację bez przerwy. Blue-green deployment utrzymuje dwa identyczne środowiska i przełącza ruch ze starego na nowe - daje zero przestojów i natychmiastowy rollback przez przełączenie z powrotem, kosztem podwójnej infrastruktury. Wdrożenie bez przestojów przebiega tak:
- przygotuj nową wersję w równoległym środowisku,
- uruchom health checki i smoke testy,
- przełącz ruch na nową wersję,
- wyłącz starą wersję po weryfikacji.
Canary deployment stopniowo kieruje ruch do nowej wersji, zaczynając od małego procentu użytkowników:
- wdróż nową wersję dla małego procentu użytkowników,
- monitoruj metryki i błędy,
- stopniowo zwiększaj ruch,
- zrób pełny rollout albo rollback.
W Kubernetesie o tym, czy instancja dostaje ruch, decydują sondy:
1# deployment.yaml - fragment: strategia wdrożenia i sondy zdrowia
2spec:
3 replicas: 4
4 strategy:
5 type: RollingUpdate
6 rollingUpdate:
7 maxSurge: 1 # najwyżej 1 dodatkowy pod w trakcie wdrożenia
8 maxUnavailable: 0 # stary pod znika dopiero, gdy nowy jest gotowy
9 template:
10 spec:
11 containers:
12 - name: api
13 image: registry.imperium.rome/api:2.4.0
14 readinessProbe: # czy przyjmować ruch?
15 httpGet:
16 path: /health/ready
17 port: 3000
18 initialDelaySeconds: 5
19 periodSeconds: 10
20 timeoutSeconds: 2
21 successThreshold: 2 # dwa udane sprawdzenia z rzędu
22 failureThreshold: 3 # trzy nieudane = pod wypada z ruchu
23 livenessProbe: # czy proces żyje?
24 httpGet:
25 path: /health/live
26 port: 3000
27 periodSeconds: 10
28 failureThreshold: 3 # trzy nieudane = restart konteneraSonda readiness usuwa pod z ruchu, gdy nie jest gotowy, a liveness restartuje kontener, który przestał odpowiadać. successThreshold większy niż 1 wolno ustawić tylko dla readiness; domyślnie periodSeconds to 10, a failureThreshold - 3. maxUnavailable: 0 sprawia, że rolling update nigdy nie zmniejsza liczby gotowych podów.
W ostatniej lekcji modułu zbudujemy blue-green, rolling update i feature flagi w kodzie.
Pamiętaj: siła Imperium nie leżała w jednym wielkim forcie, tylko w sieci legionów, które mogły się wzajemnie zastąpić -, buduj aplikację tak samo.
Kod do tej lekcji: src/scaling.ts
1// Scaling i Microservices - Budowanie Legionu Kohort
2
3// 1. Vertical vs Horizontal Scaling
4// Vertical: wiekszy serwer (wiecej CPU/RAM)
5// Horizontal: wiecej instancji (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. Komunikacja miedzy serwisami
28// TCP Transport (domyslny):
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 dla 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: jeden punkt wejscia
71// - Service Discovery: automatyczne wykrywanie serwisow
72// - Circuit Breaker: ochrona przed kaskadowymi awariami
73// - Saga Pattern: transakcje rozproszone
74// - CQRS: oddzielenie odczytu od zapisu
75Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Blue-green deployment strategy oferuje:
2. Canary deployment polega na:
Zadania praktyczne w grze
- Edytor kodu
Napisz konfigurację deployment z readiness probe i liveness probe
- Układanie w pionie
Ułóż kroki procesu canary deployment:
- Układanie w pionie
Uporządkuj kroki zero-downtime deployment: