skip to content
OphusDev

Gestione dei requisiti: il fattore umano dietro le performance

/ 5 min read

Updated:
Table of Contents

Per circa un anno e mezzo ho seguito un progetto per un cliente che si occupava di onboarding di potenziali clienti.

La richiesta iniziale, a prima vista, era abbastanza chiara:

  • Avere un servizio che esponesse delle API per consultare in tempo reale un insieme di fonti dati.
  • Garantire tempi di risposta inferiori a 1 secondo.

Startup: Partenza in quarta (con qualche dubbio)

Il progetto è partito con quella tipica urgenza dettata dalla necessità di andare in produzione al più presto.

Ricevuta la lista delle fonti da cui recuperare i dati, mi sono reso conto di partire avvantaggiato: per tutte e otto le fonti avevo già a disposizione i microservizi dedicati. Conoscevo a memoria il dominio e come gestire i dati.

Tuttavia, nel mettere insieme i pezzi, sono emerse subito un paio di considerazioni in netto contrasto con i requisiti del cliente (consultazione in tempo reale e risposte sotto il secondo).

Conoscendo bene quelle fonti, sapevo che un paio di endpoint remoti fallivano spesso per timeout o errori di sovraccarico.

Ogni mio microservizio è dotato di retry automatici con backoff esponenziale e, tramite appositi header, posso pilotare questi tentativi (aumentarli, diminuirli, velocizzarli). Ma una cosa era certa: sia applicando i retry sia evitandoli, la velocità di risposta o la correttezza del dato ne avrebbero risentito.

Ho condiviso subito queste osservazioni con il team. La questione è stata presa in esame, ma sul momento non sono stati richiesti cambiamenti immediati.

(Spoiler: i cambiamenti arriveranno).

Senza darci per vinti, in circa un mese il servizio era pronto all’uso e completamente integrato.

Primi test (i miei e quelli del cliente)

Per questo progetto, più che una suite estesa di unit test, era fondamentale avere dei test di integrazione. Questa necessità mi ha dato l’opportunità di approfondire l’uso di k6, un framework con cui è stato davvero semplice costruire un’efficace suite di stress test, da eseguire sia in locale sia in ambiente di test.

I risultati degli stress test sui tempi di risposta non mi soddisfacevano del tutto: eravamo ancora lontani dal secondo richiesto.

Inizialmente il progetto ha soddisfatto i criteri del cliente ma, come previsto, i nodi sono venuti al pettine e i rallentamenti strutturali delle ricerche si sono presentati.

Ci siamo riuniti in una sessione di brainstorming con i product manager e i referenti del cliente.

Non potendo scendere a compromessi sui requisiti iniziali, serviva un approccio architetturale diverso.

Cambiando l’ordine degli addendi… il risultato cambia?

Analizzando meglio la situazione, è emerso che per sei delle fonti richieste potevo recuperare il 95% dei dati complessivi con uno scarto temporale di appena qualche settimana rispetto al “tempo reale”.

Ottimo.

Ho strutturato la soluzione così:

  1. Coda di eventi: Ho preparato degli endpoint interni basati su code che accettassero determinati eventi dagli altri microservizi, salvando in locale solo i dati mancanti o modificati rispetto all’ultimo record in mio possesso.
  2. Ottimizzazione DB: Sfruttando PostgreSQL, ho creato gli indici necessari sulle tabelle, inclusi alcuni indici GIST sulle colonne utili a effettuare ricerche full-text.
  3. Approccio ibrido via Header: Ho aggiornato le API permettendo, tramite degli header dedicati, di scegliere la modalità di ricerca:
    • In realtime sulle fonti dati esterne (dati aggiornatissimi, ma con tempi di attesa più lunghi).
    • In cache sui dati già collezionati (risposta fulminea, ma dati potenzialmente meno aggiornati).

Inoltre, se un dato non era presente localmente, una volta recuperato dalla fonte esterna veniva salvato nel database, così da evitare query remote alla ricerca successiva.

Ho rilanciato la suite di stress test con k6: tempo di risposta medio a ~0.5 secondi.

Progetto consegnato, qualche sessione di rodaggio e piccoli aggiustamenti, e poi dritti in produzione.

Linea Piatta: La calma prima della tempesta

Per circa sei mesi le metriche di utilizzo e le performance sono state eccellenti. Il servizio era stabile e ampiamente sotto i tempi previsti.

Gli utenti finali avevano persino trovato una “scorciatoia” ingegnosa: per ogni controllo inviavano due richieste speculari:

  • La prima per ricevere immediatamente i dati in cache.
  • La seconda per ottenere il dato in tempo reale.

In questo modo confrontavano i due output. Il sistema comunque reggeva il carico senza problemi.

Fast Forward: Il cambio di rotta

Dopo qualche tempo, ho notato che il volume delle metriche cominciava a calare drasticamente fino quasi ad azzerarsi, mentre i tempi medi di risposta erano aumentati. Il sistema non aveva bug, quindi cosa stava succedendo?

Analizzando i log, è emerso che le richieste in arrivo erano diventate esclusivamente consultazioni in tempo reale, azzerando quelle in cache. Questo spiegava l’impennata dei tempi di risposta, legati alla lentezza fisiologica delle fonti esterne su cui non avevo margine di manovra.

Era cambiato radicalmente il modo in cui gli utenti usavano la piattaforma del cliente. Richiedendo solo ed esclusivamente il tempo reale, non era più possibile mantenere la promessa della risposta sotto il secondo. Di conseguenza, il progetto è stato messo in stand-by.

Conclusioni

Dal punto di vista tecnico, il bilancio è super positivo: il progetto è solido, documentato e scalabile. Mi ha permesso di usare nuovi strumenti in un contesto reale.

Cosa mi ha permesso di evolvere il progetto in tempi record? Senza dubbio la conoscenza profonda del dominio. Avere padronanza della materia mi ha permesso di:

  • Sviluppare rapidamente e anticipare gli ostacoli logici.
  • Spiegare chiaramente al business perché certe richieste fossero irrealizzabili a causa della natura stessa dei dati.
  • Sapere esattamente dove recuperare le informazioni e comprenderne il significato intrinseco.
  • Gestire gli edge case più complessi legati alla tipologia dei dati interrogati.