Microservizi: la trappola del monolite distribuito
Dividere la tua app in 10 servizi non la rende più veloce. Rende più difficile il debug. Una guida tecnica ai monoliti modulari, alla progettazione guidata dal dominio (DDD) e al modello Saga.
Nel 2015 il mantra del settore era assoluto: “I monoliti sono dinosauri. I microservizi sono il futuro”. Entro il 2025, i postumi della sbornia sono iniziati. Le aziende che dividono ciecamente le loro applicazioni stanno ora annegando in quello che chiamiamo il monolite distribuito: un sistema con tutta la complessità del calcolo distribuito e nessuno dei vantaggi del disaccoppiamento.
A Maison Code Paris, agiamo come coscienza architettonica per i nostri clienti. Spesso ereditiamo progetti “Microservizi” in cui un semplice accesso utente tocca sei diversi servizi, ha una latenza di 2 secondi e costa € 5.000 al mese in tariffe di ingresso nel cloud.
Questo articolo è una guida tecnica sobria su quando adottare i microservizi e, cosa più importante, su quando scappare da essi.
Perché Maison Code ne parla
In Maison Code Paris, agiamo come la coscienza architettonica dei nostri clienti. Spesso ereditiamo stack “moderni” costruiti senza una comprensione fondamentale della scala.
Discutiamo di questo argomento perché rappresenta un punto di svolta critico nella maturità ingegneristica. Implementarlo correttamente differenzia un MVP fragile da una piattaforma resiliente di livello aziendale.
Il caso del monolite modulare
Prima di digitare “mkdir service-user”, considera il Modular Monolith. Si tratta di una singola unità distribuibile (un binario/contenitore) in cui il codice è rigorosamente separato in moduli (pacchetti) che rispecchiano i domini aziendali.
La struttura è importante
Un “Spaghetti Monolith” ha file che si importano a vicenda in modo casuale. Un monolite modulare ha confini rigidi imposti da regole di linting o obiettivi di compilazione.
“testo”. origine/ moduli/ auth/ # Contesto delimitato: autenticazione api/ # Interfaccia pubblica interno/# Implementazione privata (database, logica) inventory/ # Contesto delimitato: Inventory api/ interno/ billing/ # Contesto delimitato: Fatturazione api/ interno/
**La Legge di Demetra**: Il codice in "fatturazione" *non può* importare "inventario/interno". Può chiamare solo "inventory/api".
Se segui questa convalida, ottieni il 90% dei vantaggi dei microservizi (separazione del team, codice più pulito) con lo 0% degli svantaggi (latenza di rete, coerenza finale, complessità di distribuzione).
## Quando dividere (le condizioni al contorno)
Consigliamo di dividere un modulo in un microservizio separato solo se soddisfa uno di questi rigorosi criteri:
1. **Requisiti hardware eterogenei**:
*L'**API Core** è vincolata all'I/O. Richiede 1 GB di RAM e 0,5 CPU.
* Il **Processore di immagini** è vincolato alla CPU. Richiede 16 GB di RAM e accesso alla GPU.
* *Decisione*: estrarre l'elaboratore di immagini. L'implementazione di nodi GPU per l'API è uno spreco di denaro.
2. **Velocità di rilascio indipendente**:
* Il **team di pagamento** invia il codice ogni ora.
* Il **Ledger Team** (Contabilità) spinge una volta al mese dopo un controllo rigoroso.
* *Decisione*: Estrai la cassa per evitare di bloccare la squadra ad alta velocità.
3. **Isolamento fatale (il "raggio di esplosione")**:
* Se il **Generatore PDF** presenta una perdita di memoria e si blocca, non dovrebbe disattivare il **Gateway di pagamento**.
* *Decisione*: isolare i componenti instabili o rischiosi.
## Modelli di comunicazione: REST, gRPC ed eventi
Una volta che hai più servizi, il problema più difficile è la comunicazione. Le "chiamate di funzione" diventano "pacchetti di rete".
### Sincrono: la trappola HTTP
Il servizio A chiama il servizio B tramite HTTP/REST.
"OTTIENI /users/123".
Ciò abbina la disponibilità di A a B. Se B è inattivo, A fallisce. Se B è lento, A si blocca.
Se A chiama B, che chiama C, che chiama D, hai una **Catena di chiamate**.
Se ogni servizio ha una disponibilità del 99,9%, una catena di 4 servizi ha una disponibilità di $ 0,999^4\circa 99,6\%$. Stai progettando un fallimento valido.
### gRPC: la variante ad alte prestazioni
Per la comunicazione interna, preferiamo **gRPC** (Protobufs) rispetto a REST/JSON.
* **Binary Packed**: carichi utili 10 volte più piccoli.
* **Fortemente tipizzato**: la generazione del codice garantisce che il servizio A sappia esattamente cosa si aspetta il servizio B.
* **Multiplexing**: supporto HTTP/2 pronto all'uso.
### Asincrona: l'architettura guidata dagli eventi
Questo è il Santo Graal del disaccoppiamento.
Il servizio A *non* chiama il servizio B. Il servizio A emette un evento.
1. Registri degli utenti (Servizio A).
2. Il servizio A pubblica l'evento "USER_REGISTERED" su Kafka/RabbitMQ.
3. Il servizio B (e-mail) consuma l'evento -> Invia e-mail di benvenuto.
4. Il servizio C (Analytics) consuma l'evento -> Dashboard degli aggiornamenti.
Se il servizio B è offline (manutenzione, arresto anomalo), il servizio A continua a funzionare. Il messaggio rimane in coda. Quando il servizio B torna online, elabora il backlog. Questa è **Resilienza**.
## Coerenza dei dati: la parte più difficile
In un Monolite, hai **Transazioni ACID**.
`INIZIO TRANSAZIONE; INSERISCI Utente; INSERISCI Conto; IMPEGNO;`
O succedono entrambe le cose, oppure non succede nessuna delle due.
Nei microservizi hai **Database per servizio**.
Il servizio A ha `users_db`. Il servizio B ha "accounts_db". Non è possibile eseguire una transazione su due server di database diversi.
### Il modello della saga
Come gestisci una transazione distribuita?
**Scenario**: l'utente effettua un ordine.
1. **Servizio di inventario**: riserva di scorte.
2. **Servizio di pagamento**: Carta di addebito.
3. **Servizio di spedizione**: stampa dell'etichetta.
Cosa succede se "Carta di addebito" fallisce? Hai già prenotato lo stock. È necessario "Annulla" passaggio 1.
Questa è una **Transazione di compensazione**.
"sirena".
sequenzaDiagramma
partecipante O come orchestratore dell'ordine
partecipante I come Inventario
partecipante P come Pagamento
O->>I: Stock di riserva (Articolo X)
I-->>O: Successo
O->>P: Addebita $100
P-->>O: Guasto (nsf)
O->>I: RILASCIO Stock (Compensazione)
I-->>O: Successo
O-->>Utente: Ordine fallito
Implementare correttamente le Saghe è incredibilmente complesso. È necessario un orchestratore di macchine a stati (come AWS Step Functions o Temporal.io standard). Se non hai bisogno di questa complessità, non creare microservizi.
Osservabilità: vedere nell’oscurità
In un monolite, il debug è tail -f /var/log/app.log.
Nei microservizi, una richiesta di un singolo utente potrebbe raggiungere 10 contenitori diversi su 10 nodi diversi.
Devi implementare il Tracciamento distribuito (OpenTelemetry).
Ogni richiesta riceve un “TraceID” nel bilanciatore del carico Ingress. Questo ID viene propagato nelle intestazioni HTTP (X-B3-TraceId) a ogni servizio downstream.
Strumenti come Jaeger, Datadog o Honeycomb visualizzano quindi la “Cascata” della richiesta.
- “Perché questi 500 ms sono stati lenti?”
- “Oh, il servizio D ha impiegato 450 ms su una query DB.”
Senza tracciare, stai volando alla cieca.
Infrastruttura: esempio di codice
Utilizziamo Terraform per fornire l’infrastruttura per servizi indipendenti.
# servizio-pagamento.tf
risorsa "kubernetes_deployment" "pagamento" {
metadati {
nome = "servizio di pagamento"
}
specifica {
repliche = 3
selezionatore {
etichette_corrispondenza = {
app = "pagamento"
}
}
modello {
metadati {
etichette = {
app = "pagamento"
}
}
specifica {
contenitore {
immagine = "codice maison/pagamento:v1.2"
nome = "pagamento"
# La regola d'oro: limitare le risorse
risorse {
limiti = {
CPU = "500m"
memoria = "512Mi"
}
richieste = {
CPU = "250m"
memoria = "256Mi"
}
}
ambiente {
nome = "DB_HOST"
valore_da {
riferimento_chiave_segreta {
nome = "crediti-db-pagamento"
chiave = "ospite"
}
}
}
}
}
}
}
}
La strategia di decomposizione: Lo Strangolatore Fig
Se disponi di un monolite legacy, non riscriverlo da zero. Usa il Schema del fico strangolatore.
- Metti un Proxy (API Gateway) davanti al Monolito.
- Instradare tutto il traffico verso Monolith.
- Identificare un contesto delimitato (ad esempio, “Recensioni”).
- Costruisci il “Servizio recensioni”.
- Modifica proxy: se il percorso è “/api/reviews”, instrada al nuovo servizio. Altrimenti dirigetevi verso Monolith.
- Ripeti finché il monolito non scompare.
11. Il ruolo del gateway API
Non vuoi che 10 servizi gestiscano l’autenticazione in modo indipendente. Posiziona un gateway (Kong, Tyk o AWS API Gateway) sul perimetro. Gestisce:
- Aut: Convalida JWT.
- Limitazione di velocità: 100 richieste/min per IP.
- Caching: memorizza nella cache le richieste GET pubbliche.
- Routing:
/api/v1/users-> Servizio utenti. Ciò mantiene i tuoi servizi interni “stupidi” e concentrati sulla logica aziendale.
##12. Rete di servizi (Istio/Linkerd)
Quando hai 50 servizi, il “Servizio A” che comunica con il “Servizio B” necessita di crittografia (mTLS). Gestire manualmente i certificati è un inferno. Un Service Mesh inserisce un “Sidecar Proxy” (Envoy) accanto a ogni contenitore. Il proxy gestisce:
- mTLS: crittografia automatica.
- Riprovi: “Se fallisce, riprova 3 volte con backoff esponenziale”.
- Interruzione del circuito: “Se il servizio B presenta errori del 50%, smetti di chiamarlo per 1 minuto”. Disaccoppia la logica di rete dal codice dell’applicazione.
13. Test del contratto (patto)
Come si testa il servizio A senza avviare il servizio B?
Test a contratto.
Il servizio A (Consumatore) scrive un “File patto”:
“Mi aspetto che GET /user restituisca { id: string }”.
Il servizio B (provider) esegue un test su questo file Pact nella pipeline CI.
Se il servizio B modifica “id” in “userId”, la compilazione fallisce.
Ciò rileva le modifiche importanti prima della distribuzione.
14. Conclusione
I microservizi sono un modello di scalabilità organizzativa, non un’ottimizzazione delle prestazioni. Introducono latenza di rete, problemi di coerenza e complessità operativa in cambio di velocità del team e scalabilità indipendente.
Se sei un team di 5 sviluppatori, stai costruendo un Monolito. Potresti chiamarli microservizi, ma stai semplicemente costruendo una macchina antidolore distribuita.
Ridimensiona la struttura del tuo codice prima di ridimensionare la tua infrastruttura.
**[Assumi i nostri architetti](/contact)**.