Load tests, Stress tests e performance di un servizio REST

A fine 2012 ho iniziato ad avvertire una maggiore sensibilità dei clienti riguardo il tema delle prestazioni di un sistema basato sul web. Parecchi degli applicativi a cui stavo lavorando in quel periodo erano già basati su architetture con piattaforme REST e dati in formato JSON, che si adattavano molto bene al tema della "misurazione dei tempi di risposta". La separazione dei moduli applicativi e - in particolare - del "presentation layer" dallo strato di servizi, è a mio parere una caratteristica essenziale per ottenere una metrica dell'applicazione sotto carico.

Faccio subito una precisazione ritengo doverosa. In ogni situazione - ed in particolar modo se si lavora con SPA (Single Page Application) - è fondamentale distinguere tra le problematiche prestazioni dovute ad un codice client "lento", magari non adeguatamente ottimizzato, da una piattaforma di servizi che fatica a fornire risposte nei tempi desiderati.

In questo post mi soffermerò su questa seconda casistica, poichè i problemi di performance lato client sono molto complicati da individuare, e spesso sono strettamente legati alla tecnologia (o al framework client, nel caso di JavaScript) che si sta utilizzando.

Ma ritorniamo rapidamente al nostro server. Immaginiamo di avere un backend basato su ASP.NET WebAPI: essendo sviluppatore .NET mi sembra la scelta più naturale. Abbiamo già innestato controller applicativi e le action che emettono correttamente i nostri flussi dati in JSON. Supponiamo di riuscire a tracciare un elenco delle request che una normale navigazione della nostra applicazione produce durante l'utilizzo della UI grafica. Questo tracciato di request (e opportuni verb, header e parametri) rappresenta una "sessione utente": questo è il dominio che dovremo andare a considerare per fare la nostra misurazione delle tempistiche di risposta.

Un esempio parla più di mille teorie, si dice di solito...ed è vero! Quindi ipotizziamo di avere un classico portale di commercio elettronico che permette la navigazione del catalogo, la scelta di uno o più prodotti da mettere nel carello virtuale, l'accesso autenticato tramite username e password, e l'operazione di checkout con acquisto dei prodotti. Ignoriamo l'interfacciamento con i sistemi di pagamento tramite carta di credito: è troppo complicato e per nulla utile in questo caso...
Una normale sessione utente (o "user session") è composta da questi passi funzionali:
  1. Visualizzazione della pagina principale del sistema e scaricamento della lista dei primi 10 prodotti più venduti presenti nello storage
  2. Navigazione al catalogo e visualizzazione delle categorie merceologiche
  3. Visualizzazione dell'elenco dei prodotti presenti nella categoria
  4. Dettaglio della scheda di un prodotto, sue specifiche e descrizioni
  5. Aggiunta di quel prodotto nel carrello elettronico
  6. Inserimento di username e password utente (ipotiziamo che il profilo utente sia già stato creato)
  7. Conferma dell'acquisto con la funzione di "checkout"
I passi riportati sopra rappresentano un comportamento "standard" di un utente del nostro portale, e - almeno in questa fase iniziale - non complichiamo troppo le cose pensando che ci possono (e ci saranno) delle naturali deviazioni rispetto questo specifico tracciato (es. navigazione più lunga, visualizzazione di più schede di prodotti, ecc.).

Quello che conta è "simulare" grossomodo quello che comporta una navigazione "da manuale", per farsi un'idea di quante "user session" contemporanee il nostro sistema è in grado di supportare senza creare disservizi o impattare negativamente sulla user experience dell'utente.

Mi rendo conto del fatto che, tutto quello che sto descrivendo, suona un po' "impreciso" e "ipotizzato"; ma vi posso assicurare che la predizione dei carichi a regime di una applicazione web, non è una scienza esatta. Le variabili sono migliaia e - cosa più importante - la componente fondamentale che farà pesare da una parte o dall'altra i vostri risultati e il comportamento umano, per definizione impredicibile ed incontrollabile. Quindi, il meglio che potete fare è cercare di approssimare con basso margine di errore, sperando che le eccezioni - che comunque ci saranno - non vadano a pesare in maniera troppo rilevante sul numero finale.

Quello che bisogna fare da questo punto in poi è certamente più matematico, e meno da "chiromante" ;). Si tratta di mettere un numero (in millisecondi) di fianco ad ogni punto del precedente elenco. Come? Facile: rilevando l'orario corrente prima dell'esecuzione della request lato client, e dopoaver ricevuto la relativa response; la durata di ogni singolo step è presto misurata.

E' fondamentale calcolare altri due indicatori : l'average duration di una request (la durata media), e il throughput, cioè il numero di richieste al secondo che la nostra service platform è in grado di erogare:
  • [average duration]: tempo medio di durata, somma delle durate (in millisecondi) delle varie richieste presenti nel nostro scenario di "user session", diviso per il numero di richieste...che ve lo dico a fare? ;)
  • [throughput]: calcolato come 1000 / [average duration], dove il valore 1000 rappresenta il numero di millisecondi in un secondo, il tutto misurato in "richieste al secondo"
L'obiettivo finale è capire quanti utenti concorrenti il nostro sistema è in grado di sostenere. E per fare ciò è necessario usare la Legge di Little, che dice: il numero di utenti simultanei reali ("concurrent users") che un'applicazione è in grado di supportare è definibile con la formula:
  • [concurrent users] = [throughput] / λ
