Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Load Balancing - rozłożenie obciążenia legionu

dowódco legionów! Konsul Caesar.js zauważył, że jeden fort wykonuje całą pracę, podczas gdy inne stoją bezczynnie w garnizonie. Czas nauczyć się load balancingu - sztuki rozkładania obciążenia między wszystkie oddziały armii!

Czym jest load balancing w świecie legionariuszów?

Wyobraź sobie armię rzymskich legionów:

  • Jeden fort obsługuje wszystkich klientów = przeciążenie
  • Oddziały stoją bezczynnie = marnotrawstwo zasobów
  • Nierównomierne obciążenie = niektóre forty padają pod ciężarem zadań
  • Brak redundancji = jeśli główny fort upadnie, cała armia staje

Load balancer to jak mądry legat, który rozdziela zadania między forty!

Application Load Balancing

1// load-balancer.service.ts
2import { Injectable } from '@nestjs/common';
3import { ConfigService } from '@nestjs/config';
4
5interface ServerInstance {
6 id: string;
7 host: string;
8 port: number;
9 weight: number;
10 currentLoad: number;
11 isHealthy: boolean;
12 lastHealthCheck: Date;
13}
14
15@Injectable()
16export class LoadBalancerService {
17 private servers: ServerInstance[] = [];
18 private currentIndex = 0;
19
20 constructor(private configService: ConfigService) {
21 this.initializeServers();
22 this.startHealthChecks();
23 }
24
25 private initializeServers(): void {
26 const serverConfigs = this.configService.get('LEGION_SERVERS') || [
27 { host: 'cohort-1.legion.local', port: 3000, weight: 10 },
28 { host: 'cohort-2.legion.local', port: 3000, weight: 15 },
29 { host: 'cohort-3.legion.local', port: 3000, weight: 8 },
30 ];
31
32 this.servers = serverConfigs.map((config, index) => ({
33 id: `cohort-${index + 1}`,
34 ...config,
35 currentLoad: 0,
36 isHealthy: true,
37 lastHealthCheck: new Date(),
38 }));
39 }
40
41 // Round Robin - kolejno każdy fort
42 getNextServerRoundRobin(): ServerInstance | null {
43 const healthyServers = this.servers.filter(s => s.isHealthy);
44 if (healthyServers.length === 0) return null;
45
46 const server = healthyServers[this.currentIndex % healthyServers.length];
47 this.currentIndex++;
48 
49 console.log(`Wybrano fort: ${server.id} (Round Robin)`);
50 return server;
51 }
52
53 // Weighted Round Robin - uwzględnia siłę kohort
54 getNextServerWeighted(): ServerInstance | null {
55 const healthyServers = this.servers.filter(s => s.isHealthy);
56 if (healthyServers.length === 0) return null;
57
58 const totalWeight = healthyServers.reduce((sum, s) => sum + s.weight, 0);
59 let randomWeight = Math.random() * totalWeight;
60
61 for (const server of healthyServers) {
62 randomWeight -= server.weight;
63 if (randomWeight <= 0) {
64 console.log(`⚖ Wybrano fort: ${server.id} (Weighted)`);
65 return server;
66 }
67 }
68
69 return healthyServers[0];
70 }
71
72 // Least Connections - najmniej obciążony fort
73 getNextServerLeastConnections(): ServerInstance | null {
74 const healthyServers = this.servers.filter(s => s.isHealthy);
75 if (healthyServers.length === 0) return null;
76
77 const leastLoaded = healthyServers.reduce((min, server) => 
78 server.currentLoad < min.currentLoad ? server : min
79 );
80
81 console.log(` Wybrano fort: ${leastLoaded.id} (Least Connections: ${leastLoaded.currentLoad})`);
82 return leastLoaded;
83 }
84
85 // Resource-based - na podstawie zasobów systemowych
86 async getNextServerResourceBased(): Promise<ServerInstance | null> {
87 const healthyServers = this.servers.filter(s => s.isHealthy);
88 if (healthyServers.length === 0) return null;
89
90 // Pobierz metryki zasobów dla każdej kohorty
91 const serversWithMetrics = await Promise.all(
92 healthyServers.map(async (server) => {
93 const metrics = await this.getServerMetrics(server);
94 return {
95 ...server,
96 cpuUsage: metrics.cpu,
97 memoryUsage: metrics.memory,
98 responseTime: metrics.avgResponseTime,
99 score: this.calculateServerScore(metrics),
100 };
101 })
102 );
103
104 // Wybierz fort z najniższym score (najmniej obciążony)
105 const bestServer = serversWithMetrics.reduce((best, server) => 
106 server.score < best.score ? server : best
107 );
108
109 console.log(` Wybrano fort: ${bestServer.id} (Resource-based, score: ${bestServer.score})`);
110 return bestServer;
111 }
112
113 private calculateServerScore(metrics: any): number {
114 // Prostą formuła: wyższa wartość = gorszy serwer
115 return (
116 metrics.cpu * 0.4 + // 40% wagi dla CPU
117 metrics.memory * 0.3 + // 30% wagi dla pamięci
118 metrics.avgResponseTime * 0.2 + // 20% wagi dla czasu odpowiedzi
119 metrics.activeConnections * 0.1 // 10% wagi dla połączeń
120 );
121 }
122
123 async incrementLoad(serverId: string): Promise<void> {
124 const server = this.servers.find(s => s.id === serverId);
125 if (server) {
126 server.currentLoad++;
127 }
128 }
129
130 async decrementLoad(serverId: string): Promise<void> {
131 const server = this.servers.find(s => s.id === serverId);
132 if (server && server.currentLoad > 0) {
133 server.currentLoad--;
134 }
135 }
136
137 private async getServerMetrics(server: ServerInstance): Promise<any> {
138 try {
139 // W rzeczywistej implementacji: HTTP call do /metrics endpoint
140 const response = await fetch(`http://${server.host}:${server.port}/health/metrics`);
141 return await response.json();
142 } catch (error) {
143 // Fallback metrics
144 return {
145 cpu: Math.random() * 100,
146 memory: Math.random() * 100,
147 avgResponseTime: Math.random() * 1000,
148 activeConnections: Math.floor(Math.random() * 100),
149 };
150 }
151 }
152
153 @Cron('*/30 * * * * *') // Co 30 sekund
154 private async startHealthChecks(): Promise<void> {
155 for (const server of this.servers) {
156 try {
157 const response = await fetch(`http://${server.host}:${server.port}/health`, {
158 timeout: 5000,
159 });
160 
161 server.isHealthy = response.ok;
162 server.lastHealthCheck = new Date();
163 
164 if (!server.isHealthy) {
165 console.warn(` Kohorta ${server.id} nie odpowiada!`);
166 }
167 } catch (error) {
168 server.isHealthy = false;
169 server.lastHealthCheck = new Date();
170 console.error(` Kohorta ${server.id} nieosiągalna:`, error.message);
171 }
172 }
173 }
174
175 getLegionStatus() {
176 return {
177 totalServers: this.servers.length,
178 healthyServers: this.servers.filter(s => s.isHealthy).length,
179 totalLoad: this.servers.reduce((sum, s) => sum + s.currentLoad, 0),
180 servers: this.servers.map(s => ({
181 id: s.id,
182 host: s.host,
183 isHealthy: s.isHealthy,
184 currentLoad: s.currentLoad,
185 weight: s.weight,
186 lastHealthCheck: s.lastHealthCheck,
187 })),
188 };
189 }
190}

