Files
workspace/docs/multi-region-guide.md
2026-04-15 11:26:46 +02:00

11 KiB

Guida Multi-Region — adiuvAI API

Stato attuale: FastAPI containerizzata (docker-compose) su singolo VPS Hetzner in Europa. Obiettivo: ridurre la latenza per utenti fuori dall'Europa.


Fase 1 — Ottimizzare Cloudflare (già in uso)

1.1 Argo Smart Routing

  • Dashboard → Traffic → Argo — attivalo (~$5/mese + $0.10/GB)
  • Usa i backbone privati Cloudflare invece dell'internet pubblico
  • Riduce la latenza del 30-40% senza toccare nulla lato server
  • Singolo cambiamento con miglior rapporto costo/beneficio

1.2 SSL/TLS

  • Dashboard → SSL/TLS → Overview → mode Full (Strict) (non "Flexible", causa redirect loop)
  • Abilita TLS 1.3 (meno round-trip nell'handshake)
  • Abilita Early Hints (103) in Speed → Optimization

1.3 Cache Rules

Di default Cloudflare non cachea le risposte API (Content-Type application/json). Per gli endpoint pubblici (es. /api/v1/health):

  • Dashboard → Caching → Cache Rules → crea regola:
    • Match: URI Path starts with /api/v1/health
    • Action: Cache, Edge TTL 30s, Browser TTL 10s
  • Lato codice: aggiungere header Cache-Control: public, s-maxage=30 e CDN-Cache-Control: public, max-age=30 all'health endpoint
  • NON cacheate endpoint autenticati (il JWT rende ogni richiesta unica)

1.4 Response Compression

  • Dashboard → Speed → Optimization → Content Optimization
  • Abilita Brotli (più efficiente di gzip per payload JSON)
  • Le risposte JSON vengono compresse automaticamente al transit

1.5 WebSocket

  • Dashboard → Network → verifica che WebSockets sia ON (default nel piano Free)
  • Il /chat/stream WebSocket viene proxato ma non cacheato
  • Il keepalive di 30s che già avete mantiene la connessione viva attraverso Cloudflare

1.6 Tiered Cache (piano Pro+)

  • Dashboard → Caching → Tiered Cache → attiva Smart Tiered Caching
  • Cloudflare usa data center "upper-tier" come cache intermedia
  • Riduce le hit al tuo origin server

1.7 Timeout

  • Dashboard → Network → WebSocket timeout: aumenta se gli utenti hanno sessioni chat lunghe
  • Proxy Read Timeout: default 100s, sufficiente per le LLM call (il tool loop ha cap 5 iterazioni)

Fase 2 — Secondo nodo in US East

Architettura target

                    ┌─── Cloudflare (Geo Steering) ───┐
                    │                                   │
           utenti EU/Africa                     utenti Americas
                    │                                   │
          ┌────────▼─────────┐              ┌──────────▼─────────┐
          │  VPS EU (attuale)  │              │   VPS US (nuovo)    │
          │  docker-compose    │              │   docker-compose    │
          │  app + PG primary  │              │   app + PG replica  │
          └────────┬──────────┘              └──────────┬──────────┘
                    │                                    │
                    └── PG streaming replication ────────┘
                        (async, read-only replica)

Opzione A: Secondo VPS Hetzner (Ashburn) + Cloudflare Load Balancing

Estensione naturale del setup attuale — minimo cambiamento architetturale.

Step 1 — Provisioning del VPS US

  1. Crea un VPS Hetzner in Ashburn (us-east) (stesse specs del nodo EU)
  2. Setup identico: Docker, Docker Compose, git
  3. Configura un tunnel WireGuard tra EU e US per il traffico DB (mai esporre PG sulla rete pubblica)

Step 2 — PostgreSQL Streaming Replication

Sul PRIMARY (EU):

  1. Creare un utente replication:
    CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD '<strong_password>';
    
  2. Creare un replication slot:
    SELECT pg_create_physical_replication_slot('replica_us_east');
    
  3. Configurare pg_hba.conf per permettere connessioni dal VPS US:
    host replication replicator <US_VPS_WIREGUARD_IP>/32 scram-sha-256
    
  4. Esporre la porta PG solo sull'IP WireGuard nel docker-compose.yml:
    services:
      db:
        ports:
          - "10.0.0.1:5432:5432"  # solo interfaccia WireGuard
    

Sul REPLICA (US):

  1. Base backup dal primary:

    docker run --rm \
      -v postgres_data:/var/lib/postgresql/data \
      pgvector/pgvector:pg16 \
      bash -c "pg_basebackup -h <PRIMARY_WG_IP> -U replicator \
               -D /var/lib/postgresql/data -Fp -Xs -P -R"
    

    Il flag -R crea automaticamente standby.signal e scrive primary_conninfo in postgresql.auto.conf.

  2. Avviare PG in modalita replica (legge standby.signal e si connette al primary)

  3. Verificare:

    • Sul primary: SELECT * FROM pg_stat_replication; (deve mostrare il replica)
    • Sul replica: SELECT pg_is_in_recovery(); (deve restituire t)

Step 3 — Modifiche al codice FastAPI

Modifiche necessarie in app/config/settings.py:

  • Aggiungere DATABASE_READ_URL: str = "" — URL del replica locale per le letture
  • Aggiungere REGION: str = "eu" — identificativo regione per health check e observability

Modifiche in app/db.py:

  • Creare un secondo engine read_engine che usa DATABASE_READ_URL (fallback a DATABASE_URL se vuoto)
  • Esporre un get_read_session() dependency da usare nelle query read-only

Modifiche in app/main.py:

  • L'health endpoint deve restituire region nel payload
  • Aggiungere header Cache-Control / CDN-Cache-Control per il caching all'edge

Nelle route, per le query di sola lettura pesanti (es. ricerca, listing):

  • Usare db: AsyncSession = Depends(get_read_session) invece di get_session
  • Le scritture (auth, billing, update) continuano a usare get_session (→ primary EU)

Step 4 — Docker Compose per il nodo US

Creare un docker-compose.replica.yml (override) che:

  • Sovrascrive le env dell'app con DATABASE_READ_URL verso il DB locale e DATABASE_URL verso il primary EU
  • Imposta REGION=us-east
  • Avvia PG in modalita replica (con primary_conninfo che punta al primary EU via WireGuard)

Il .env sul nodo US:

DATABASE_URL=postgresql+asyncpg://postgres:<pass>@<PRIMARY_WG_IP>:5432/adiuvai
DATABASE_READ_URL=postgresql+asyncpg://postgres:postgres@db:5432/adiuvai
REGION=us-east
PRIMARY_DB_HOST=<PRIMARY_WG_IP>
REPLICATOR_PASSWORD=<strong_password>
# ... resto delle variabili (JWT_SECRET, STRIPE, LLM keys, etc.) identiche al nodo EU

Avvio: docker compose -f docker-compose.yml -f docker-compose.replica.yml up -d

Step 5 — Deploy CI multi-region

Estendere il workflow .gitea/workflows/deploy.yaml con un secondo job deploy-us:

  • Identico a deploy ma con SSH verso il VPS US
  • Usa secrets.SSH_HOST_US, secrets.SSH_USER_US, secrets.SSH_KEY_US
  • Il comando di deploy usa -f docker-compose.yml -f docker-compose.replica.yml
  • NON esegue alembic upgrade head — le migrazioni girano solo sul primary (il replica riceve le DDL via replication)

I due job deploy e deploy-us possono girare in parallelo (entrambi dipendono solo da test).

Step 6 — Cloudflare Geo Steering

  1. Dashboard → Traffic → Load Balancing (piano Pro, ~$5/mese per pool)
  2. Creare due Origin Pools:
    • eu-pool: origin = IP del VPS EU, health check = GET /api/v1/health
    • us-pool: origin = IP del VPS US, health check = GET /api/v1/health
  3. Creare un Load Balancer su api.adiuvai.com:
    • Steering policy: Geo
    • EU/Africa → eu-pool
    • Americas → us-pool
    • Fallback: eu-pool
  4. Impostare health monitor: GET /api/v1/health, interval 60s, timeout 5s
    • Se un nodo va giù, tutto il traffico va al nodo sano (automatic failover)

Opzione B: Fly.io (alternativa più semplice, meno controllo)

Se preferisci evitare la gestione manuale di un secondo VPS:

  1. Crea un fly.toml nella root del progetto API
  2. fly launch — Fly rileva il Dockerfile e deploya
  3. fly regions add iad — aggiunge US East (Ashburn)
  4. Fly gestisce: routing anycast, health checks, TLS, auto-scaling
  5. Il DB resta su Hetzner EU — Fly non risolve il problema del database, ma elimina tutta la gestione infrastrutturale dell'app layer
  6. Costo: ~$5-15/mese per region (dipende dalle risorse)
  7. Contro: meno controllo, vendor lock-in, il DB non ha replica locale

Opzione C: Hetzner Cloud Load Balancer + geo DNS esterno

  • Hetzner offre load balancer nativi, ma sono single-region (non cross-region)
  • Non adatto per geo-routing, utile solo per HA nella stessa region

Fase 3 — Terzo nodo in Asia (futuro)

Stessa procedura della Fase 2:

  1. VPS Hetzner Singapore (o AWS ap-southeast-1)
  2. Secondo PG replica con slot replica_asia
  3. Terzo pool in Cloudflare Load Balancing con geo steering per Asia-Pacific
  4. Terzo job deploy-asia nel CI

Da valutare solo quando il traffico dall'Asia lo giustifica.


Sicurezza della rete tra i nodi

Metodo Pro Contro
WireGuard Semplice, veloce, <1ms overhead, kernel-level Setup manuale per nodo
Hetzner vSwitch Zero config se entrambi su Hetzner Solo stessa region
Tailscale WireGuard gestito, zero config rete Dipendenza esterna
SSH tunnel Nessun software extra Overhead maggiore, meno stabile

Raccomandazione: WireGuard (o Tailscale per semplicita) tra tutti i nodi. Mai esporre PostgreSQL 5432 sull'IP pubblico.


Considerazioni specifiche per adiuvAI

  • L'app e local-first: la maggior parte delle operazioni (tasks, notes, projects) avviene in SQLite locale nell'Electron app. Il backend serve solo auth, chat streaming, cloud storage e billing. Questo significa che la latenza del backend impatta meno di quanto sembrerebbe.
  • WebSocket /chat/stream: il geo steering porta l'utente al nodo piu vicino, ma la risposta LLM dipende dalla latenza verso OpenAI/Anthropic (non verso il tuo server). Il beneficio principale e nel tempo di handshake e nel primo token.
  • _pending_states in-memory per OAuth: gia documentato come non scalabile su multi-worker. Con multi-region diventa critico — servira Redis condiviso o spostare lo state su DB.
  • JWT_SECRET deve essere identico su tutti i nodi — un token emesso dal nodo EU deve essere validato dal nodo US.
  • Alembic migrations: eseguire SOLO sul primary. Il replica riceve le DDL via streaming replication.

Stima costi

Componente Costo mensile
Argo Smart Routing ~$5 + $0.10/GB
Cloudflare Load Balancing ~$5/pool
VPS Hetzner US (CX22) ~$5-10
WireGuard Gratis
Totale Fase 1 ~$5
Totale Fase 2 ~$15-20