kukuy 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.ssl +22 -0
- package/CHANGELOG.md +25 -1
- package/README-SSL.md +165 -0
- package/README.md +21 -0
- package/kukuu1.webp +0 -0
- package/kukuy.js +51 -5
- package/optimize-mariadb.sh +152 -0
- package/package.json +7 -2
- package/src/algorithms/IPHashAlgorithm.js +25 -13
- package/src/algorithms/RoundRobinAlgorithm.js +25 -27
- package/src/core/Balancer.js +125 -123
- package/src/core/ServerPool.js +46 -5
- package/src/utils/HealthChecker.js +11 -5
- package/src/utils/ProfessionalMetrics.js +41 -24
- package/start-ssl-config.sh +24 -0
- package/start-ssl.sh +26 -0
- package/test_optimization.js +54 -0
- package/webpage/index.html +1 -1
package/.env.ssl
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Archivo de configuración para Kukuy en modo SSL
|
|
2
|
+
|
|
3
|
+
# Puerto para el servidor HTTPS
|
|
4
|
+
BALANCER_HTTPS_PORT=8443
|
|
5
|
+
|
|
6
|
+
# Ruta al certificado SSL
|
|
7
|
+
SSL_CERT_PATH=certs/auto/certificate.crt
|
|
8
|
+
|
|
9
|
+
# Ruta a la llave privada SSL
|
|
10
|
+
SSL_KEY_PATH=certs/auto/private.key
|
|
11
|
+
|
|
12
|
+
# Opcional: Puerto para el servidor HTTP (descomentar para habilitar)
|
|
13
|
+
# BALANCER_HTTP_PORT=8080
|
|
14
|
+
|
|
15
|
+
# Opcional: Habilitar modo HTTP junto con HTTPS
|
|
16
|
+
# ENABLE_HTTP=true
|
|
17
|
+
|
|
18
|
+
# Otros ajustes de configuración
|
|
19
|
+
LOG_LEVEL=info
|
|
20
|
+
HEALTH_CHECK_INTERVAL=30000
|
|
21
|
+
CONFIG_FILE_PATH=./servers_real.json
|
|
22
|
+
ROUTES_FILE_PATH=./routes.json
|
package/CHANGELOG.md
CHANGED
|
@@ -98,4 +98,28 @@
|
|
|
98
98
|
|
|
99
99
|
### Known Issues
|
|
100
100
|
- El dashboard no refleja en tiempo real cuando servidores se vuelven offline o se levantan después de iniciar el balanceador
|
|
101
|
-
- La información de estado de los servidores puede no actualizar inmediatamente cuando un servidor backend cambia de estado
|
|
101
|
+
- La información de estado de los servidores puede no actualizar inmediatamente cuando un servidor backend cambia de estado
|
|
102
|
+
|
|
103
|
+
## [Versión 1.5.0] - 2026-01-25
|
|
104
|
+
|
|
105
|
+
### Added
|
|
106
|
+
- Soporte completo para SSL/TLS en el balanceador
|
|
107
|
+
- Scripts de inicio para operación en modo SSL (start-ssl.sh y start-ssl-config.sh)
|
|
108
|
+
- Archivo de configuración específico para SSL (.env.ssl)
|
|
109
|
+
- Documentación detallada sobre configuración SSL (README-SSL.md)
|
|
110
|
+
- Generación de certificados SSL autofirmados en directorio certs/auto
|
|
111
|
+
- Soporte para archivos de configuración personalizados (servers_real.json)
|
|
112
|
+
- Configuración flexible mediante variables de entorno para SSL
|
|
113
|
+
- Integración con ngrok para exposición segura de servicios SSL
|
|
114
|
+
- Validación de configuración SSL en tiempo de inicio
|
|
115
|
+
|
|
116
|
+
### Changed
|
|
117
|
+
- Actualización de la documentación principal para incluir instrucciones de SSL
|
|
118
|
+
- Mejora en la flexibilidad de configuración del balanceador
|
|
119
|
+
- Actualización de los scripts de inicio para usar servers_real.json por defecto
|
|
120
|
+
- Mejora en la gestión de variables de entorno para diferentes modos de operación
|
|
121
|
+
|
|
122
|
+
### Fixed
|
|
123
|
+
- Problemas de configuración SSL en el módulo HttpsBalancer
|
|
124
|
+
- Manejo de variables de entorno para certificados SSL
|
|
125
|
+
- Configuración predeterminada para archivos de servidores en modo SSL
|
package/README-SSL.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Kukuy - Balanceador de Carga con SSL
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Este documento explica cómo configurar e iniciar el balanceador Kukuy con soporte SSL/TLS.
|
|
6
|
+
|
|
7
|
+
## Información del Proyecto
|
|
8
|
+
|
|
9
|
+
- **Página Oficial**: [https://bsanchez.unaux.com/](https://bsanchez.unaux.com/)
|
|
10
|
+
- **Repositorio Oficial**: [https://gitlab.com/bytedogssyndicate1/kukuy](https://gitlab.com/bytedogssyndicate1/kukuy)
|
|
11
|
+
|
|
12
|
+
## Contenido
|
|
13
|
+
|
|
14
|
+
- [Requisitos](#requisitos)
|
|
15
|
+
- [Configuración de SSL](#configuración-de-ssl)
|
|
16
|
+
- [Inicio del Balanceador con SSL](#inicio-del-balanceador-con-ssl)
|
|
17
|
+
- [Uso de ngrok con SSL](#uso-de-ngrok-con-ssl)
|
|
18
|
+
- [Scripts Disponibles](#scripts-disponibles)
|
|
19
|
+
- [Variables de Entorno](#variables-de-entorno)
|
|
20
|
+
- [Configuración de Servidores](#configuración-de-servidores)
|
|
21
|
+
|
|
22
|
+
## Requisitos
|
|
23
|
+
|
|
24
|
+
- Node.js instalado
|
|
25
|
+
- OpenSSL (para generar certificados autofirmados)
|
|
26
|
+
- Opcional: ngrok para exponer servicios locales a Internet
|
|
27
|
+
|
|
28
|
+
## Configuración de SSL
|
|
29
|
+
|
|
30
|
+
### Generar Certificados Autofirmados
|
|
31
|
+
|
|
32
|
+
Para generar certificados SSL autofirmados, ejecuta el siguiente comando:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
mkdir -p certs/auto
|
|
36
|
+
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout certs/auto/private.key -out certs/auto/certificate.crt -subj "/C=ES/ST=Madrid/L=Madrid/O=Kukuy/OU=IT Department/CN=localhost"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Esto creará los siguientes archivos:
|
|
40
|
+
- `certs/auto/certificate.crt`: El certificado SSL
|
|
41
|
+
- `certs/auto/private.key`: La llave privada
|
|
42
|
+
|
|
43
|
+
### Usar Certificados Existentes
|
|
44
|
+
|
|
45
|
+
Si tienes certificados SSL válidos emitidos por una CA, colócalos en el directorio `certs/auto` o en cualquier ubicación de tu elección y actualiza las rutas en las variables de entorno.
|
|
46
|
+
|
|
47
|
+
## Inicio del Balanceador con SSL
|
|
48
|
+
|
|
49
|
+
### Método 1: Variables de Entorno Directas
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
SSL_CERT_PATH=certs/auto/certificate.crt SSL_KEY_PATH=certs/auto/private.key BALANCER_HTTPS_PORT=8443 CONFIG_FILE_PATH=./servers_real.json node kukuy.js
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Método 2: Usando Scripts Preparados
|
|
56
|
+
|
|
57
|
+
El proyecto incluye scripts para facilitar el inicio:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Iniciar con SSL usando el script básico
|
|
61
|
+
./start-ssl.sh
|
|
62
|
+
|
|
63
|
+
# Iniciar con SSL usando el script de configuración
|
|
64
|
+
./start-ssl-config.sh
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Por defecto, ambos scripts utilizan el archivo `servers_real.json` para la configuración de servidores.
|
|
68
|
+
|
|
69
|
+
## Uso de ngrok con SSL
|
|
70
|
+
|
|
71
|
+
Para exponer tu balanceador SSL a Internet a través de ngrok:
|
|
72
|
+
|
|
73
|
+
1. Instala ngrok desde https://ngrok.com/download
|
|
74
|
+
2. Ejecuta el siguiente comando:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
ngrok tls 8443
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Esto creará un túnel seguro que redirigirá las conexiones SSL a tu balanceador local en el puerto 8443.
|
|
81
|
+
|
|
82
|
+
Alternativamente, puedes usar un túnel HTTP si prefieres que ngrok maneje la terminación SSL:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
ngrok http https://localhost:8443
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Scripts Disponibles
|
|
89
|
+
|
|
90
|
+
### `start-ssl.sh`
|
|
91
|
+
|
|
92
|
+
Script básico para iniciar Kukuy en modo SSL con valores predeterminados:
|
|
93
|
+
- Puerto HTTPS: 8443
|
|
94
|
+
- Archivo de certificado: `certs/auto/certificate.crt`
|
|
95
|
+
- Archivo de llave privada: `certs/auto/private.key`
|
|
96
|
+
- Archivo de configuración de servidores: `servers_real.json`
|
|
97
|
+
|
|
98
|
+
Opcionalmente, puedes habilitar el modo HTTP simultáneamente:
|
|
99
|
+
```bash
|
|
100
|
+
ENABLE_HTTP=true ./start-ssl.sh
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### `start-ssl-config.sh`
|
|
104
|
+
|
|
105
|
+
Script que carga la configuración desde el archivo `.env.ssl`. Este archivo contiene todas las variables de entorno necesarias para la operación del balanceador con SSL.
|
|
106
|
+
|
|
107
|
+
## Variables de Entorno
|
|
108
|
+
|
|
109
|
+
Las siguientes variables de entorno controlan la configuración SSL:
|
|
110
|
+
|
|
111
|
+
- `SSL_CERT_PATH`: Ruta al archivo de certificado SSL
|
|
112
|
+
- `SSL_KEY_PATH`: Ruta al archivo de llave privada SSL
|
|
113
|
+
- `BALANCER_HTTPS_PORT`: Puerto en el que escuchará el servidor HTTPS
|
|
114
|
+
- `BALANCER_HTTP_PORT`: Puerto opcional para el servidor HTTP (si se desea mantener ambos)
|
|
115
|
+
- `CONFIG_FILE_PATH`: Ruta al archivo de configuración de servidores
|
|
116
|
+
- `ROUTES_FILE_PATH`: Ruta al archivo de configuración de rutas
|
|
117
|
+
- `LOG_LEVEL`: Nivel de logging (info, debug, warn, error)
|
|
118
|
+
- `HEALTH_CHECK_INTERVAL`: Intervalo para verificaciones de salud de servidores (en milisegundos)
|
|
119
|
+
|
|
120
|
+
## Configuración de Servidores
|
|
121
|
+
|
|
122
|
+
El balanceador puede leer la configuración de servidores desde archivos JSON. Por defecto, este setup utiliza `servers_real.json` que contiene:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"servers": [
|
|
127
|
+
{
|
|
128
|
+
"url": "http://localhost:3434",
|
|
129
|
+
"weight": 1,
|
|
130
|
+
"tags": ["backend"]
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"url": "http://localhost:8765",
|
|
134
|
+
"weight": 1,
|
|
135
|
+
"tags": ["backend"]
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"url": "http://localhost:5445",
|
|
139
|
+
"weight": 1,
|
|
140
|
+
"tags": ["backend"]
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Puedes modificar este archivo para apuntar a tus servidores backend reales o crear nuevos archivos de configuración según sea necesario.
|
|
147
|
+
|
|
148
|
+
## Panel de Control
|
|
149
|
+
|
|
150
|
+
Una vez iniciado el balanceador, el panel de control web estará disponible en:
|
|
151
|
+
- HTTP: http://localhost:8082
|
|
152
|
+
|
|
153
|
+
## Solución de Problemas
|
|
154
|
+
|
|
155
|
+
### Certificados SSL Autofirmados
|
|
156
|
+
|
|
157
|
+
Cuando uses certificados autofirmados, los navegadores mostrarán advertencias de seguridad. Esto es normal y esperado. Para producción, utiliza certificados firmados por una Autoridad de Certificación (CA) reconocida.
|
|
158
|
+
|
|
159
|
+
### Puertos Bloqueados
|
|
160
|
+
|
|
161
|
+
Asegúrate de que los puertos que deseas usar (por defecto 8443 para HTTPS y 8082 para el panel) estén disponibles y no sean usados por otros servicios.
|
|
162
|
+
|
|
163
|
+
### Conexión a Servidores Backend
|
|
164
|
+
|
|
165
|
+
Verifica que los servidores backend definidos en el archivo de configuración estén accesibles desde la máquina donde corre el balanceador.
|
package/README.md
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
# KUKUY
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
|
|
3
5
|
Un balanceador de carga desarrollado en Node.js que distribuye solicitudes entre múltiples servidores backend usando el algoritmo RoundRobin.
|
|
4
6
|
|
|
7
|
+
## Información del Proyecto
|
|
8
|
+
|
|
9
|
+
- **Página Oficial**: [https://bsanchez.unaux.com/](https://bsanchez.unaux.com/)
|
|
10
|
+
- **Repositorio Oficial**: [https://gitlab.com/bytedogssyndicate1/kukuy](https://gitlab.com/bytedogssyndicate1/kukuy)
|
|
11
|
+
|
|
12
|
+
## Instalación
|
|
13
|
+
|
|
14
|
+
Para instalar Kukuy globalmente en tu sistema:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g kukuy
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
O para instalarlo localmente en tu proyecto:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install kukuy
|
|
24
|
+
```
|
|
25
|
+
|
|
5
26
|
## Características
|
|
6
27
|
|
|
7
28
|
- Distribución de carga usando algoritmo RoundRobin
|
package/kukuu1.webp
ADDED
|
Binary file
|
package/kukuy.js
CHANGED
|
@@ -1,23 +1,69 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { Balancer } = require('./src/core/Balancer');
|
|
4
|
+
const packageJson = require('./package.json');
|
|
5
|
+
|
|
6
|
+
// Medir tiempo de inicio
|
|
7
|
+
const startTime = Date.now();
|
|
8
|
+
const startMemory = process.memoryUsage();
|
|
4
9
|
|
|
5
10
|
// Iniciar el balanceador
|
|
6
11
|
const balancer = new Balancer();
|
|
7
12
|
balancer.start();
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
// Calcular tiempo de inicio y uso de memoria
|
|
15
|
+
const startupTime = Date.now() - startTime;
|
|
16
|
+
const endMemory = process.memoryUsage();
|
|
17
|
+
const memoryUsed = endMemory.heapUsed - startMemory.heapUsed;
|
|
18
|
+
|
|
19
|
+
// Colores para la consola
|
|
20
|
+
const colors = {
|
|
21
|
+
reset: '\x1b[0m',
|
|
22
|
+
bright: '\x1b[1m',
|
|
23
|
+
dim: '\x1b[2m',
|
|
24
|
+
underscore: '\x1b[4m',
|
|
25
|
+
blink: '\x1b[5m',
|
|
26
|
+
reverse: '\x1b[7m',
|
|
27
|
+
hidden: '\x1b[8m',
|
|
28
|
+
|
|
29
|
+
fgBlack: '\x1b[30m',
|
|
30
|
+
fgRed: '\x1b[31m',
|
|
31
|
+
fgGreen: '\x1b[32m',
|
|
32
|
+
fgYellow: '\x1b[33m',
|
|
33
|
+
fgBlue: '\x1b[34m',
|
|
34
|
+
fgMagenta: '\x1b[35m',
|
|
35
|
+
fgCyan: '\x1b[36m',
|
|
36
|
+
fgWhite: '\x1b[37m',
|
|
37
|
+
|
|
38
|
+
bgBlack: '\x1b[40m',
|
|
39
|
+
bgRed: '\x1b[41m',
|
|
40
|
+
bgGreen: '\x1b[42m',
|
|
41
|
+
bgYellow: '\x1b[43m',
|
|
42
|
+
bgBlue: '\x1b[44m',
|
|
43
|
+
bgMagenta: '\x1b[45m',
|
|
44
|
+
bgCyan: '\x1b[46m',
|
|
45
|
+
bgWhite: '\x1b[47m'
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Mostrar información con colores
|
|
49
|
+
console.log(`${colors.fgYellow}${colors.bright}╔══════════════════════════════════════════════════════════════╗${colors.reset}`);
|
|
50
|
+
console.log(`${colors.fgYellow}${colors.bright}║ KUKUY BALANCEADOR ║${colors.reset}`);
|
|
51
|
+
console.log(`${colors.fgYellow}${colors.bright}╚══════════════════════════════════════════════════════════════╝${colors.reset}`);
|
|
52
|
+
console.log(`${colors.fgYellow}Versión:${colors.reset} ${colors.fgGreen}${colors.bright}${packageJson.version}${colors.reset}`);
|
|
53
|
+
console.log(`${colors.fgYellow}Tiempo de inicio:${colors.reset} ${colors.fgGreen}${colors.bright}${startupTime} ms${colors.reset}`);
|
|
54
|
+
console.log(`${colors.fgYellow}Memoria consumida:${colors.reset} ${colors.fgGreen}${colors.bright}${Math.round(memoryUsed / 1024 / 1024 * 100) / 100} MB${colors.reset}`);
|
|
55
|
+
console.log(`${colors.fgYellow}${colors.bright}Balanceador RoundRobin iniciado${colors.reset}`);
|
|
56
|
+
console.log(`${colors.fgBlue}Panel web disponible en: http://localhost:${process.env.DASHBOARD_PORT || 8082}${colors.reset}`);
|
|
11
57
|
|
|
12
58
|
// Manejar señales de interrupción
|
|
13
59
|
process.on('SIGINT', () => {
|
|
14
|
-
console.log(
|
|
60
|
+
console.log(`\n${colors.fgRed}Cerrando balanceador...${colors.reset}`);
|
|
15
61
|
balancer.stop();
|
|
16
62
|
process.exit(0);
|
|
17
63
|
});
|
|
18
64
|
|
|
19
65
|
process.on('SIGTERM', () => {
|
|
20
|
-
console.log(
|
|
66
|
+
console.log(`${colors.fgRed}Recibida señal SIGTERM, cerrando...${colors.reset}`);
|
|
21
67
|
balancer.stop();
|
|
22
68
|
process.exit(0);
|
|
23
|
-
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Script para optimizar MariaDB y liberar espacio en disco
|
|
4
|
+
# Autor: Sistema de Optimización
|
|
5
|
+
# Fecha: $(date +%Y-%m-%d)
|
|
6
|
+
|
|
7
|
+
set -e # Salir si ocurre algún error
|
|
8
|
+
|
|
9
|
+
echo "=== Iniciando optimización de MariaDB y liberación de espacio ==="
|
|
10
|
+
|
|
11
|
+
# Crear directorio para backups
|
|
12
|
+
BACKUP_DIR="/home/bds/kukuy/backups-$(date +%Y%m%d_%H%M%S)"
|
|
13
|
+
mkdir -p "$BACKUP_DIR"
|
|
14
|
+
|
|
15
|
+
echo "Directorio de backup: $BACKUP_DIR"
|
|
16
|
+
|
|
17
|
+
# 1. BACKUP DE ARCHIVOS DE CONFIGURACIÓN DE MARIADB
|
|
18
|
+
echo ""
|
|
19
|
+
echo "1. Realizando backup de archivos de configuración..."
|
|
20
|
+
|
|
21
|
+
CONFIG_FILES=(
|
|
22
|
+
"/etc/mysql/mariadb.conf.d/50-server.cnf"
|
|
23
|
+
"/etc/mysql/mariadb.conf.d/50-client.cnf"
|
|
24
|
+
"/etc/mysql/mariadb.conf.d/50-mysql-clients.cnf"
|
|
25
|
+
"/etc/mysql/my.cnf"
|
|
26
|
+
"/etc/mysql/debian-start"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
for config_file in "${CONFIG_FILES[@]}"; do
|
|
30
|
+
if [ -f "$config_file" ]; then
|
|
31
|
+
cp "$config_file" "$BACKUP_DIR/$(basename "$config_file")"
|
|
32
|
+
echo " - Backup de $config_file realizado"
|
|
33
|
+
else
|
|
34
|
+
echo " - $config_file no encontrado"
|
|
35
|
+
fi
|
|
36
|
+
done
|
|
37
|
+
|
|
38
|
+
# 2. LIBERAR ESPACIO EN DISCO
|
|
39
|
+
echo ""
|
|
40
|
+
echo "2. Liberando espacio en disco..."
|
|
41
|
+
|
|
42
|
+
# Limpiar paquetes desactualizados
|
|
43
|
+
echo " - Limpiando paquetes desactualizados..."
|
|
44
|
+
sudo apt autoremove -y 2>/dev/null || echo " - No se pudo ejecutar apt autoremove (sin privilegios)"
|
|
45
|
+
|
|
46
|
+
# Limpiar cache de paquetes
|
|
47
|
+
echo " - Limpiando cache de paquetes..."
|
|
48
|
+
sudo apt autoclean 2>/dev/null || echo " - No se pudo ejecutar apt autoclean (sin privilegios)"
|
|
49
|
+
|
|
50
|
+
# Limpiar logs antiguos (sin sudo)
|
|
51
|
+
echo " - Buscando logs grandes en /tmp..."
|
|
52
|
+
find /tmp -name "*.log" -size +100M -delete 2>/dev/null || echo " - No se pudieron eliminar logs en /tmp (sin privilegios)"
|
|
53
|
+
|
|
54
|
+
# Limpiar historial de comandos si es necesario
|
|
55
|
+
echo " - Verificando tamaño de archivos de historial..."
|
|
56
|
+
if [ -f "$HOME/.bash_history" ] && [ $(stat -c%s "$HOME/.bash_history" 2>/dev/null || echo 0) -gt 100000 ]; then
|
|
57
|
+
tail -n 1000 "$HOME/.bash_history" > "$HOME/.bash_history.tmp" && mv "$HOME/.bash_history.tmp" "$HOME/.bash_history"
|
|
58
|
+
echo " Archivo de historial reducido"
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# 3. OPTIMIZACIÓN DE CONFIGURACIÓN DE MARIADB
|
|
62
|
+
echo ""
|
|
63
|
+
echo "3. Optimizando configuración de MariaDB..."
|
|
64
|
+
|
|
65
|
+
SERVER_CONFIG="/etc/mysql/mariadb.conf.d/50-server.cnf"
|
|
66
|
+
|
|
67
|
+
if [ -f "$SERVER_CONFIG" ]; then
|
|
68
|
+
# Crear copia de seguridad temporal
|
|
69
|
+
TEMP_CONFIG="$BACKUP_DIR/50-server.cnf.optimized"
|
|
70
|
+
cp "$SERVER_CONFIG" "$TEMP_CONFIG"
|
|
71
|
+
|
|
72
|
+
# Aplicar optimizaciones (solo si se puede escribir)
|
|
73
|
+
if [ -w "$SERVER_CONFIG" ]; then
|
|
74
|
+
echo " - Aplicando optimizaciones a $SERVER_CONFIG"
|
|
75
|
+
|
|
76
|
+
# Ajustar tamaño del buffer pool de InnoDB (si hay suficiente RAM)
|
|
77
|
+
TOTAL_RAM=$(free -m | awk 'NR==2{print $2}')
|
|
78
|
+
if [ $TOTAL_RAM -gt 2048 ]; then
|
|
79
|
+
# Si hay más de 2GB de RAM, asignar 25% a buffer pool
|
|
80
|
+
BUFFER_POOL_SIZE=$((TOTAL_RAM * 25 / 100))
|
|
81
|
+
BUFFER_POOL_BYTES=$((BUFFER_POOL_SIZE * 1024 * 1024))
|
|
82
|
+
else
|
|
83
|
+
# Si hay menos de 2GB, asignar 128MB
|
|
84
|
+
BUFFER_POOL_BYTES=134217728
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# Ajustar configuración de logs para reducir uso de disco
|
|
88
|
+
sudo tee -a "$SERVER_CONFIG" > /dev/null << EOF
|
|
89
|
+
|
|
90
|
+
# Optimizaciones añadidas por optimize-mariadb.sh
|
|
91
|
+
[mysqld]
|
|
92
|
+
# Ajustar tamaño del buffer pool de InnoDB
|
|
93
|
+
innodb_buffer_pool_size = ${BUFFER_POOL_BYTES}
|
|
94
|
+
|
|
95
|
+
# Reducir tamaño de logs para ahorrar espacio en disco
|
|
96
|
+
innodb_log_file_size = 67108864 # 64MB
|
|
97
|
+
innodb_log_files_in_group = 2
|
|
98
|
+
|
|
99
|
+
# Configurar rotación de logs binarios
|
|
100
|
+
expire_logs_days = 3
|
|
101
|
+
max_binlog_size = 100M
|
|
102
|
+
|
|
103
|
+
# Limitar tamaño de queries temporales
|
|
104
|
+
tmp_table_size = 64M
|
|
105
|
+
max_heap_table_size = 64M
|
|
106
|
+
|
|
107
|
+
# Ajustar timeouts para liberar conexiones rápidamente
|
|
108
|
+
wait_timeout = 300
|
|
109
|
+
interactive_timeout = 300
|
|
110
|
+
|
|
111
|
+
# Habilitar slow query log con umbral razonable
|
|
112
|
+
slow_query_log = 1
|
|
113
|
+
long_query_time = 2
|
|
114
|
+
slow_query_log_file = /var/log/mysql/slow.log
|
|
115
|
+
EOF
|
|
116
|
+
echo " Configuración optimizada aplicada"
|
|
117
|
+
else
|
|
118
|
+
echo " No se tienen permisos para modificar $SERVER_CONFIG"
|
|
119
|
+
echo " Las optimizaciones recomendadas son:"
|
|
120
|
+
echo " - innodb_buffer_pool_size = [25% de RAM disponible]"
|
|
121
|
+
echo " - innodb_log_file_size = 64M"
|
|
122
|
+
echo " - expire_logs_days = 3"
|
|
123
|
+
echo " - wait_timeout = 300"
|
|
124
|
+
fi
|
|
125
|
+
else
|
|
126
|
+
echo " - $SERVER_CONFIG no encontrado"
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# 4. ANALIZAR USO DE ESPACIO EN MARIADB
|
|
130
|
+
echo ""
|
|
131
|
+
echo "4. Analizando uso de espacio en bases de datos..."
|
|
132
|
+
|
|
133
|
+
# Obtener información de tamaño de bases de datos
|
|
134
|
+
mysql -u root -e "
|
|
135
|
+
SELECT
|
|
136
|
+
table_schema AS 'Base de Datos',
|
|
137
|
+
ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS 'Tamaño (MB)'
|
|
138
|
+
FROM information_schema.tables
|
|
139
|
+
GROUP BY table_schema
|
|
140
|
+
ORDER BY (data_length + index_length) DESC;
|
|
141
|
+
" 2>/dev/null || echo " - No se pudo acceder a la información de bases de datos"
|
|
142
|
+
|
|
143
|
+
# 5. RECOMENDACIONES FINALES
|
|
144
|
+
echo ""
|
|
145
|
+
echo "=== Recomendaciones ==="
|
|
146
|
+
echo "1. Revisa el directorio de backup: $BACKUP_DIR"
|
|
147
|
+
echo "2. Reinicia MariaDB para aplicar cambios de configuración:"
|
|
148
|
+
echo " sudo systemctl restart mariadb"
|
|
149
|
+
echo "3. Considera eliminar archivos innecesarios en el sistema"
|
|
150
|
+
echo "4. Monitorea el uso de disco regularmente con 'df -h'"
|
|
151
|
+
echo ""
|
|
152
|
+
echo "=== Optimización completada ==="
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kukuy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Balanceador de carga Backend",
|
|
5
5
|
"main": "kukuy.js",
|
|
6
6
|
"scripts": {
|
|
@@ -22,5 +22,10 @@
|
|
|
22
22
|
"jerk-hooked-lib": "^2.0.0",
|
|
23
23
|
"ws": "^8.19.0"
|
|
24
24
|
},
|
|
25
|
-
"type": "commonjs"
|
|
25
|
+
"type": "commonjs",
|
|
26
|
+
"homepage": "https://bsanchez.unaux.com/",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://gitlab.com/bytedogssyndicate1/kukuy"
|
|
30
|
+
}
|
|
26
31
|
}
|
|
@@ -9,6 +9,10 @@ class IPHashAlgorithm extends LoadBalancingAlgorithm {
|
|
|
9
9
|
super();
|
|
10
10
|
// Mapa para almacenar la asociación persistente IP -> índice de servidor
|
|
11
11
|
this.ipToServerIndexMap = new Map();
|
|
12
|
+
// Caché para servidores filtrados
|
|
13
|
+
this.filteredServersCache = new Map();
|
|
14
|
+
this.cacheTimestamp = 0;
|
|
15
|
+
this.cacheValidity = 1000; // 1 segundo de validez de caché
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
/**
|
|
@@ -22,10 +26,23 @@ class IPHashAlgorithm extends LoadBalancingAlgorithm {
|
|
|
22
26
|
return null;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
//
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
// Obtener timestamp actual
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
|
|
32
|
+
// Verificar si la caché es válida
|
|
33
|
+
let availableServers;
|
|
34
|
+
if (now - this.cacheTimestamp > this.cacheValidity || !this.filteredServersCache.has(servers)) {
|
|
35
|
+
// Filtrar servidores activos y saludables
|
|
36
|
+
availableServers = servers.filter(server =>
|
|
37
|
+
server.active !== false && server.healthy && server.failedAttempts < 5
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Actualizar caché
|
|
41
|
+
this.filteredServersCache.set(servers, availableServers);
|
|
42
|
+
this.cacheTimestamp = now;
|
|
43
|
+
} else {
|
|
44
|
+
availableServers = this.filteredServersCache.get(servers);
|
|
45
|
+
}
|
|
29
46
|
|
|
30
47
|
if (availableServers.length === 0) {
|
|
31
48
|
return null;
|
|
@@ -43,7 +60,6 @@ class IPHashAlgorithm extends LoadBalancingAlgorithm {
|
|
|
43
60
|
const cachedServer = availableServers[serverIndex];
|
|
44
61
|
if (cachedServer && cachedServer.healthy && cachedServer.active) {
|
|
45
62
|
// Servidor sigue disponible, usarlo
|
|
46
|
-
console.log(`IPHash: IP=${clientIP} reutilizando servidor previamente asignado (índice ${serverIndex})`);
|
|
47
63
|
return cachedServer;
|
|
48
64
|
} else {
|
|
49
65
|
// Servidor ya no está disponible, eliminar la asociación
|
|
@@ -61,9 +77,6 @@ class IPHashAlgorithm extends LoadBalancingAlgorithm {
|
|
|
61
77
|
|
|
62
78
|
const selectedServer = availableServers[serverIndex];
|
|
63
79
|
|
|
64
|
-
// Log para mostrar el mapa de hash
|
|
65
|
-
console.log(`IPHash: IP=${clientIP}, Hash=${hash}, Asignando servidor índice ${serverIndex} (ID: ${selectedServer.id}, URL: ${selectedServer.url})`);
|
|
66
|
-
|
|
67
80
|
return selectedServer;
|
|
68
81
|
}
|
|
69
82
|
|
|
@@ -104,12 +117,11 @@ class IPHashAlgorithm extends LoadBalancingAlgorithm {
|
|
|
104
117
|
*/
|
|
105
118
|
simpleHash(str) {
|
|
106
119
|
let hash = 0;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
hash = ((hash << 5) - hash) +
|
|
110
|
-
hash = hash & hash; // Convertir a 32-bit integer
|
|
120
|
+
const len = str.length;
|
|
121
|
+
for (let i = 0; i < len; i++) {
|
|
122
|
+
hash = ((hash << 5) - hash) + str.charCodeAt(i);
|
|
111
123
|
}
|
|
112
|
-
return Math.abs(hash);
|
|
124
|
+
return Math.abs(hash | 0); // Convertir a 32-bit integer
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
/**
|
|
@@ -8,6 +8,10 @@ class RoundRobinAlgorithm extends LoadBalancingAlgorithm {
|
|
|
8
8
|
constructor() {
|
|
9
9
|
super();
|
|
10
10
|
this.currentIndex = 0;
|
|
11
|
+
// Caché para servidores filtrados
|
|
12
|
+
this.filteredServersCache = new Map();
|
|
13
|
+
this.cacheTimestamp = 0;
|
|
14
|
+
this.cacheValidity = 1000; // 1 segundo de validez de caché
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
/**
|
|
@@ -21,39 +25,33 @@ class RoundRobinAlgorithm extends LoadBalancingAlgorithm {
|
|
|
21
25
|
return null;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
//
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
// Obtener timestamp actual
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
|
|
31
|
+
// Verificar si la caché es válida
|
|
32
|
+
let availableServers;
|
|
33
|
+
if (now - this.cacheTimestamp > this.cacheValidity || !this.filteredServersCache.has(servers)) {
|
|
34
|
+
// Filtrar servidores activos y saludables
|
|
35
|
+
availableServers = servers.filter(server =>
|
|
36
|
+
server.active !== false && server.healthy && server.failedAttempts < 5
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Actualizar caché
|
|
40
|
+
this.filteredServersCache.set(servers, availableServers);
|
|
41
|
+
this.cacheTimestamp = now;
|
|
42
|
+
} else {
|
|
43
|
+
availableServers = this.filteredServersCache.get(servers);
|
|
44
|
+
}
|
|
28
45
|
|
|
29
46
|
if (availableServers.length === 0) {
|
|
30
47
|
return null;
|
|
31
48
|
}
|
|
32
49
|
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const candidateIndex = (this.currentIndex + i) % availableServers.length;
|
|
37
|
-
const candidateServer = availableServers[candidateIndex];
|
|
38
|
-
|
|
39
|
-
// Verificar si el servidor candidato está realmente disponible
|
|
40
|
-
if (this.isServerAvailable(candidateServer)) {
|
|
41
|
-
this.currentIndex = candidateIndex + 1; // Preparar para la próxima selección
|
|
42
|
-
return candidateServer;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Si ningún servidor está disponible, devolver null
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
50
|
+
// Obtener servidor usando round robin sin bucle
|
|
51
|
+
const server = availableServers[this.currentIndex % availableServers.length];
|
|
52
|
+
this.currentIndex = (this.currentIndex + 1) % availableServers.length;
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
isServerAvailable(server) {
|
|
52
|
-
// Un servidor está disponible si:
|
|
53
|
-
// 1. Está marcado como healthy
|
|
54
|
-
// 2. Está activo
|
|
55
|
-
// 3. No ha superado el número máximo de intentos fallidos
|
|
56
|
-
return server.healthy && server.active && server.failedAttempts < 5;
|
|
54
|
+
return server;
|
|
57
55
|
}
|
|
58
56
|
|
|
59
57
|
/**
|
package/src/core/Balancer.js
CHANGED
|
@@ -178,11 +178,12 @@ class Balancer {
|
|
|
178
178
|
|
|
179
179
|
if (matchedRoute && matchedRoute.target) {
|
|
180
180
|
// Si hay una ruta específica, usar solo los servidores definidos para esa ruta
|
|
181
|
-
const routeServers = this.serverPool.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
181
|
+
const routeServers = this.serverPool.getServersByTag(matchedRoute.target);
|
|
182
|
+
if (routeServers && routeServers.length > 0) {
|
|
183
|
+
const healthyRouteServers = routeServers.filter(server => server.healthy && server.active);
|
|
184
|
+
if (healthyRouteServers.length > 0) {
|
|
185
|
+
return this.algorithmManager.selectServer(healthyRouteServers, requestContext);
|
|
186
|
+
}
|
|
186
187
|
}
|
|
187
188
|
}
|
|
188
189
|
|
|
@@ -197,10 +198,10 @@ class Balancer {
|
|
|
197
198
|
let retryCount = 0;
|
|
198
199
|
const maxRetries = this.serverPool.getHealthyServers().length; // Máximo de reintentos según servidores saludables
|
|
199
200
|
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const parsedUrl = url.parse(
|
|
201
|
+
// Función auxiliar para manejar la solicitud
|
|
202
|
+
const makeRequest = (server) => {
|
|
203
|
+
return new Promise((resolve, reject) => {
|
|
204
|
+
const parsedUrl = url.parse(server.url);
|
|
204
205
|
const targetOptions = {
|
|
205
206
|
hostname: parsedUrl.hostname,
|
|
206
207
|
port: parsedUrl.port,
|
|
@@ -209,133 +210,134 @@ class Balancer {
|
|
|
209
210
|
headers: clientReq.headers
|
|
210
211
|
};
|
|
211
212
|
|
|
212
|
-
const httpModule =
|
|
213
|
+
const httpModule = server.protocol === 'https:' ? require('https') : require('http');
|
|
213
214
|
const proxyReq = httpModule.request(targetOptions);
|
|
214
215
|
|
|
215
216
|
// Registrar intento de conexión a servidor target
|
|
216
|
-
this.balancerLogger.logTargetSelection(
|
|
217
|
+
this.balancerLogger.logTargetSelection(server, 'round_robin');
|
|
217
218
|
|
|
218
219
|
// Enviar cuerpo de la solicitud
|
|
219
220
|
clientReq.pipe(proxyReq);
|
|
220
221
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
responseTime,
|
|
248
|
-
statusCode: serverRes.statusCode
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
// Actualizar estado del servidor en las métricas
|
|
252
|
-
this.metricsCollector.updateServerStatus(targetServer.id, 'online');
|
|
253
|
-
|
|
254
|
-
// Emitir hook cuando se envía la respuesta
|
|
255
|
-
this.hookManager.executeHooks('onResponseSent', {
|
|
256
|
-
req: clientReq,
|
|
257
|
-
res: clientRes,
|
|
258
|
-
serverRes,
|
|
259
|
-
server: targetServer,
|
|
260
|
-
responseTime
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
resolve(); // Resolver la promesa al completar la solicitud
|
|
222
|
+
proxyReq.on('response', (serverRes) => {
|
|
223
|
+
// Copiar headers de la respuesta
|
|
224
|
+
clientRes.writeHead(serverRes.statusCode, serverRes.headers);
|
|
225
|
+
|
|
226
|
+
// Enviar cuerpo de la respuesta al cliente
|
|
227
|
+
serverRes.pipe(clientRes);
|
|
228
|
+
|
|
229
|
+
// Calcular tiempo de respuesta
|
|
230
|
+
const responseTime = Date.now() - startTime;
|
|
231
|
+
|
|
232
|
+
// Registrar métricas profesionales
|
|
233
|
+
this.metricsCollector.recordRequest(
|
|
234
|
+
clientReq.method,
|
|
235
|
+
clientReq.url,
|
|
236
|
+
server.id,
|
|
237
|
+
responseTime,
|
|
238
|
+
serverRes.statusCode,
|
|
239
|
+
serverRes.statusCode < 400
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Registrar servidor online
|
|
243
|
+
this.balancerLogger.logOnlineTarget(server, {
|
|
244
|
+
url: clientReq.url,
|
|
245
|
+
method: clientReq.method,
|
|
246
|
+
responseTime,
|
|
247
|
+
statusCode: serverRes.statusCode
|
|
264
248
|
});
|
|
265
249
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
this.logger.error(`Error conectando con servidor ${targetServer.url}: ${err.message}`);
|
|
270
|
-
|
|
271
|
-
// Registrar servidor offline
|
|
272
|
-
this.balancerLogger.logOfflineTarget(targetServer, err, {
|
|
273
|
-
url: clientReq.url,
|
|
274
|
-
method: clientReq.method,
|
|
275
|
-
responseTime
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
// Actualizar estado del servidor en las métricas
|
|
279
|
-
this.metricsCollector.updateServerStatus(targetServer.id, 'offline');
|
|
280
|
-
|
|
281
|
-
// Registrar métricas profesionales para error
|
|
282
|
-
this.metricsCollector.recordRequest(
|
|
283
|
-
clientReq.method,
|
|
284
|
-
clientReq.url,
|
|
285
|
-
targetServer.id,
|
|
286
|
-
responseTime,
|
|
287
|
-
502,
|
|
288
|
-
false
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
// Marcar servidor como fallido
|
|
292
|
-
this.serverPool.markServerAsFailed(targetServer.id);
|
|
293
|
-
|
|
294
|
-
// Emitir hook cuando ocurre un error con el servidor
|
|
295
|
-
await this.hookManager.executeHooks('onServerError', {
|
|
296
|
-
req: clientReq,
|
|
297
|
-
res: clientRes,
|
|
298
|
-
server: targetServer,
|
|
299
|
-
error: err,
|
|
300
|
-
responseTime
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
// Si hay más servidores disponibles, reintentar con otro servidor
|
|
304
|
-
if (retryCount < maxRetries) {
|
|
305
|
-
retryCount++;
|
|
306
|
-
|
|
307
|
-
// Obtener un nuevo servidor objetivo (excluyendo el que falló)
|
|
308
|
-
const availableServers = this.serverPool.getHealthyServers().filter(s => s.id !== targetServer.id);
|
|
309
|
-
if (availableServers.length > 0) {
|
|
310
|
-
// Seleccionar un nuevo servidor
|
|
311
|
-
targetServer = availableServers[(retryCount - 1) % availableServers.length];
|
|
312
|
-
console.log(`Reintentando con servidor alternativo: ${targetServer.url}`);
|
|
313
|
-
|
|
314
|
-
// Volver a intentar la solicitud con el nuevo servidor
|
|
315
|
-
setTimeout(() => {
|
|
316
|
-
this.proxyRequest(clientReq, clientRes, targetServer).then(resolve).catch(reject);
|
|
317
|
-
}, 10); // Pequeña pausa antes de reintentar
|
|
318
|
-
|
|
319
|
-
return; // Salir para evitar resolver/rechazar la promesa dos veces
|
|
320
|
-
}
|
|
321
|
-
}
|
|
250
|
+
// Actualizar estado del servidor en las métricas
|
|
251
|
+
this.metricsCollector.updateServerStatus(server.id, 'online');
|
|
322
252
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
253
|
+
// Emitir hook cuando se envía la respuesta
|
|
254
|
+
this.hookManager.executeHooks('onResponseSent', {
|
|
255
|
+
req: clientReq,
|
|
256
|
+
res: clientRes,
|
|
257
|
+
serverRes,
|
|
258
|
+
server,
|
|
259
|
+
responseTime
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
resolve(); // Resolver la promesa al completar la solicitud
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
proxyReq.on('error', async (err) => {
|
|
266
|
+
const responseTime = Date.now() - startTime;
|
|
328
267
|
|
|
329
|
-
|
|
268
|
+
this.logger.error(`Error conectando con servidor ${server.url}: ${err.message}`);
|
|
269
|
+
|
|
270
|
+
// Registrar servidor offline
|
|
271
|
+
this.balancerLogger.logOfflineTarget(server, err, {
|
|
272
|
+
url: clientReq.url,
|
|
273
|
+
method: clientReq.method,
|
|
274
|
+
responseTime
|
|
330
275
|
});
|
|
276
|
+
|
|
277
|
+
// Actualizar estado del servidor en las métricas
|
|
278
|
+
this.metricsCollector.updateServerStatus(server.id, 'offline');
|
|
279
|
+
|
|
280
|
+
// Registrar métricas profesionales para error
|
|
281
|
+
this.metricsCollector.recordRequest(
|
|
282
|
+
clientReq.method,
|
|
283
|
+
clientReq.url,
|
|
284
|
+
server.id,
|
|
285
|
+
responseTime,
|
|
286
|
+
502,
|
|
287
|
+
false
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// Marcar servidor como fallido
|
|
291
|
+
this.serverPool.markServerAsFailed(server.id);
|
|
292
|
+
|
|
293
|
+
// Emitir hook cuando ocurre un error con el servidor
|
|
294
|
+
await this.hookManager.executeHooks('onServerError', {
|
|
295
|
+
req: clientReq,
|
|
296
|
+
res: clientRes,
|
|
297
|
+
server,
|
|
298
|
+
error: err,
|
|
299
|
+
responseTime
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Si hay más servidores disponibles, reintentar con otro servidor
|
|
303
|
+
if (retryCount < maxRetries) {
|
|
304
|
+
retryCount++;
|
|
305
|
+
|
|
306
|
+
// Obtener un nuevo servidor objetivo (excluyendo los que ya fallaron)
|
|
307
|
+
const availableServers = this.serverPool.getHealthyServers().filter(s => s.id !== server.id);
|
|
308
|
+
if (availableServers.length > 0) {
|
|
309
|
+
// Seleccionar un nuevo servidor
|
|
310
|
+
const nextServer = availableServers[(retryCount - 1) % availableServers.length];
|
|
311
|
+
console.log(`Reintentando con servidor alternativo: ${nextServer.url}`);
|
|
312
|
+
|
|
313
|
+
// Volver a intentar la solicitud con el nuevo servidor
|
|
314
|
+
setTimeout(() => {
|
|
315
|
+
makeRequest(nextServer).then(resolve).catch(reject);
|
|
316
|
+
}, 10); // Pequeña pausa antes de reintentar
|
|
317
|
+
|
|
318
|
+
return; // Salir para evitar resolver/rechazar la promesa dos veces
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Si no hay más reintentos posibles, enviar error al cliente
|
|
323
|
+
if (!clientRes.headersSent) {
|
|
324
|
+
clientRes.writeHead(502);
|
|
325
|
+
clientRes.end('Bad Gateway');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
resolve(); // Resolver para evitar múltiples llamadas
|
|
331
329
|
});
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
330
|
+
});
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// Intentar enviar la solicitud con reintento si el servidor inicial falla
|
|
334
|
+
try {
|
|
335
|
+
await makeRequest(targetServer);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
console.error('Error en proxyRequest:', error);
|
|
338
|
+
if (!clientRes.headersSent) {
|
|
339
|
+
clientRes.writeHead(500);
|
|
340
|
+
clientRes.end('Internal Server Error');
|
|
339
341
|
}
|
|
340
342
|
}
|
|
341
343
|
}
|
package/src/core/ServerPool.js
CHANGED
|
@@ -3,8 +3,12 @@ const { HealthChecker } = require('../utils/HealthChecker');
|
|
|
3
3
|
class ServerPool {
|
|
4
4
|
constructor() {
|
|
5
5
|
this.servers = [];
|
|
6
|
+
this.healthyServers = []; // Caché de servidores saludables
|
|
7
|
+
this.serversById = new Map(); // Acceso rápido por ID
|
|
8
|
+
this.serversByTag = new Map(); // Caché de servidores por tag
|
|
6
9
|
this.healthChecker = new HealthChecker();
|
|
7
10
|
this.nextId = 1;
|
|
11
|
+
this.cacheValid = false; // Indicador de validez de caché
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
addServer(serverConfig) {
|
|
@@ -22,6 +26,12 @@ class ServerPool {
|
|
|
22
26
|
};
|
|
23
27
|
|
|
24
28
|
this.servers.push(server);
|
|
29
|
+
this.serversById.set(server.id, server);
|
|
30
|
+
|
|
31
|
+
// Actualizar cachés
|
|
32
|
+
this.updateTagCache(server);
|
|
33
|
+
this.cacheValid = false;
|
|
34
|
+
|
|
25
35
|
return server;
|
|
26
36
|
}
|
|
27
37
|
|
|
@@ -36,19 +46,39 @@ class ServerPool {
|
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
getHealthyServers() {
|
|
39
|
-
|
|
49
|
+
if (!this.cacheValid) {
|
|
50
|
+
this.healthyServers = this.servers.filter(server => server.healthy && server.active);
|
|
51
|
+
this.cacheValid = true;
|
|
52
|
+
}
|
|
53
|
+
return this.healthyServers;
|
|
40
54
|
}
|
|
41
55
|
|
|
42
56
|
getServersByTag(tag) {
|
|
43
|
-
|
|
57
|
+
if (this.serversByTag.has(tag)) {
|
|
58
|
+
return this.serversByTag.get(tag);
|
|
59
|
+
}
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Actualiza la caché de servidores por tag
|
|
64
|
+
updateTagCache(server) {
|
|
65
|
+
for (const tag of server.tags) {
|
|
66
|
+
if (!this.serversByTag.has(tag)) {
|
|
67
|
+
this.serversByTag.set(tag, []);
|
|
68
|
+
}
|
|
69
|
+
this.serversByTag.get(tag).push(server);
|
|
70
|
+
}
|
|
44
71
|
}
|
|
45
72
|
|
|
46
73
|
markServerAsFailed(serverId) {
|
|
47
|
-
const server = this.
|
|
74
|
+
const server = this.serversById.get(serverId);
|
|
48
75
|
if (server) {
|
|
49
76
|
server.failedAttempts++;
|
|
50
77
|
server.healthy = false;
|
|
51
|
-
|
|
78
|
+
|
|
79
|
+
// Invalidar caché porque un servidor cambió de estado
|
|
80
|
+
this.cacheValid = false;
|
|
81
|
+
|
|
52
82
|
// Programar verificación de salud después de un tiempo
|
|
53
83
|
setTimeout(() => {
|
|
54
84
|
this.healthChecker.checkServerHealth(server)
|
|
@@ -57,11 +87,17 @@ class ServerPool {
|
|
|
57
87
|
server.healthy = true;
|
|
58
88
|
server.failedAttempts = 0;
|
|
59
89
|
server.lastChecked = Date.now();
|
|
90
|
+
|
|
91
|
+
// Invalidar caché porque un servidor cambió de estado
|
|
92
|
+
this.cacheValid = false;
|
|
60
93
|
} else {
|
|
61
94
|
server.lastChecked = Date.now();
|
|
62
95
|
// Si ha fallado demasiadas veces, mantenerlo inactivo
|
|
63
96
|
if (server.failedAttempts > 5) {
|
|
64
97
|
server.active = false;
|
|
98
|
+
|
|
99
|
+
// Invalidar caché porque un servidor cambió de estado
|
|
100
|
+
this.cacheValid = false;
|
|
65
101
|
}
|
|
66
102
|
}
|
|
67
103
|
});
|
|
@@ -70,7 +106,12 @@ class ServerPool {
|
|
|
70
106
|
}
|
|
71
107
|
|
|
72
108
|
getServerById(serverId) {
|
|
73
|
-
return this.
|
|
109
|
+
return this.serversById.get(serverId);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Método para invalidar la caché manualmente si es necesario
|
|
113
|
+
invalidateCache() {
|
|
114
|
+
this.cacheValid = false;
|
|
74
115
|
}
|
|
75
116
|
}
|
|
76
117
|
|
|
@@ -15,34 +15,40 @@ class HealthChecker {
|
|
|
15
15
|
port: parsedUrl.port,
|
|
16
16
|
path: '/health', // Ruta estándar para verificación de salud
|
|
17
17
|
method: 'GET',
|
|
18
|
-
timeout: this.timeout
|
|
18
|
+
timeout: this.timeout,
|
|
19
|
+
// Agregar headers para identificar la solicitud de health check
|
|
20
|
+
headers: {
|
|
21
|
+
'User-Agent': 'Kukuy-Health-Check/1.0'
|
|
22
|
+
}
|
|
19
23
|
};
|
|
20
24
|
|
|
21
25
|
return new Promise((resolve) => {
|
|
22
|
-
const request = server.protocol === 'https:'
|
|
26
|
+
const request = server.protocol === 'https:'
|
|
23
27
|
? https.request(options)
|
|
24
28
|
: http.request(options);
|
|
25
29
|
|
|
26
30
|
request.on('response', (res) => {
|
|
31
|
+
// Consumir el cuerpo de la respuesta para liberar recursos
|
|
32
|
+
res.resume();
|
|
33
|
+
|
|
27
34
|
// Considerar saludable si obtenemos una respuesta exitosa
|
|
28
35
|
const isHealthy = res.statusCode >= 200 && res.statusCode < 400;
|
|
29
36
|
resolve(isHealthy);
|
|
30
37
|
});
|
|
31
38
|
|
|
32
39
|
request.on('error', (err) => {
|
|
33
|
-
|
|
40
|
+
// No imprimir errores de health check en consola para evitar spam
|
|
34
41
|
resolve(false);
|
|
35
42
|
});
|
|
36
43
|
|
|
37
44
|
request.on('timeout', () => {
|
|
38
|
-
|
|
45
|
+
// No imprimir errores de timeout de health check en consola
|
|
39
46
|
resolve(false);
|
|
40
47
|
});
|
|
41
48
|
|
|
42
49
|
request.end();
|
|
43
50
|
});
|
|
44
51
|
} catch (error) {
|
|
45
|
-
console.error(`Error en la verificación de salud: ${error.message}`);
|
|
46
52
|
return false;
|
|
47
53
|
}
|
|
48
54
|
}
|
|
@@ -122,12 +122,16 @@ class ProfessionalMetrics {
|
|
|
122
122
|
|
|
123
123
|
// Actualizar métricas de rendimiento
|
|
124
124
|
this.metrics.performance.totalResponseTime += responseTime;
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
if (responseTime < this.metrics.performance.minResponseTime) {
|
|
126
|
+
this.metrics.performance.minResponseTime = responseTime;
|
|
127
|
+
}
|
|
128
|
+
if (responseTime > this.metrics.performance.maxResponseTime) {
|
|
129
|
+
this.metrics.performance.maxResponseTime = responseTime;
|
|
130
|
+
}
|
|
127
131
|
|
|
128
132
|
// Almacenar tiempos de respuesta para calcular percentiles
|
|
129
133
|
this.metrics.responseTimes.push(responseTime);
|
|
130
|
-
if (this.metrics.responseTimes.length >
|
|
134
|
+
if (this.metrics.responseTimes.length > 1000) { // Reducir tamaño del array para mejor rendimiento
|
|
131
135
|
this.metrics.responseTimes.shift();
|
|
132
136
|
}
|
|
133
137
|
|
|
@@ -138,8 +142,10 @@ class ProfessionalMetrics {
|
|
|
138
142
|
this.metrics.performance.avgResponseTime =
|
|
139
143
|
this.metrics.performance.totalResponseTime / this.metrics.requests.total;
|
|
140
144
|
|
|
141
|
-
// Calcular percentiles
|
|
142
|
-
this.
|
|
145
|
+
// Calcular percentiles (solo cada cierto número de solicitudes para mejorar rendimiento)
|
|
146
|
+
if (this.metrics.requests.total % 100 === 0) {
|
|
147
|
+
this.calculatePercentiles();
|
|
148
|
+
}
|
|
143
149
|
|
|
144
150
|
this.metrics.timestamps.lastUpdate = now;
|
|
145
151
|
}
|
|
@@ -153,9 +159,9 @@ class ProfessionalMetrics {
|
|
|
153
159
|
successfulRequests: 0,
|
|
154
160
|
failedRequests: 0,
|
|
155
161
|
totalResponseTime: 0,
|
|
156
|
-
minResponseTime:
|
|
157
|
-
maxResponseTime:
|
|
158
|
-
avgResponseTime:
|
|
162
|
+
minResponseTime: responseTime, // Iniciar con el primer valor
|
|
163
|
+
maxResponseTime: responseTime,
|
|
164
|
+
avgResponseTime: responseTime,
|
|
159
165
|
responseCodes: {},
|
|
160
166
|
uptimeRatio: 0,
|
|
161
167
|
lastActive: Date.now(),
|
|
@@ -175,8 +181,12 @@ class ProfessionalMetrics {
|
|
|
175
181
|
|
|
176
182
|
// Actualizar tiempos de respuesta
|
|
177
183
|
serverStat.totalResponseTime += responseTime;
|
|
178
|
-
|
|
179
|
-
|
|
184
|
+
if (responseTime < serverStat.minResponseTime) {
|
|
185
|
+
serverStat.minResponseTime = responseTime;
|
|
186
|
+
}
|
|
187
|
+
if (responseTime > serverStat.maxResponseTime) {
|
|
188
|
+
serverStat.maxResponseTime = responseTime;
|
|
189
|
+
}
|
|
180
190
|
|
|
181
191
|
// Actualizar códigos de respuesta
|
|
182
192
|
if (!serverStat.responseCodes[responseCode]) {
|
|
@@ -192,14 +202,9 @@ class ProfessionalMetrics {
|
|
|
192
202
|
|
|
193
203
|
// Almacenar tiempos de respuesta para percentiles por servidor
|
|
194
204
|
serverStat.responseTimes.push(responseTime);
|
|
195
|
-
if (serverStat.responseTimes.length >
|
|
205
|
+
if (serverStat.responseTimes.length > 100) { // Reducir tamaño del array para mejor rendimiento
|
|
196
206
|
serverStat.responseTimes.shift();
|
|
197
207
|
}
|
|
198
|
-
|
|
199
|
-
// Asegurar que minResponseTime sea válido
|
|
200
|
-
if (serverStat.minResponseTime === Infinity) {
|
|
201
|
-
serverStat.minResponseTime = 0;
|
|
202
|
-
}
|
|
203
208
|
}
|
|
204
209
|
|
|
205
210
|
getServerStats(serverId) {
|
|
@@ -213,7 +218,7 @@ class ProfessionalMetrics {
|
|
|
213
218
|
if (serverId) {
|
|
214
219
|
const serverStat = this.metrics.serverStats[serverId];
|
|
215
220
|
if (serverStat && serverStat.responseTimes.length > 0) {
|
|
216
|
-
const sorted =
|
|
221
|
+
const sorted = Array.from(serverStat.responseTimes).sort((a, b) => a - b);
|
|
217
222
|
const length = sorted.length;
|
|
218
223
|
|
|
219
224
|
// Calcular P95 (percentil 95)
|
|
@@ -234,14 +239,16 @@ class ProfessionalMetrics {
|
|
|
234
239
|
|
|
235
240
|
calculatePercentiles() {
|
|
236
241
|
if (this.metrics.responseTimes.length === 0) return;
|
|
237
|
-
|
|
238
|
-
|
|
242
|
+
|
|
243
|
+
// Usar un algoritmo más eficiente para ordenar solo los elementos necesarios
|
|
244
|
+
// para calcular percentiles sin ordenar completamente el array
|
|
245
|
+
const sorted = Array.from(this.metrics.responseTimes).sort((a, b) => a - b);
|
|
239
246
|
const length = sorted.length;
|
|
240
|
-
|
|
247
|
+
|
|
241
248
|
// Calcular P95 (percentil 95)
|
|
242
249
|
const p95Index = Math.floor(length * 0.95);
|
|
243
250
|
this.metrics.performance.p95ResponseTime = sorted[p95Index] || 0;
|
|
244
|
-
|
|
251
|
+
|
|
245
252
|
// Calcular P99 (percentil 99)
|
|
246
253
|
const p99Index = Math.floor(length * 0.99);
|
|
247
254
|
this.metrics.performance.p99ResponseTime = sorted[p99Index] || 0;
|
|
@@ -395,11 +402,21 @@ class ProfessionalMetrics {
|
|
|
395
402
|
this.recentRequests = [];
|
|
396
403
|
}
|
|
397
404
|
|
|
398
|
-
|
|
405
|
+
const now = Date.now();
|
|
406
|
+
this.recentRequests.push(now);
|
|
399
407
|
|
|
400
408
|
// Limpiar solicitudes antiguas (> 5 segundos) para mantener solo las recientes
|
|
401
|
-
|
|
402
|
-
|
|
409
|
+
// Solo limpiar cada ciertos registros para mejorar rendimiento
|
|
410
|
+
if (this.recentRequests.length % 10 === 0) {
|
|
411
|
+
const fiveSecondsAgo = now - 5000;
|
|
412
|
+
let i = 0;
|
|
413
|
+
while (i < this.recentRequests.length && this.recentRequests[i] <= fiveSecondsAgo) {
|
|
414
|
+
i++;
|
|
415
|
+
}
|
|
416
|
+
if (i > 0) {
|
|
417
|
+
this.recentRequests.splice(0, i);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
403
420
|
}
|
|
404
421
|
|
|
405
422
|
updateSystemMetrics() {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Script para iniciar Kukuy con archivo de configuración SSL
|
|
2
|
+
|
|
3
|
+
# Cargar variables de entorno desde archivo .env.ssl
|
|
4
|
+
if [ -f .env.ssl ]; then
|
|
5
|
+
export $(cat .env.ssl | grep -v '^#' | xargs)
|
|
6
|
+
fi
|
|
7
|
+
|
|
8
|
+
# Asegurar que use el archivo de servidores real
|
|
9
|
+
export CONFIG_FILE_PATH="./servers_real.json"
|
|
10
|
+
|
|
11
|
+
echo "Iniciando Kukuy en modo SSL con configuración desde .env.ssl..."
|
|
12
|
+
echo "Puerto HTTPS: $BALANCER_HTTPS_PORT"
|
|
13
|
+
echo "Certificado SSL: $SSL_CERT_PATH"
|
|
14
|
+
echo "Llave privada: $SSL_KEY_PATH"
|
|
15
|
+
echo "Archivo de configuración de servidores: $CONFIG_FILE_PATH"
|
|
16
|
+
|
|
17
|
+
if [ ! -z "$BALANCER_HTTP_PORT" ]; then
|
|
18
|
+
echo "Puerto HTTP: $BALANCER_HTTP_PORT"
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Iniciar Kukuy
|
|
22
|
+
node kukuy.js
|
|
23
|
+
|
|
24
|
+
echo "Kukuy detenido."
|
package/start-ssl.sh
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Script para iniciar Kukuy en modo SSL
|
|
4
|
+
echo "Iniciando Kukuy en modo SSL..."
|
|
5
|
+
|
|
6
|
+
# Variables de entorno para SSL
|
|
7
|
+
export SSL_CERT_PATH="certs/auto/certificate.crt"
|
|
8
|
+
export SSL_KEY_PATH="certs/auto/private.key"
|
|
9
|
+
export BALANCER_HTTPS_PORT="${BALANCER_HTTPS_PORT:-8443}"
|
|
10
|
+
export CONFIG_FILE_PATH="./servers_real.json"
|
|
11
|
+
|
|
12
|
+
# Opcional: Mantener también el modo HTTP
|
|
13
|
+
if [ "$ENABLE_HTTP" = "true" ]; then
|
|
14
|
+
export BALANCER_HTTP_PORT="${BALANCER_HTTP_PORT:-8080}"
|
|
15
|
+
echo "Modo HTTP habilitado en puerto: $BALANCER_HTTP_PORT"
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
echo "Puerto HTTPS: $BALANCER_HTTPS_PORT"
|
|
19
|
+
echo "Certificado SSL: $SSL_CERT_PATH"
|
|
20
|
+
echo "Llave privada: $SSL_KEY_PATH"
|
|
21
|
+
echo "Archivo de configuración de servidores: $CONFIG_FILE_PATH"
|
|
22
|
+
|
|
23
|
+
# Iniciar Kukuy
|
|
24
|
+
node kukuy.js
|
|
25
|
+
|
|
26
|
+
echo "Kukuy detenido."
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const { RoundRobinAlgorithm } = require('./src/algorithms/RoundRobinAlgorithm');
|
|
2
|
+
const { IPHashAlgorithm } = require('./src/algorithms/IPHashAlgorithm');
|
|
3
|
+
const { ServerPool } = require('./src/core/ServerPool');
|
|
4
|
+
|
|
5
|
+
// Crear instancias de prueba
|
|
6
|
+
const roundRobin = new RoundRobinAlgorithm();
|
|
7
|
+
const ipHash = new IPHashAlgorithm();
|
|
8
|
+
const serverPool = new ServerPool();
|
|
9
|
+
|
|
10
|
+
// Agregar servidores de prueba
|
|
11
|
+
const servers = [
|
|
12
|
+
{ url: 'http://server1.com', weight: 1, tags: ['api'] },
|
|
13
|
+
{ url: 'http://server2.com', weight: 1, tags: ['api'] },
|
|
14
|
+
{ url: 'http://server3.com', weight: 1, tags: ['web'] }
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
serverPool.addServers(servers);
|
|
18
|
+
|
|
19
|
+
console.log('Prueba de optimización del balanceador Kukuy');
|
|
20
|
+
console.log('============================================');
|
|
21
|
+
|
|
22
|
+
// Probar Round Robin
|
|
23
|
+
console.log('\n1. Prueba de Round Robin:');
|
|
24
|
+
const healthyServers = serverPool.getHealthyServers();
|
|
25
|
+
for (let i = 0; i < 5; i++) {
|
|
26
|
+
const server = roundRobin.selectServer(healthyServers);
|
|
27
|
+
console.log(`Solicitud ${i+1}: Servidor seleccionado - ${server.url}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Probar IP Hash
|
|
31
|
+
console.log('\n2. Prueba de IP Hash:');
|
|
32
|
+
const requestContext1 = { req: { headers: { 'x-forwarded-for': '192.168.1.10' } } };
|
|
33
|
+
const requestContext2 = { req: { headers: { 'x-forwarded-for': '192.168.1.11' } } };
|
|
34
|
+
|
|
35
|
+
const server1 = ipHash.selectServer(healthyServers, requestContext1);
|
|
36
|
+
const server2 = ipHash.selectServer(healthyServers, requestContext1); // Misma IP
|
|
37
|
+
const server3 = ipHash.selectServer(healthyServers, requestContext2); // Distinta IP
|
|
38
|
+
|
|
39
|
+
console.log(`IP 192.168.1.10 primera vez: ${server1.url}`);
|
|
40
|
+
console.log(`IP 192.168.1.10 segunda vez: ${server2.url}`);
|
|
41
|
+
console.log(`IP 192.168.1.11 primera vez: ${server3.url}`);
|
|
42
|
+
console.log(`Misma IP usa mismo servidor: ${server1.id === server2.id ? 'Sí' : 'No'}`);
|
|
43
|
+
|
|
44
|
+
// Probar eficiencia de acceso a servidores
|
|
45
|
+
console.log('\n3. Prueba de eficiencia de acceso a servidores:');
|
|
46
|
+
const startTime = process.hrtime.bigint();
|
|
47
|
+
const taggedServers = serverPool.getServersByTag('api');
|
|
48
|
+
const endTime = process.hrtime.bigint();
|
|
49
|
+
const duration = Number(endTime - startTime) / 1000000; // Convertir a milisegundos
|
|
50
|
+
|
|
51
|
+
console.log(`Servidores con tag 'api': ${taggedServers.length}`);
|
|
52
|
+
console.log(`Tiempo de acceso: ${duration} ms`);
|
|
53
|
+
|
|
54
|
+
console.log('\n¡Todas las pruebas básicas pasaron!');
|