Skip to main content

Servizi Stateful (Decoder + VPN)

Questa guida affronta la sfida di scalare servizi che mantengono connessioni TCP persistenti, come il Decoder GPS con OpenVPN.

Il Problema

I dispositivi GPS stabiliscono connessioni TCP long-lived con il decoder. Questo crea diverse sfide:

  1. Connessioni persistenti: Un GPS mantiene la stessa connessione per ore/giorni
  2. Stato della sessione: Il decoder traccia quale device è connesso su quale socket
  3. 1NCE VPN Limit: Solo 1 tunnel VPN attivo per account 1NCE
                    ❓ A quale istanza si connette il GPS?

┌─────────────────────────────┼──────────────────────────────────┐
│ Nomad Cluster │ │
│ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Decoder + │ │ Decoder + │ │ Decoder + │ │
│ │ OpenVPN Client │ │ OpenVPN Client │ │ OpenVPN Client │ │
│ │ Instance 1 │ │ Instance 2 │ │ Instance 3 │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ ❌ │ ❌ │ ❌ │
│ └──────────────────┴──────────────────┘ │
│ │ │
│ Solo 1 può connettersi! │
└───────────────────────────────────────────────────────────────┘

Differenza Stateless vs Stateful

TipoEsempioScalabilità
StatelessAuth, Positions, Geofences✅ Facile - Load Balancer standard
StatefulDecoder, WebSocket⚠️ Richiede sticky sessions o architettura dedicata

Architettura Consigliata: Single Entry Point

Il decoder rimane su una VM dedicata (non in Nomad), mentre tutti gli altri servizi scalano liberamente:

┌───────────────────────────────────────────────────────────────────┐
│ │
│ GPS Devices │
│ (100.64.x.x via 1NCE) │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 1NCE VPN │ ◄── Solo 1 tunnel possibile │
│ │ Tunnel │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ VM Dedicata (Compute Engine) │ │
│ │ n1-standard-4 (4 vCPU, 15GB RAM) │ │
│ │ │ │
│ │ ┌────────────┐ ┌────────────────┐ │ │
│ │ │ OpenVPN │──│ Decoder │ │ │
│ │ │ Client │ │ (Traccar) │ │ │
│ │ └────────────┘ └───────┬────────┘ │ │
│ │ │ │ │
│ │ Redis XADD positions │ │
│ └───────────────────────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Memorystore (Redis) │ │
│ │ (Gestito da GCP) │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────┼────────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ WebSocket │ │ Positions │ │ DB Persist │ │
│ │ x3 istanze │ │ x3 istanze │ │ x2 istanze │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │
│ Nomad Cluster (tutti scalabili) │
└───────────────────────────────────────────────────────────────────┘

Perché Questa Architettura Funziona

  1. Decoder non serve scalare orizzontalmente:

    • Già ottimizzato per 2000+ connessioni
    • CPU-bound solo per parsing (basso overhead)
    • Scala verticalmente (più CPU/RAM sulla VM)
  2. Disaccoppiamento via Redis:

    • Decoder pubblica su Redis Stream
    • Tutti gli altri servizi consumano da Redis
    • Redis è il "bus" centrale che abilita scaling
  3. Compatibilità 1NCE VPN:

    • Solo 1 VM ha il client VPN
    • Rispetta il limite di 1 connessione

Implementazione

VM Decoder (Compute Engine)

# Crea VM dedicata per decoder
gcloud compute instances create decoder-vm \
--project=visla-gps-prod \
--zone=europe-west1-b \
--machine-type=n1-standard-4 \
--image-family=cos-stable \
--image-project=cos-cloud \
--boot-disk-size=50GB \
--boot-disk-type=pd-ssd \
--tags=decoder

# Nessuna porta pubblica necessaria (VPN!)
# Solo SSH per amministrazione
gcloud compute firewall-rules create allow-ssh-decoder \
--allow tcp:22 \
--source-ranges=<YOUR_IP>/32 \
--target-tags=decoder

Docker Compose (sulla VM Decoder)

version: '3.8'

services:
openvpn-client:
image: dperson/openvpn-client
container_name: openvpn-client
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun
volumes:
- ./openvpn-config:/vpn:ro
environment:
FIREWALL: ''
ROUTE: '100.64.0.0/10'
restart: unless-stopped
healthcheck:
test: ["CMD", "ping", "-c", "1", "10.66.5.1"]
interval: 30s
timeout: 10s
retries: 3

decoder:
image: gcr.io/visla-gps/decoder:latest
container_name: decoder
depends_on:
- openvpn-client
# Condivide la rete con OpenVPN per accedere ai GPS
network_mode: "service:openvpn-client"
environment:
# Redis gestito (Memorystore)
REDIS_URL: redis://10.0.0.5:6379
# Performance tuning
JAVA_OPTS: "-Xmx8g -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
NETTY_WORKER_THREADS: "16"
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "5"

Connessione a Redis Gestito

La VM decoder si connette a Memorystore tramite Private IP:

# Ottieni IP Memorystore
gcloud redis instances describe visla-redis \
--region=europe-west1 \
--format="get(host)"

# Output: 10.0.0.5

Configura la VM per accedere alla VPC:

# La VM deve essere nella stessa VPC di Memorystore
gcloud compute instances create decoder-vm \
--network=visla-vpc \
--subnet=visla-subnet-europe \
# ... altri parametri

