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)