Proxy Load Balancer Implementation

1// proxy-load-balancer.controller.ts
2import { All, Controller, Req, Res } from '@nestjs/common';
3import { Request, Response } from 'express';
4import { createProxyMiddleware } from 'http-proxy-middleware';
5
6@Controller('/api/proxy')
7export class ProxyLoadBalancerController {
8 constructor(private loadBalancer: LoadBalancerService) {}
9
10 @All('*')
11 async proxyRequest(@Req() req: Request, @Res() res: Response): Promise<void> {
12 // Wybierz serwer za pomocą load balancera
13 const targetServer = await this.loadBalancer.getNextServerResourceBased();
14 
15 if (!targetServer) {
16 res.status(503).json({
17 error: 'Wszystkie kohorty legionu są niedostępne!',
18 code: 'LEGION_UNAVAILABLE'
19 });
20 return;
21 }
22
23 // Zwiększ licznik obciążenia
24 await this.loadBalancer.incrementLoad(targetServer.id);
25
26 // Utwórz proxy do wybranego serwera
27 const proxy = createProxyMiddleware({
28 target: `http://${targetServer.host}:${targetServer.port}`,
29 changeOrigin: true,
30 pathRewrite: {
31 '^/api/proxy': '', // Usuń prefix
32 },
33 onProxyReq: (proxyReq, req, res) => {
34 // Dodaj informacje o load balancerze
35 proxyReq.setHeader('X-Proxy-Server', targetServer.id);
36 proxyReq.setHeader('X-Load-Balancer', 'legionariusze-legion-lb');
37 console.log(`➡ Przekierowanie ${req.method} ${req.url} do ${targetServer.id}`);
38 },
39 onProxyRes: async (proxyRes, req, res) => {
40 // Zmniejsz licznik obciążenia po zakończeniu
41 await this.loadBalancer.decrementLoad(targetServer.id);
42 console.log(`⬅ Odpowiedź z ${targetServer.id}: ${proxyRes.statusCode}`);
43 },
44 onError: async (err, req, res) => {
45 await this.loadBalancer.decrementLoad(targetServer.id);
46 console.error(`❌ Błąd proxy do ${targetServer.id}:`, err.message);
47 
48 if (!res.headersSent) {
49 res.status(502).json({
50 error: 'Błąd komunikacji ze fortyem',
51 server: targetServer.id,
52 details: err.message
53 });
54 }
55 },
56 });
57
58 proxy(req, res);
59 }
60}

