Connessioni Multi-Istanza
Questa guida spiega come multiple istanze di servizi Nomad si connettono a un singolo database Cloud SQL e a Redis.
Architettura Overviewβ
Quando scali i tuoi microservizi su Nomad con multiple istanze, tutti si connettono agli stessi servizi gestiti:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Nomad Cluster β
β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β Auth Service β β Auth Service β β Auth Service β β
β β Instance 1 β β Instance 2 β β Instance 3 β β
β ββββββββ¬ββββββββ ββββββββ¬ββββββββ ββββββββ¬ββββββββ β
β β β β β
β β max_conn=10 β max_conn=10 β max_conn=10 β
β βββββββββββββββββββΌββββββββββββββββββ β
β β β
βββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββ΄ββββββββββββββ
βΌ βΌ
βββββββββββββββββββββββββββ ββββββββββββββββββββββββ
β Cloud SQL β β Memorystore β
β PostgreSQL β β (Redis) β
β max_connections=200 β β β
βββββββββββββββββββββββββββ ββββββββββββββββββββββββ
PostgreSQL: Connection Poolingβ
PerchΓ© il Connection Pooling Γ¨ Importanteβ
PostgreSQL ha un limite di connessioni simultanee. Senza pooling:
- 3 istanze Γ 50 connessioni = 150 connessioni
- Con auto-scaling a 10 istanze = 500 connessioni β (troppo!)
Configurazione Pool per Servizioβ
Ogni servizio deve configurare il proprio pool di connessioni:
// Node.js con pg-pool
import { Pool } from 'pg';
const pool = new Pool({
host: process.env.DATABASE_HOST, // IP privato Cloud SQL
port: 5432,
database: 'visla_production',
user: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
// Configurazione pool PER ISTANZA
max: 10, // Max 10 connessioni per istanza
idleTimeoutMillis: 30000, // Chiudi connessioni inattive dopo 30s
connectionTimeoutMillis: 5000,
});
// Uso nel codice
const result = await pool.query('SELECT * FROM devices WHERE user_id = $1', [userId]);
// Go con pgxpool
import "github.com/jackc/pgx/v5/pgxpool"
config, _ := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
config.MaxConns = 10 // Max connessioni per istanza
config.MinConns = 2 // Mantieni almeno 2 connessioni
config.MaxConnLifetime = time.Hour
config.MaxConnIdleTime = 30 * time.Minute
pool, _ := pgxpool.NewWithConfig(context.Background(), config)
Calcolo Connessioni Totaliβ
| Servizio | Istanze | Conn/Istanza | Totale |
|---|---|---|---|
| Auth Service | 3 | 10 | 30 |
| Positions | 5 | 5 | 25 |
| Geofences | 2 | 5 | 10 |
| Devices | 3 | 10 | 30 |
| Sharing | 2 | 5 | 10 |
| Billing | 2 | 5 | 10 |
| DB Persister | 3 | 15 | 45 |
| Totale | 160 |
[!IMPORTANT] Configura Cloud SQL con almeno 200 max_connections (
max_connections=200nei database flags) per avere margine.
PgBouncer: Connection Pooler Avanzatoβ
Per scenari ad alto throughput, usa PgBouncer come proxy:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Nomad Cluster β
β β
β βββββββββββββ βββββββββββββ βββββββββββββ βββββββββββββ β
β β Service 1 β β Service 2 β β Service 3 β β Service N β β
β βββββββ¬ββββββ βββββββ¬ββββββ βββββββ¬ββββββ βββββββ¬ββββββ β
β β β β β β
β βββββββββββββββ΄βββββββ¬βββββββ΄ββββββββββββββ β
β βΌ β
β βββββββββββββββββββ β
β β PgBouncer β β
β β (1-2 istanze) β β
β β 100 β 20 conn β βββ Multiplexing β
β ββββββββββ¬βββββββββ β
βββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββ
β Cloud SQL β
β 20 connessioni β
βββββββββββββββββββββββββββ
Job Nomad per PgBouncerβ
job "pgbouncer" {
datacenters = ["dc1"]
type = "service"
group "pgbouncer" {
count = 2 # 2 istanze per HA
network {
port "pgbouncer" {
static = 6432
}
}
service {
name = "pgbouncer"
port = "pgbouncer"
check {
type = "tcp"
interval = "10s"
timeout = "2s"
}
}
task "pgbouncer" {
driver = "docker"
config {
image = "edoburu/pgbouncer:1.21.0"
ports = ["pgbouncer"]
}
template {
data = <<EOF
[databases]
visla_production = host=${CLOUD_SQL_IP} port=5432 dbname=visla_production
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
pool_mode = transaction
max_client_conn = 500
default_pool_size = 20
min_pool_size = 5
reserve_pool_size = 5
EOF
destination = "local/pgbouncer.ini"
}
template {
data = <<EOF
"visla_app" "{{ with secret "database/creds/app" }}{{ .Data.password }}{{ end }}"
EOF
destination = "secrets/userlist.txt"
}
resources {
cpu = 256
memory = 128
}
}
}
}
Pool Modesβ
| Mode | Descrizione | Use Case |
|---|---|---|
session | Connessione per sessione | App con prepared statements persistenti |
transaction | Connessione per transazione | Consigliato per microservizi |
statement | Connessione per statement | Solo query semplici |
Redis: Connessione Multi-Istanzaβ
Redis Γ¨ progettato nativamente per connessioni multiple. Non ha le stesse limitazioni di PostgreSQL.
Connessione Baseβ
// Node.js con ioredis
import Redis from 'ioredis';
const redis = new Redis({
host: process.env.REDIS_HOST, // IP privato Memorystore
port: 6379,
password: process.env.REDIS_PASSWORD, // Se AUTH abilitato
// Retry automatico
retryDelayOnFailover: 100,
maxRetriesPerRequest: 3,
});
// Go con go-redis
import "github.com/redis/go-redis/v9"
rdb := redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_HOST") + ":6379",
Password: os.Getenv("REDIS_PASSWORD"),
DB: 0,
PoolSize: 10, // Connessioni per istanza
})
Pub/Sub tra Istanzeβ
Il caso d'uso piΓΉ comune: broadcast di eventi tra istanze.
Device invia posizione
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Nomad Cluster β
β β
β User A βββΊ ββββββββββββββββββ β
β β WebSocket ββββ β
β β Instance 1 β β β
β ββββββββββββββββββ β β
β β redis.publish() β
β User B βββΊ ββββββββββββββββββ β β β
β β WebSocket ββββΌβββββββββΌβββββββ β
β β Instance 2 β β β β β
β ββββββββββββββββββ β βΌ β β
β β ββββββββββ β β
β User C βββΊ ββββββββββββββββββ β β Redis β β β
β β WebSocket ββββ βPub/Sub ββββ β
β β Instance 3 ββββββββ β β
β ββββββββββββββββββ ββββββββββ β
β β β β
β β Tutti ricevono β β
β ββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Implementazione Pub/Subβ
// Publisher (es. Positions Service)
import Redis from 'ioredis';
const publisher = new Redis(process.env.REDIS_URL);
async function publishPosition(deviceId, position) {
await publisher.publish('device:positions', JSON.stringify({
deviceId,
lat: position.lat,
lng: position.lng,
timestamp: Date.now()
}));
}
// Subscriber (es. WebSocket Service - ogni istanza)
import Redis from 'ioredis';
const subscriber = new Redis(process.env.REDIS_URL);
// TUTTE le istanze WebSocket ricevono questo messaggio
subscriber.subscribe('device:positions');
subscriber.on('message', (channel, message) => {
const data = JSON.parse(message);
// Invia solo ai client connessi a QUESTA istanza
// che sono autorizzati a vedere questo device
broadcastToAuthorizedClients(data);
});
Variabili d'Ambiente nei Job Nomadβ
Template per Serviziβ
job "auth-service" {
# ...
group "auth" {
count = 3 # 3 istanze
task "auth" {
driver = "docker"
config {
image = "gcr.io/visla-gps/auth-service:latest"
}
# Variabili dal Vault o Consul
template {
data = <<EOF
DATABASE_URL=postgresql://visla_app:{{ with secret "database/creds/app" }}{{ .Data.password }}{{ end }}@{{ key "config/database/host" }}:5432/visla_production?sslmode=require
REDIS_URL=redis://:{{ with secret "redis/creds" }}{{ .Data.password }}{{ end }}@{{ key "config/redis/host" }}:6379
EOF
destination = "secrets/.env"
env = true
}
resources {
cpu = 256
memory = 512
}
}
}
}
Best Practicesβ
1. Graceful Shutdownβ
Quando un'istanza viene terminata, chiudi le connessioni correttamente:
// Node.js
process.on('SIGTERM', async () => {
console.log('Shutting down gracefully...');
// Smetti di accettare nuove richieste
server.close();
// Chiudi pool database
await pool.end();
// Chiudi Redis
await redis.quit();
process.exit(0);
});
2. Health Checksβ
Includi check di connettivitΓ nei health endpoints:
app.get('/health', async (req, res) => {
try {
// Check database
await pool.query('SELECT 1');
// Check Redis
await redis.ping();
res.json({ status: 'healthy' });
} catch (error) {
res.status(503).json({
status: 'unhealthy',
error: error.message
});
}
});
3. Connection Retryβ
Configura retry automatico per connessioni temporaneamente non disponibili:
// pg-pool retry
const pool = new Pool({
// ...
connectionTimeoutMillis: 5000,
retryDelay: 1000,
});
pool.on('error', (err) => {
console.error('Unexpected database error', err);
// Non terminare il processo, il pool riproverΓ
});
Troubleshootingβ
Errore: "too many connections"β
Causa: Troppe istanze o pool size troppo alto.
Soluzione:
- Riduci
maxnel pool di ogni servizio - Aumenta
max_connectionsin Cloud SQL - Implementa PgBouncer
Errore: "connection refused" intermittenteβ
Causa: Cloud SQL failover o manutenzione.
Soluzione:
- Abilita retry automatico nel pool
- Usa connection timeout appropriato
- Implementa circuit breaker pattern
Redis: "READONLY" errorβ
Causa: Failover Redis in corso, stai scrivendo sulla replica.
Soluzione:
const redis = new Redis({
// ...
retryDelayOnFailover: 100,
enableReadyCheck: true,
maxRetriesPerRequest: 3
});