#Blog
Asynchronous Batching
L'arma (non) segreta per migliorare le prestazioni delle applicazioni web
11/12/2023
🇬🇧 Are you looking for the english version? Click here
Introduzione
La programmazione asincrona è un concetto fondamentale nello sviluppo web moderno e una tecnica potente nel nostro arsenale è il batching asincrono, o asynchronous batching. In questo articolo approfondiremo il mondo del batching asincrono utilizzando Node.js e Fastify, un framework web noto per la sua velocità e il basso overhead.
Illustreremo il concetto creando un server Fastify che risponderà alle nostre richieste sfruttando sempre la stessa Promise.
Comprensione del batch asincrono
Fondamentalmente, il batching asincrono implica il raggruppamento di più operazioni asincrone in una singola unità di lavoro. Questo approccio è particolarmente utile quando si gestiscono attività o richieste ripetitive, poiché può migliorare significativamente le prestazioni e l’utilizzo delle risorse.
Configurazione di Fastify
Per iniziare, assicurati di avere Node.js e npm installati sul tuo computer - la versione non è importante, Node.js supporta le Promises dalla versione 0.12. Crea un nuovo progetto Fastify, installa le dipendenze necessarie (npm i fastify
) e configura un server di base. La natura semplice e leggera di Fastify lo rende la scelta ideale per questa dimostrazione.
// Importiamo Fastify
const fastify = require('fastify')();
// Definiamo un percorso di base
fastify.get('/', async (request, reply) => {
return { message: 'Hello, Fastify!' };
});
// Configuriamo il server per l'ascolto sulla porta 3000
fastify.listen({ port: 3000 }, (err, address) => {
if (err) throw err;
console.log(`Server listening on ${address}`);
});
Se ora esegui il tuo server con node index.js
e visiti http://localhost:3000
, dovresti vedere un messaggio di benvenuto.
Creazione di un endpoint di esempio
Nel nostro esempio, ci concentreremo su un endpoint HTTP specifico che risponde con la stessa Promise per ogni richiesta in entrata. Questo scenario può sembrare estremamente semplificato, ma rappresenta un esempio semplice del batching asincrono in azione.
// Definiamo una Promise condivisa per semplicità
const sharedPromise = new Promise((resolve) => {
// Simuliamo del lavoro intensivo
for (let i = 0; i < 10_000_000; i++) { }
// Risolviamo la Promise
resolve({ message: 'Hello, shared world!' });
});
// Definiamo il percorso per il batching asincrono
fastify.get('/batched', async (request, reply) => {
// Restituiamo la stessa Promise per ogni richiesta
return sharedPromise;
});
Come puoi vedere, nel pezzo di codice sopra stiamo effettivamente creando una singola Promise restituita dal nostro endpoint. In questo modo, chiediamo al nostro server di eseguire una singola attività, ma restituiamo la stessa risorsa a ogni client che la richiede.
N.B. Ricordati di chiudere il server con Ctrl + C
e riavviarlo ogni volta che apporti modifiche al codice. Se hai una versione aggiornata di Node.js, puoi lanciare il comando node --watch index.js
per riavviare automaticamente il server quando il file viene modificato. Lo stesso vale per nodemon
o altri tool simili.
Testare il nostro server
Ora che abbiamo configurato il nostro server, possiamo testarlo con un semplice client HTTP. In questo esempio useremo autocannon insieme a npx, un piccolo tool da CLI sviluppato da npm che ci permette di eseguire i pacchetti senza doverli installare localmente.
npx autocannon -c 100 -d 5 http://localhost:3000/batched
Questo comando eseguirà 100 connessioni simultanee per 5 secondi, quindi restituirà un report con i risultati. Se tutto è andato bene, dovresti ottenere qualcosa di molto simile a questo, insieme ad una tabella riepilogativa: 302k requests in 5.02s, 51 MB read
.
Per meglio comprendere cosa questo significhi, possiamo eseguire lo stesso test senza il batching asincrono. Per farlo, modifichiamo il nostro endpoint per restituire una nuova Promise per ogni richiesta.
// Definiamo il percorso per il batching asincrono
fastify.get('/batched', async (request, reply) => {
// Restituiamo una nuova Promise per ogni richiesta
return new Promise((resolve) => {
// Simuliamo del lavoro intensivo
for (let i = 0; i < 10_000_000; i++) { }
// Risolviamo la Promise
resolve({ message: 'Hello, shared world!' });
});
});
Ora possiamo eseguire nuovamente il nostro test e confrontare i risultati. Sul mio computer, l’output è il seguente: 2k requests in 5.02s, 260 kB read
.
Come puoi notare, il batching asincrono ha migliorato le prestazioni del nostro server di oltre 100 volte!
Implementazione del batch asincrono nella vita reale
Lo sviluppo della funzionalità di batching asincrono implica l’intercettazione di più richieste in entrata all’URL designato e la restituzione della stessa Promise per tutte. In uno scenario reale, salveremo l’esecuzione della nostra Promise in una sorta di stato (Globale o locale, non importa) in modo che ogni client possa effettivamente richiedere la stessa risorsa.
Per esempio, potremmo decidere di salvare una ipotetica chiamata a getUsers
in una cache temporanea e restituire la stessa Promise per ogni richiesta. In questo modo, il nostro server eseguirà la chiamata solo una volta, ma restituirà la risposta a tutti i client che la richiedono, auto-invalidandosi dopo un certo periodo di tempo.
// Definiamo una cache temporanea
const cache = {};
// Definiamo il percorso per il batching asincrono
fastify.get('/batched', async (request, reply) => {
// Se la Promise non è in cache, creiamola
if (!cache.users) {
// Eseguiamo la Promise
cache.users = getUsers();
setTimeout(() => {
// Invalidiamo la cache dopo 5 minuti
cache.users = null;
}, 5 * 60 * 1000);
}
// Restituiamo la Promise
return cache.users;
});
Miglioramenti in termini di prestazioni
Il batching asincrono brilla negli scenari in cui più richieste attivano operazioni identiche. Elaborando queste richieste insieme, riduciamo al minimo il sovraccarico associato alle attività ridondanti, con conseguente miglioramento dei tempi di risposta e un utilizzo più efficiente delle risorse.
Considerazioni sul parallellismo - Cache Stampede
Anche se l’invio in batch asincrono migliora le prestazioni, è fondamentale considerare il sovraccarico sul processo e sull’Event Loop. L’architettura basata sugli eventi di Node supporta intrinsecamente queste situazioni, ma un’attenta progettazione è essenziale per garantire che il sistema si comporti in modo prevedibile sotto vari carichi.
Conclusione
Il batch asincrono è uno strumento potente nell’arsenale di uno sviluppatore, poiché offre miglioramenti significativi delle prestazioni per determinati tipi di carichi di lavoro. Esplorando la sua implementazione in un server Node-Fastify, abbiamo acquisito informazioni su come questa tecnica può essere applicata per semplificare e ottimizzare lo sviluppo di applicazioni web. Mentre sperimenti il batch asincrono, considera le sue potenziali applicazioni oltre l’esempio presentato, poiché i suoi vantaggi si estendono a un’ampia gamma di scenari nel panorama in continua evoluzione dello sviluppo web.
Se vuoi vederlo in azione in uno scenario reale, guarda la nostra libreria di caching dove la implementiamo per prevenire il problema solitamente risolto da questa tecnica: Cache Stampede.
Spero che l’articolo ti sia piaciuto, a presto!