Skip to main content

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

ServizioIstanzeConn/IstanzaTotale
Auth Service31030
Positions5525
Geofences2510
Devices31030
Sharing2510
Billing2510
DB Persister31545
Totale160

[!IMPORTANT] Configura Cloud SQL con almeno 200 max_connections (max_connections=200 nei 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

ModeDescrizioneUse Case
sessionConnessione per sessioneApp con prepared statements persistenti
transactionConnessione per transazioneConsigliato per microservizi
statementConnessione per statementSolo 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:

  1. Riduci max nel pool di ogni servizio
  2. Aumenta max_connections in Cloud SQL
  3. Implementa PgBouncer

Errore: "connection refused" intermittente

Causa: Cloud SQL failover o manutenzione.

Soluzione:

  1. Abilita retry automatico nel pool
  2. Usa connection timeout appropriato
  3. 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
});