con il simbolo λ che rappresenta la media del valore di arrivo ("arrival rate") delle richieste. Questo "arrival rate" è l'inverso dell' inter-arrival time, cioè il tempo (misurato in millisecondi) che trascorre tra una richiesta e la successiva.

E' giunto il momento di fare una considerazione estremamente importante: esiste una netta distinzione tra Stress Test e Load Tests. I primi sono mirati a trovare il punto di rottura di una applicazione, cioè a sovraccaricare un sistema al punto tale da raggiungere una situazione nella quale il servizio stesso non viene più erogato. I test di carico (o "Load Tests") sono invece mirati a misurare il numero massimo di utenti (o sessioni utente) che l'applicativo è in grado di sostenere in una porzione di tempo stabilita, erogando il servizio definito dal processo di business.

I test di carico devono quindi rappresentare una stima (ovviamente simulata ed approssimata) di un utilizzo reale. E realisticamente parlando, è abbastanza inverosimile che le request alla piattaforma WebAPI arrivino in continuazione, senza sosta alcuna. Va quindi considerato un altro parametro (ancora un'altro...mi spiace) che rappresenta l'intervallo tra l'arrivo di una response, e l'inizio della request successiva: questo parametro è solitamente chiamato "think time".

L'inter-arrival time che abbiamo citato qualche riga più sopra, è calcolato sommando la durata media dei una singola request/response al server, al think time. E il think time a sua volta è stabilito a tavolino - non calcolato - e basato su un'osservazione del comportamento di modelli simili in situazioni reali; il che vuol dire che la sua durata è solitamente rilevata su dati prelevati a campione durante l'uso di una applicazione.

Ipotizziamo quindi che il "tempo di riflessione" (traduzione orribile, me ne rendo conto) sia di 5 secondi; ed è una casistica tutto sommato verosimile quella per cui possa arrivare - in media - una request ogni 5000 millesimi di secondo alla piattaforma WebAPI. Quindi, riassumendo:
  • [inter-arrival time] = [think time] + [average request duration]
  • [arrival rate] = 1 / [inter-arrival time]
Il calcolo del numero di utenti - come abbiamo visto - è quindi strettamente legato alla durata di esecuzione della singola richiesta, e dal tempo di intervallo stimato tra una richiesta e l'altra. Ed essendo quest'ultimo una stima, non è possibile dare una risposta precisa ad un vostro eventuale cliente che vi chiede : "Quanti utenti è in grado di supportare la mia applicazione?". Potete rispondere solo avvalendovi di alcuni parametri dettati dall'esperienza (vostra o di altri prima di voi), misurando quanto è effettivamente misurabile, ma nulla di più.

Ma anche solo misurare quel poco che è certo, non è una cosa banale. Ed è per questo che a metà 2013 ho iniziato un piccolo progetto, di cui ho giù parlato in questo post, chiamato RESTful Stress, disponibile sul Chrome Web Store come Chrome App.

Come si usa? Non è nulla di complicato...ma ne parleremo la prossima volta...
M.

Commenti

  1. Ciao,
    una domanda ma nel caso di un applicativo già online, che dopo un aggiornamento riceve un quantitativo di richieste che porta alla "rottura" dell'applicazione, come si gestisce una situazione limite come questa? Cioè per capirci, si può usare il "Throttling", ma se l'applicativo non è MVC?

    Grazie.
    D.

    RispondiElimina
    Risposte
    1. Ciao Davide.
      Un applicativo pubblicato su rete internet è per definizione esposto ad ogni genere di attacco proveniente da utenti leciti e/o illeciti del sistema.

      Il modulo di gestione del "throttling" che ho descritto in un mio articolo dello scorso anno (qui il link http://zenprogramming-it.blogspot.it/2014/03/throttling-monitor-con-aspnet-mvc.html) è una soluzione che mitiga questa problematica, ma non è assolutamente sufficiente a garantirti una sicurezza professionale per la tua applicazione.

      Per questo scopo dovresti dotarti di sistemi di firewall, balancer (per gestire il failover) e reverse proxy, che si pongono come "schermo" tra l'utente esterno e la tua piattaforma; sia questa realizzata con .NET, Java, NodeJs, PHP o qualsiasi altra tecnologia.

      Ovviamente questo genere di soluzione comporta un inevitabile incremento dei costi di gestione, con le conseguenze che si possono immaginare (vanno fatte delle considerazioni a livello di "management" per valutare se la soluzione è sostenibile dal punto di vista del budget economico a disposizione del progetto).

      Inoltre - a parere mio - devi fare delle dovute considerazioni su quanto è "interessante" il flusso funzionale del tuo sistema o quanto i dati contenuti in esso sono preziosi per un potenziale attaccante. Se - per esempio - la tua piattaforma contiene informazioni personali, mediche oppure transazioni bancarie, fa certamente più "gola" rispetto a un blog oppure a un sistema che offre servizi di pubblica utilità.

      Tirando le somme. Il mio consiglio è quello di fare una valutazione accurata di quello che vuoi proteggere, indirizzando nella maniera più opportuna le scelte sulla base di una analisi del rischio che - spesso - viene mal valutata anche in ambienti professionali.

      Ciao.
      M.

      Elimina

Posta un commento

Post popolari in questo blog

RESTful Stress: misurare le performance di un servizio REST

Cancellazione fisica vs cancellazione logica dei dati