Nel panorama dei database cloud NoSQL, Firestore si distingue come soluzione flessibile e scalabile per lo sviluppo mobile, web e server. Tuttavia, nonostante le sue capacità, è diffusa la convinzione errata che Firestore sia in grado di reggere qualsiasi carico senza battere ciglio. In teoria è vero, ma nella pratica la questione è più sfumata. Immagini di lanciare la sua nuova feature di punta dopo aver già registrato in passato picchi di traffico importanti. Oppure conosce bene le abitudini dei suoi utenti e sa che in una determinata fascia oraria il carico aumenta in modo significativo. In questo articolo analizzeremo un classico problema di scalabilità di Firestore e le illustreremo un metodo pratico per testarlo ed evitarlo.
La regola 500/50/5: una breve introduzione a Firestore
Firestore è progettato per scalare ma, come ogni sistema elastico e distribuito, ha bisogno di tempo per adattarsi all'aumento del carico. È qui che entra in gioco la regola 500/50/5:
Inizi con un massimo di 500 operazioni al secondo su una nuova collection, quindi aumenti il traffico del 50% ogni 5 minuti.
Questa linea guida assicura che i meccanismi interni di scaling di Firestore riescano a stare al passo con la crescita, evitando problemi tipici come latenza elevata o errori DEADLINE_EXCEEDED.
Ecco k6: l'alleato ideale per i load test
Per illustrare l'importanza della regola 500/50/5 abbiamo creato uno script con k6, uno strumento open-source per i load test. k6 è una scelta eccellente per diversi motivi:
- È semplice da usare grazie a un linguaggio di scripting basato su JavaScript.
- Fornisce metriche di performance in tempo reale e analisi dettagliate.
- È estremamente scalabile: da una singola istanza può generare 100.000–300.000 richieste al secondo.
Lo script
Lo script è disponibile qui. Ecco una panoramica del suo funzionamento:
Carico iniziale e di destinazione:
- Parte da 500 richieste al secondo (RPS)
- Punta a raggiungere 1500 RPS (può ovviamente modificare questo valore)
Strategia di ramp-up:
- Mantiene ogni livello di carico per 5 minuti (300 secondi)
- Aumenta il carico del 50% in finestre di 1 minuto
- Prosegue con questo schema fino a raggiungere o superare l'RPS target
Generazione dinamica degli stage:
- Calcola automaticamente il numero di stage necessari
- Crea una sequenza di stage alternati di tipo "stable" e "ramp-up"
- Registra l'RPS target e la durata di ciascuno stage per maggiore chiarezza
Selezione dei documenti:
- Legge gli ID dei documenti da un file ('orders.txt')
- Seleziona casualmente un ID documento per ogni richiesta
- Dovrà reperire questi ID documento per il suo caso d'uso, dato che io ho usato un dataset fittizio
Esecuzione delle richieste:
- Esegue richieste GET verso l'API REST di Firestore
- Include l'autenticazione tramite bearer token
- Ho incluso anche uno script che genera il token al posto suo
Monitoraggio delle performance:
- Tiene traccia delle letture riuscite e degli errori
- Registra eventuali codici di stato diversi da 200, con i relativi dettagli
Può eseguire lo script con k6 run warm-up.js dopo aver installato k6 (ad esempio tramite brew se è su Mac). Può ottenere un token per lo script con generate-firebase-token.py. In entrambi gli script ci sono alcune variabili da aggiornare: usi la funzione "Trova" del suo editor e cerchi INSERT.
L'esperimento: successo o fallimento
Abbiamo condotto due esperimenti per dimostrare l'impatto del rispettare (o ignorare) la regola 500/50/5, così che possa toccare con mano la differenza:
Esperimento 1: la ricetta per il fallimento
In questo scenario abbiamo iniziato con 2000 richieste al secondo (RPS) e siamo saliti a 2500 RPS in 5 minuti, ignorando completamente la regola 500/50/5.
```js
// Warmup parameters
const initialRPS = 2000;
const targetRPS = 2500;
const stablePeriodSeconds = 300; // 5 minutes
const rampPeriodSeconds = 0;
const stageCount = Math.ceil(Math.log(targetRPS / initialRPS) / Math.log(1.5));
```
Eseguito tra 1/1/10 dalle 0110 alle 0115 CEST
Risultati:
```bash
INFO[0335] Warmup Stages: source=console
INFO[0335] Stage 1: Target RPS: 2500, Duration: 300s source=console
✗ status is 200
↳ 4% — ✓ 6408 / ✗ 123474
checks.........................: 4.93% ✓ 6408 ✗ 123474
data_received..................: 23 MB 70 kB/s
data_sent......................: 4.6 MB 14 kB/s
dropped_iterations.............: 1 0.003028/s
errors.........................: 123474 373.866077/s
http_req_blocked...............: avg=473.49ms min=0s med=0s max=59.9s p(90)=0s p(95)=0s
http_req_connecting............: avg=287.6ms min=0s med=0s max=38.39s p(90)=0s p(95)=0s
http_req_duration..............: avg=806.08ms min=0s med=0s max=1m3s p(90)=0s p(95)=2.01s
{ expected_response:true }...: avg=12.62s min=311.44ms med=9.48s max=1m0s p(90)=30.92s p(95)=36.56s
http_req_failed................: 95.06% ✓ 123474 ✗ 6409
http_req_receiving.............: avg=82.85ms min=0s med=0s max=59.4s p(90)=0s p(95)=30µs
http_req_sending...............: avg=651.35µs min=0s med=0s max=8.82s p(90)=0s p(95)=92µs
http_req_tls_handshaking.......: avg=261.72ms min=0s med=0s max=57.6s p(90)=0s p(95)=0s
http_req_waiting...............: avg=722.58ms min=0s med=0s max=1m2s p(90)=0s p(95)=1.89s
http_reqs......................: 129883 393.271845/s
iteration_duration.............: avg=32.65s min=2.58µs med=33.98s max=1m12s p(90)=48.47s p(95)=51.64s
iterations.....................: 129883 393.271845/s
successful_reads...............: 4.93% ✓ 6408 ✗ 123474
vus............................: 47 min=0 max=25000
vus_max........................: 25000 min=4179 max=25000
running (5m30.3s), 00000/25000 VUs, 129882 complete and 21 interrupted iterations
firestore_warmup ✓ [======================================] 00021/25000 VUs 5m0s 2105.47 iters/s
```
Ecco come si presenta in Key Visualiser:

