Skip to main content

Scaling Decoder Senza VPN

Questa guida spiega come scalare orizzontalmente il decoder GPS senza utilizzare OpenVPN, usando DNS e TCP Load Balancer.

Quando Usare Questa Architettura

ScenarioArchitettura Consigliata
Usi 1NCE VPN e vuoi sicurezza massimaServizi Stateful - VM dedicata
Vuoi scalare orizzontalmente il decoderQuesta guida - LB + Nomad
Hai GPS con SIM diverse (non 1NCE)Questa guida - LB + Nomad
Hai 5000+ GPS e serve alta disponibilitàQuesta guida - LB + Nomad

Architettura

┌─────────────────────────────────────────────────────────────────────┐
│ │
│ GPS Devices │
│ (Qualsiasi connettività: 1NCE, Vodafone, Tim, etc.) │
│ │ │
│ │ DNS: decoder.visla.com → 34.xx.xx.xx │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ TCP Load Balancer (Layer 4) │ │
│ │ IP Statico: 34.xx.xx.xx │ │
│ │ Porta: 5052 │ │
│ │ │ │
│ │ ⚙️ session_affinity: CLIENT_IP │ │
│ │ ⚙️ timeout: 86400s (24 ore) │ │
│ └────────────────────────┬─────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────┼─────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Decoder │ │ Decoder │ │ Decoder │ │
│ │ Instance 1 │ │ Instance 2 │ │ Instance 3 │ │
│ │ Nomad │ │ Nomad │ │ Nomad │ │
│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │
│ │ │ │ │
│ └─────────────────┼─────────────────┘ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Memorystore │ │
│ │ (Redis) │ │
│ └────────────────┘ │
│ │
│ Nomad Cluster │
└─────────────────────────────────────────────────────────────────────┘

Come Funziona CLIENT_IP Affinity

Il problema con le connessioni TCP persistenti è che un GPS deve rimanere connesso alla stessa istanza per tutta la durata della sessione.

Soluzione: Hash dell'IP Client

GPS-A (IP: 80.1.2.3)  ──► hash("80.1.2.3") % 3 = 0 ──► Instance 1
GPS-B (IP: 80.1.2.4) ──► hash("80.1.2.4") % 3 = 1 ──► Instance 2
GPS-C (IP: 80.1.2.5) ──► hash("80.1.2.5") % 3 = 2 ──► Instance 3
GPS-D (IP: 80.1.2.6) ──► hash("80.1.2.6") % 3 = 0 ──► Instance 1

Risultato: Lo stesso GPS va sempre alla stessa istanza, finché il numero di istanze non cambia.

Cosa Succede al Failover

Situazione normale (3 istanze):
┌─────────┬────────────┐
│ GPS │ Istanza │
├─────────┼────────────┤
│ GPS-A │ Instance 1 │
│ GPS-B │ Instance 2 │
│ GPS-C │ Instance 3 │
└─────────┴────────────┘

Instance 2 muore → Load Balancer ricalcola:
┌─────────┬────────────┬─────────────┐
│ GPS │ Prima │ Dopo │
├─────────┼────────────┼─────────────┤
│ GPS-A │ Instance 1 │ Instance 1 │ ✅ Invariato
│ GPS-B │ Instance 2 │ Instance 1 │ 🔄 Riconnesso
│ GPS-C │ Instance 3 │ Instance 3 │ ✅ Invariato
└─────────┴────────────┴─────────────┘

[!TIP] I GPS sono progettati per riconnettersi automaticamente in caso di disconnessione. Il failover causa solo pochi secondi di interruzione.


Implementazione

1. Creare IP Statico

# Crea IP statico per il Load Balancer
gcloud compute addresses create decoder-lb-ip \
--region=europe-west1

# Visualizza l'IP allocato
gcloud compute addresses describe decoder-lb-ip \
--region=europe-west1 \
--format="get(address)"

# Output esempio: 34.76.123.45

2. Configurare DNS

Aggiungi un record A nel tuo DNS provider:

decoder.visla.com    A    34.76.123.45    TTL: 300

3. Creare Health Check

gcloud compute health-checks create tcp decoder-health \
--port=5052 \
--check-interval=10s \
--timeout=5s \
--healthy-threshold=2 \
--unhealthy-threshold=3

4. Creare Backend Service

gcloud compute backend-services create decoder-backend \
--protocol=TCP \
--health-checks=decoder-health \
--session-affinity=CLIENT_IP \
--timeout=86400s \
--global

[!IMPORTANT] --timeout=86400s (24 ore) è fondamentale per connessioni TCP long-lived dei GPS.

5. Creare Instance Group

# Crea Instance Group (unmanaged per Nomad)
gcloud compute instance-groups unmanaged create decoder-group \
--zone=europe-west1-b

# Aggiungi i nodi Nomad al gruppo
gcloud compute instance-groups unmanaged add-instances decoder-group \
--zone=europe-west1-b \
--instances=nomad-client-1,nomad-client-2,nomad-client-3

# Configura named port
gcloud compute instance-groups unmanaged set-named-ports decoder-group \
--zone=europe-west1-b \
--named-ports=decoder:5052

# Aggiungi al backend service
gcloud compute backend-services add-backend decoder-backend \
--instance-group=decoder-group \
--instance-group-zone=europe-west1-b \
--global

6. Creare Forwarding Rule

gcloud compute forwarding-rules create decoder-lb \
--address=decoder-lb-ip \
--ports=5052 \
--backend-service=decoder-backend \
--global

7. Configurare Firewall

