254 lines
11 KiB
Markdown
254 lines
11 KiB
Markdown
# 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:
|
|
```sql
|
|
CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD '<strong_password>';
|
|
```
|
|
2. Creare un replication slot:
|
|
```sql
|
|
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`:
|
|
```yaml
|
|
services:
|
|
db:
|
|
ports:
|
|
- "10.0.0.1:5432:5432" # solo interfaccia WireGuard
|
|
```
|
|
|
|
**Sul REPLICA (US):**
|
|
|
|
1. Base backup dal primary:
|
|
```bash
|
|
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:
|
|
```env
|
|
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** |
|