Il risultato? Tasso di successo sotto il 5%. Ahia.
Esperimento 2: la ricetta per il successo
In questo test abbiamo rispettato la regola 500/50/5, partendo da 500 RPS e salendo gradualmente a 1500 RPS nell'arco di circa 20 minuti.
```js
// Warmup parameters
const initialRPS = 500;
const targetRPS = 1500;
const stablePeriodSeconds = 300; // 5 minutes
const rampPeriodSeconds = 60; // 1 minute
const stageCount = Math.ceil(Math.log(targetRPS / initialRPS) / Math.log(1.5));
```
Eseguito tra 1/1/10 dalle 0140 alle 0158 CEST
Risultati:
```bash
INFO[1111] Warmup Stages: source=console
INFO[1111] Stage 1: Target RPS: 500, Duration: 300s source=console
INFO[1111] Stage 2: Target RPS: 750, Duration: 60s source=console
INFO[1111] Stage 3: Target RPS: 750, Duration: 300s source=console
INFO[1111] Stage 4: Target RPS: 1125, Duration: 60s source=console
INFO[1111] Stage 5: Target RPS: 1125, Duration: 300s source=console
INFO[1111] Stage 6: Target RPS: 1500, Duration: 60s source=console
✗ status is 200
↳ 99% — ✓ 863739 / ✗ 231
checks.........................: 99.97% ✓ 863739 ✗ 231
data_received..................: 1.5 GB 1.4 MB/s
data_sent......................: 173 MB 156 kB/s
dropped_iterations.............: 20999 18.915827/s
errors.........................: 231 0.208084/s
http_req_blocked...............: avg=50.75ms min=0s med=0s max=41.09s p(90)=1µs p(95)=1µs
http_req_connecting............: avg=35.83ms min=0s med=0s max=29.93s p(90)=0s p(95)=0s
http_req_duration..............: avg=554.84ms min=0s med=334.66ms max=1m0s p(90)=728.85ms p(95)=1.29s
{ expected_response:true }...: avg=554.07ms min=304.44ms med=334.66ms max=59.46s p(90)=728.84ms p(95)=1.29s
http_req_failed................: 0.02% ✓ 231 ✗ 863739
http_req_receiving.............: avg=68.05ms min=0s med=6.92ms max=59.42s p(90)=21.82ms p(95)=160.12ms
http_req_sending...............: avg=288.4µs min=0s med=32µs max=12.11s p(90)=89µs p(95)=150µs
http_req_tls_handshaking.......: avg=16.93ms min=0s med=0s max=46.68s p(90)=0s p(95)=0s
http_req_waiting...............: avg=486.5ms min=0s med=327.66ms max=1m0s p(90)=615.6ms p(95)=943.67ms
http_reqs......................: 863970 778.261194/s
iteration_duration.............: avg=609.81ms min=2.2µs med=334.94ms max=1m0s p(90)=748.42ms p(95)=1.35s
iterations.....................: 863970 778.261194/s
successful_reads...............: 99.97% ✓ 863739 ✗ 231
vus............................: 14 min=14 max=5720
vus_max........................: 5849 min=1000 max=5849
running (18m30.1s), 00000/05849 VUs, 863970 complete and 14 interrupted iterations
firestore_warmup ✓ [======================================] 00014/05849 VUs 18m0s 1499.93 iters/s
```
Ecco come si presenta in Key Visualiser:

Inizio dello scaling

Fine dello scaling
L'esito? Un notevole tasso di successo del 99,97%.
Eseguire i propri test
Possiamo lanciare lo script con k6 in locale, soluzione che presenta vantaggi come la facilità di setup, l'assenza di costi e così via. Va però considerato che potrebbe essere limitato dalle risorse della sua macchina locale, avere una sola istanza a disposizione e ottenere risultati condizionati dai vincoli di rete. Per misurazioni più affidabili, può aver senso eseguire lo script su una VM (Google Cloud).
La regola 500/50/5 non è un semplice consiglio: è una linea guida fondamentale per garantire che la sua implementazione di Firestore scali in modo fluido ed efficiente. Seguendola e affidandosi a strumenti come k6 per testare le sue strategie di scaling, può evitare problemi di performance e mantenere reattiva la sua applicazione man mano che cresce.
Ricordi: quando si parla di scaling dei database, chi va piano va sano e va lontano. Buon scaling!

— -
Vuole approfondire lo scaling di Firestore o ha bisogno di aiuto per ottimizzare la sua infrastruttura cloud? Visiti doit.com per scoprire come possiamo aiutarla a sfruttare al massimo il potenziale del cloud.