# Permetti traffico dal Load Balancer ai nodi
gcloud compute firewall-rules create allow-decoder-lb \
--allow=tcp:5052 \
--source-ranges=130.211.0.0/22,35.191.0.0/16 \
--target-tags=nomad-client

# Permetti health checks
gcloud compute firewall-rules create allow-decoder-health \
--allow=tcp:5052 \
--source-ranges=35.191.0.0/16,130.211.0.0/22 \
--target-tags=nomad-client

Nomad Job

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

group "decoder" {
count = 3 # Numero di istanze

# Spread su nodi diversi per HA
constraint {
operator = "distinct_hosts"
value = "true"
}

network {
port "decoder" {
static = 5052 # Porta fissa richiesta per TCP LB
}
}

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

tags = ["tcp", "gps"]

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

task "decoder" {
driver = "docker"

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

template {
data = <<EOF
REDIS_URL=redis://{{ key "config/redis/host" }}:6379
JAVA_OPTS=-Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200
NETTY_WORKER_THREADS=8
EOF
destination = "secrets/.env"
env = true
}

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

# Graceful shutdown per connessioni attive
kill_timeout = "30s"
}

# Rolling update: 1 alla volta
update {
max_parallel = 1
min_healthy_time = "30s"
healthy_deadline = "5m"
auto_revert = true
}

# Restart policy
restart {
attempts = 3
interval = "5m"
delay = "15s"
mode = "delay"
}
}
}

Configurazione GPS

Opzione A: DNS (Consigliata)

SERVER,decoder.visla.com,5052#
APN,iot.1nce.net#

Opzione B: IP Diretto

SERVER,34.76.123.45,5052#
APN,iot.1nce.net#

[!TIP] Usa il DNS per flessibilità. Se l'IP cambia, basta aggiornare il record DNS senza riconfigurare i GPS.


Sicurezza

Senza VPN, la porta 5052 è esposta su Internet. Ecco come proteggerla:

1. Firewall - Solo IP noti

Se i GPS usano SIM 1NCE, limita agli IP 1NCE:

gcloud compute firewall-rules create allow-decoder-1nce \
--allow=tcp:5052 \
--source-ranges=185.48.172.0/22,185.48.176.0/22 \
--target-tags=nomad-client \
--priority=1000

# Nega tutto il resto
gcloud compute firewall-rules create deny-decoder-all \
--action=DENY \
--rules=tcp:5052 \
--source-ranges=0.0.0.0/0 \
--target-tags=nomad-client \
--priority=2000

2. Autenticazione IMEI

Il decoder già implementa autenticazione basata su IMEI:

  1. GPS invia primo pacchetto con IMEI
  2. Decoder verifica IMEI nel database
  3. IMEI non valido → Connessione chiusa
// Nel decoder (già implementato)
DeviceSession getDeviceSession(Channel channel, Object msg) {
String imei = parseImei(msg);
Device device = deviceManager.lookup(imei);
if (device == null) {
channel.close(); // Connessione rifiutata
return null;
}
return createSession(channel, device);
}

3. Rate Limiting

Aggiungi Cloud Armor per protezione DDoS:

gcloud compute security-policies create decoder-policy

gcloud compute security-policies rules create 1000 \
--security-policy=decoder-policy \
--action=throttle \
--rate-limit-threshold-count=100 \
--rate-limit-threshold-interval-sec=60 \
--conform-action=allow \
--exceed-action=deny-429

gcloud compute backend-services update decoder-backend \
--security-policy=decoder-policy \
--global

Monitoring

Metriche Chiave

# Connessioni attive per istanza
docker exec decoder netstat -an | grep :5052 | grep ESTABLISHED | wc -l

# Memory usage
docker stats decoder --no-stream

# Logs
docker logs decoder --tail 100 -f

Grafana Dashboard

Metriche da monitorare:

MetricaDescrizioneAlert
decoder_connections_activeConnessioni TCP attive> 2000 per istanza
decoder_messages_per_secondThroughput< 10 (se atteso > 100)
decoder_errors_totalErrori parsing> 10/min
jvm_memory_used_bytesMemoria JVM> 80% heap

Scaling

Scaling Orizzontale

# Scala a 5 istanze
nomad job scale decoder decoder 5

# Verifica
nomad job status decoder

Scaling Automatico (HPA)

# Nel job Nomad
scaling {
enabled = true
min = 2
max = 10

policy {
evaluation_interval = "30s"
cooldown = "5m"

check "cpu" {
source = "nomad-apm"
query = "avg_cpu"

strategy "target-value" {
target = 70
}
}
}
}

Confronto Architetture

AspettoVPN + VM SingolaDNS + Load Balancer
Scaling⬆️ Solo verticale↔️ Orizzontale
HA⚠️ Single point of failure✅ Multi-istanza
Sicurezza✅ Tunnel cifrato⚠️ Firewall + IMEI auth
Complessità⚠️ Config VPN✅ Standard GCP
Costo~€100/mese~€120/mese (+ LB €20)
Comunicazione bidirezionale✅ Sì❌ Solo GPS → Server
Max GPS consigliati~2000~10000+

Checklist Implementazione

  • Creare IP statico GCP
  • Configurare record DNS
  • Creare Health Check TCP
  • Creare Backend Service con CLIENT_IP affinity
  • Creare Instance Group con nodi Nomad
  • Creare Forwarding Rule
  • Configurare Firewall rules
  • Deploy Nomad job decoder
  • Testare connessione GPS
  • Configurare monitoring/alerting
  • Testare failover (kill un'istanza)