Scaling del Decoder

Scaling Verticale

Il decoder scala verticalmente. Ecco le raccomandazioni:

GPS DevicesMachine TypevCPURAMCosto/mese
1-500n1-standard-227.5 GB~€50
500-1500n1-standard-4415 GB~€100
1500-3000n1-standard-8830 GB~€200
3000+n1-highmem-8852 GB~€280

Tuning JVM

# docker-compose.yml
decoder:
environment:
JAVA_OPTS: >
-Xmx${HEAP_SIZE:-8g}
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+ParallelRefProcEnabled
-XX:+UseStringDeduplication
NETTY_WORKER_THREADS: "${WORKER_THREADS:-16}"

Monitoring Risorse

# SSH nella VM
gcloud compute ssh decoder-vm --zone=europe-west1-b

# Monitor in tempo reale
docker stats decoder

# Connessioni TCP attive
docker exec decoder netstat -an | grep ESTABLISHED | wc -l

Alternative: Senza VPN 1NCE

Se non usi 1NCE VPN o vuoi scalare orizzontalmente il decoder, ecco le alternative:

Opzione A: Load Balancer TCP con Sticky Sessions

GPS Devices ──► TCP Load Balancer ──► Decoder Instances
(Client IP Affinity)
# Terraform - GCP TCP Load Balancer
resource "google_compute_backend_service" "decoder" {
name = "decoder-backend"
protocol = "TCP"
port_name = "decoder"

# Sticky sessions: stesso GPS → stessa istanza
session_affinity = "CLIENT_IP"

# Timeout lungo per connessioni persistenti
timeout_sec = 86400 # 24 ore

backend {
group = google_compute_instance_group_manager.decoder.instance_group
}

health_checks = [google_compute_health_check.decoder.id]
}

resource "google_compute_health_check" "decoder" {
name = "decoder-health"

tcp_health_check {
port = 5052
}

check_interval_sec = 10
timeout_sec = 5
healthy_threshold = 2
unhealthy_threshold = 3
}

Nomad Job per Decoder (senza VPN):

job "decoder" {
datacenters = ["dc1"]
type = "service"

group "decoder" {
count = 3 # Multiple istanze

network {
port "decoder" {
static = 5052 # Porta fissa per load balancer
}
}

service {
name = "decoder"
port = "decoder"

check {
type = "tcp"
interval = "10s"
timeout = "2s"
}
}

task "decoder" {
driver = "docker"

config {
image = "gcr.io/visla-gps/decoder:latest"
ports = ["decoder"]
}

env {
REDIS_URL = "redis://{{ key "config/redis/host" }}:6379"
JAVA_OPTS = "-Xmx4g -XX:+UseG1GC"
}

resources {
cpu = 2000 # 2 vCPU
memory = 4096 # 4 GB
}
}
}
}

Opzione B: Sharding per Range IP

Partiziona i GPS su decoder specifici:

GPS IP 100.64.0.0/12   → decoder-1.visla.com
GPS IP 100.64.16.0/12 → decoder-2.visla.com
GPS IP 100.64.32.0/12 → decoder-3.visla.com

[!WARNING] Questa opzione richiede configurazione manuale dei GPS e non è consigliata per la maggior parte dei casi.


Failover e Alta Disponibilità

VM Singola: Rischi

Con una singola VM decoder, un failure causa downtime. Mitigazioni:

  1. Preemptible VM Monitoring:

    # Alert se la VM va giù
    gcloud monitoring policies create \
    --condition="compute.googleapis.com/instance/uptime < 1" \
    --notification-channels=<CHANNEL_ID>
  2. Auto-Restart:

    # Restart automatico se la VM crasha
    gcloud compute instances set-scheduling decoder-vm \
    --automatic-restart
  3. Instance Template + Health Check:

    # Managed Instance Group con auto-healing
    gcloud compute instance-groups managed create decoder-mig \
    --template=decoder-template \
    --size=1 \
    --health-check=decoder-health \
    --initial-delay=300

Cosa Succede Durante Failover

  1. GPS si disconnette → Connessione TCP chiusa
  2. GPS riprova → Nuova connessione alla nuova istanza
  3. Stato ripristinato → GPS invia primo pacchetto, decoder riconosce device

[!TIP] I GPS sono progettati per riconnettersi automaticamente. Un failover di 1-2 minuti è generalmente accettabile.


Costi Totali Architettura

ComponenteConfigurazioneCosto/Mese
VM Decodern1-standard-4~€100
Cloud SQLdb-custom-2-8192, HA~€130
Memorystore2GB Standard HA~€90
Nomad Cluster (3 nodi)n1-standard-2 × 3~€150
Load BalancerPer altri servizi~€20
Network Egress~10GB/mese~€1
Totale~€491

Checklist Implementazione

  • Creare VM dedicata per decoder
  • Configurare OpenVPN client con credenziali 1NCE
  • Creare Memorystore Redis
  • Configurare peering VPC per Redis
  • Deploy decoder con Docker Compose
  • Verificare connessione VPN (docker logs openvpn-client)
  • Testare connessione GPS
  • Configurare alerting su VM
  • Deploy altri servizi su Nomad
  • Configurare consumatori Redis (positions, db-persister, websocket)