Una delle mie domande preferite nei colloqui è "Cosa ti dicono parole come async e await ?" perché apre l'opportunità di avere una discussione interessante con un intervistato... O non lo fa perché galleggiano su questo argomento. A mio parere, è drasticamente importante capire perché utilizziamo questa tecnica.
Ho la sensazione che molti sviluppatori preferiscano affidarsi all'affermazione "è la migliore pratica" e utilizzare ciecamente metodi asincroni.
Questo articolo illustra la differenza pratica tra metodi asincroni e sincroni.
Eseguirò un benchmark nel modo seguente. Due istanze di locust indipendenti sono in esecuzione su due macchine. Le istanze di locust simulano un utente che esegue le seguenti operazioni:
Sotto il cofano, ogni App Service si connette al proprio database ed esegue una query SELECT che impiega cinque secondi e restituisce alcune righe di dati. Vedere il codice del controller di seguito per i riferimenti. Userò Dapper per effettuare una chiamata al database. Vorrei attirare la vostra attenzione sul fatto che anche l'endpoint asincrono chiama il database in modo asincrono ( QueryAsync<T> ).
Vale la pena aggiungere che distribuisco lo stesso codice in entrambi i servizi dell'app.
Durante il test, il numero di utenti cresce in modo uniforme fino al numero target ( Numero di utenti ). La velocità di crescita è controllata da un parametro Spawn Rate (numero di utenti unici che si uniscono al secondo): più alto è il numero, più velocemente vengono aggiunti gli utenti. Lo spawn rate è impostato su 10 utenti/s per tutti gli esperimenti.
Tutti gli esperimenti sono limitati a 15 minuti.
I dettagli sulla configurazione della macchina sono reperibili nella sezione Dettagli tecnici dell'articolo.
Le linee rosse si riferiscono rispettivamente all'endpoint asincrono, mentre le linee blu all'endpoint sincrono.
Questo è tutto per quanto riguarda la teoria. Cominciamo.
Possiamo osservare che entrambi gli endpoint hanno prestazioni simili: gestiscono circa 750 richieste al minuto con un tempo di risposta medio di 5200 ms.
Il grafico più affascinante di questo esperimento è un trend di thread. Puoi vedere numeri significativamente più alti per l'endpoint sincrono (un grafico blu) — più di 100 thread!
Ciò è previsto, tuttavia, e corrisponde alla teoria: quando arriva una richiesta e l'applicazione effettua una chiamata al database, il thread viene bloccato perché deve attendere il completamento di un roundtrip. Pertanto, quando arriva un'altra richiesta, l'applicazione deve produrre un nuovo thread per gestirla.
Il grafico rosso, il conteggio dei thread dell'endpoint asincrono, dimostra un comportamento diverso. Quando arriva una richiesta e l'applicazione effettua una chiamata al database, il thread torna a un pool di thread invece di essere bloccato. Pertanto, quando arriva un'altra richiesta, questo thread libero viene riutilizzato. Nonostante le richieste in arrivo crescano, l'applicazione non richiede nuovi thread, quindi il loro conteggio rimane lo stesso.
Vale la pena menzionare la terza metrica: il tempo di risposta mediano . Entrambi gli endpoint hanno mostrato lo stesso risultato: 5200 ms. Quindi, non c'è differenza in termini di prestazioni.
Adesso è il momento di tirare su la posta.
Abbiamo raddoppiato il carico. L'endpoint asincrono gestisce questa attività con successo: la sua richiesta al minuto si aggira intorno a 1500. Il fratello sincrono ha infine raggiunto un numero comparabile di 1410. Ma se guardate il grafico qui sotto, vedrete che ci sono voluti 10 minuti!
Il motivo è che l'endpoint sincrono reagisce all'arrivo di un nuovo utente creando un altro thread, ma gli utenti vengono aggiunti al sistema (solo per ricordarti che lo Spawn Rate è di 10 utenti/s) più velocemente di quanto il server web possa adattarsi. Ecco perché ha messo in coda così tante richieste all'inizio.
Non sorprende che la metrica del conteggio dei thread sia ancora intorno a 34 per l'endpoint asincrono, mentre è aumentata da 102 a 155 per quello sincrono. Il tempo di risposta mediano si è degradato in modo simile alla velocità di richiesta al minuto : il tempo di risposta sincrono era molto più alto all'inizio dell'esperimento. Se avessi mantenuto il test per 24 ore, i numeri mediani sarebbero diventati pari.
Il terzo esperimento ha lo scopo di dimostrare le tendenze emerse durante il secondo: possiamo osservare un ulteriore degrado dell'endpoint sincrono.
Utilizzare operazioni asincrone anziché sincrone non migliora direttamente le prestazioni o l'esperienza utente. Innanzitutto, migliora la stabilità e la prevedibilità sotto pressione. In altre parole, aumenta la soglia di carico in modo che il sistema possa elaborare di più prima che si degradi.
Per ottenere un risultato di test più pulito, avrei dovuto eseguire i test da 2 VM situate nella stessa rete in cui si trovano i servizi app di destinazione.
Tuttavia, ho dato per scontato che un ritardo di rete avrebbe avuto un impatto su entrambe le app in modo più o meno simile. Pertanto, non può compromettere l'obiettivo principale, ovvero confrontare il comportamento dei metodi asincroni e sincroni.
Cosa ho modificato per forzare l'endpoint sincrono a comportarsi quasi come asincrono e tracciare il grafico sottostante (le condizioni dell'esperimento sono le stesse del terzo: 200 utenti)?