Circuit Breaker Pattern

1// circuit-breaker.service.ts
2@Injectable()
3export class CircuitBreakerService {
4 private breakers = new Map<string, CircuitBreaker>();
5
6 getOrCreateBreaker(serverId: string): CircuitBreaker {
7 if (!this.breakers.has(serverId)) {
8 this.breakers.set(serverId, new CircuitBreaker({
9 serverId,
10 failureThreshold: 5, // 5 błędów
11 recoveryTimeout: 30000, // 30 sekund
12 monitoringPeriod: 10000, // 10 sekund
13 }));
14 }
15 return this.breakers.get(serverId)!;
16 }
17
18 async executeWithBreaker<T>(
19 serverId: string, 
20 operation: () => Promise<T>
21 ): Promise<T> {
22 const breaker = this.getOrCreateBreaker(serverId);
23 return await breaker.execute(operation);
24 }
25
26 getBreakerStatus(serverId: string) {
27 const breaker = this.breakers.get(serverId);
28 return breaker ? breaker.getStatus() : null;
29 }
30
31 getAllBreakersStatus() {
32 const status = {};
33 this.breakers.forEach((breaker, serverId) => {
34 status[serverId] = breaker.getStatus();
35 });
36 return status;
37 }
38}
39
40class CircuitBreaker {
41 private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
42 private failures = 0;
43 private lastFailureTime?: Date;
44 private successCount = 0;
45
46 constructor(private config: {
47 serverId: string;
48 failureThreshold: number;
49 recoveryTimeout: number;
50 monitoringPeriod: number;
51 }) {}
52
53 async execute<T>(operation: () => Promise<T>): Promise<T> {
54 if (this.state === 'OPEN') {
55 if (this.shouldAttemptReset()) {
56 this.state = 'HALF_OPEN';
57 console.log(`Circuit breaker dla ${this.config.serverId}: próba resetu`);
58 } else {
59 throw new Error(`Circuit breaker OPEN dla serwera ${this.config.serverId}`);
60 }
61 }
62
63 try {
64 const result = await operation();
65 this.onSuccess();
66 return result;
67 } catch (error) {
68 this.onFailure();
69 throw error;
70 }
71 }
72
73 private onSuccess(): void {
74 this.failures = 0;
75 this.successCount++;
76 
77 if (this.state === 'HALF_OPEN') {
78 this.state = 'CLOSED';
79 console.log(`✅ Circuit breaker dla ${this.config.serverId}: przywrócony`);
80 }
81 }
82
83 private onFailure(): void {
84 this.failures++;
85 this.lastFailureTime = new Date();
86 
87 if (this.failures >= this.config.failureThreshold) {
88 this.state = 'OPEN';
89 console.log(`Circuit breaker dla ${this.config.serverId}: OTWORZONY`);
90 }
91 }
92
93 private shouldAttemptReset(): boolean {
94 if (!this.lastFailureTime) return false;
95 
96 const timeSinceLastFailure = Date.now() - this.lastFailureTime.getTime();
97 return timeSinceLastFailure >= this.config.recoveryTimeout;
98 }
99
100 getStatus() {
101 return {
102 serverId: this.config.serverId,
103 state: this.state,
104 failures: this.failures,
105 successCount: this.successCount,
106 lastFailureTime: this.lastFailureTime,
107 };
108 }
109}
Vai a CodeWorlds