Cloud Intelligence™Cloud Intelligence™

Cloud Intelligence™

Scalabilità Firestore: la regola 500/50/5 e come testarla

By Matthias BaetensOct 28, 20246 min read

Questa pagina è disponibile anche in English, Deutsch, Español, Français, 日本語 e Português.

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.