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:
- Connessioni persistenti: Un GPS mantiene la stessa connessione per ore/giorni
- Stato della sessione: Il decoder traccia quale device è connesso su quale socket
- 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
| Tipo | Esempio | Scalabilità |
|---|---|---|
| Stateless | Auth, Positions, Geofences | ✅ Facile - Load Balancer standard |
| Stateful | Decoder, 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
-
Decoder non serve scalare orizzontalmente:
- Già ottimizzato per 2000+ connessioni
- CPU-bound solo per parsing (basso overhead)
- Scala verticalmente (più CPU/RAM sulla VM)
-
Disaccoppiamento via Redis:
- Decoder pubblica su Redis Stream
- Tutti gli altri servizi consumano da Redis
- Redis è il "bus" centrale che abilita scaling
-
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 Devices | Machine Type | vCPU | RAM | Costo/mese |
|---|---|---|---|---|
| 1-500 | n1-standard-2 | 2 | 7.5 GB | ~€50 |
| 500-1500 | n1-standard-4 | 4 | 15 GB | ~€100 |
| 1500-3000 | n1-standard-8 | 8 | 30 GB | ~€200 |
| 3000+ | n1-highmem-8 | 8 | 52 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:
-
Preemptible VM Monitoring:
# Alert se la VM va giù
gcloud monitoring policies create \
--condition="compute.googleapis.com/instance/uptime < 1" \
--notification-channels=<CHANNEL_ID> -
Auto-Restart:
# Restart automatico se la VM crasha
gcloud compute instances set-scheduling decoder-vm \
--automatic-restart -
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
- GPS si disconnette → Connessione TCP chiusa
- GPS riprova → Nuova connessione alla nuova istanza
- 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
| Componente | Configurazione | Costo/Mese |
|---|---|---|
| VM Decoder | n1-standard-4 | ~€100 |
| Cloud SQL | db-custom-2-8192, HA | ~€130 |
| Memorystore | 2GB Standard HA | ~€90 |
| Nomad Cluster (3 nodi) | n1-standard-2 × 3 | ~€150 |
| Load Balancer | Per 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)