Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Quando si sviluppa per Funzioni di Azure usando TypeScript e Node.js, devi capire come funzionano le tue funzioni e come queste prestazioni influenzano la scala della tua app funzionale. Questa comprensione è ancora più importante quando si progettano app ad alte prestazioni. I principali fattori da considerare nella progettazione, scrittura e configurazione delle app di funzione sono la scalabilità orizzontale, le configurazioni di prestazioni di throughput e ottimizzazioni specifiche per Node.js.
Scalabilità orizzontale
Per impostazione predefinita, Funzioni di Azure monitora automaticamente il carico nell'applicazione e crea più istanze host per Node.js in base alle esigenze. Funzioni di Azure utilizza soglie integrate per diversi tipi di trigger per decidere quando aggiungere istanze, come l'età dei messaggi e la dimensione della coda per QueueTrigger. Non puoi configurare queste soglie. Per altre informazioni, vedere Scalabilità guidata dagli eventi in Funzioni di Azure.
Per migliorare la scalabilità della tua app per funzioni Node.js, segui questo approccio in questo ordine:
- Inizia con le impostazioni predefinite e ottimizza il codice usando pattern I/O asincroni.
- Aumentare (aggiungere altre istanze) prima di aumentare il numero dei processi di lavoro.
- Aumentare
FUNCTIONS_WORKER_PROCESS_COUNTsolo dopo che i test di carico mostrano la saturazione della CPU su ogni istanza. - Per i carichi di lavoro di I/O, mantenere il numero di ruoli di lavoro conservativo e concentrarsi sulla riduzione delle chiamate di blocco e della latenza delle dipendenze.
- Ripeti il test dopo ogni modifica e monitora il throughput, la latenza e i tassi di errore per verificare i miglioramenti.
Miglioramento delle prestazioni del throughput
Le configurazioni predefinite sono adatte alla maggior parte delle applicazioni Funzioni di Azure. Tuttavia, è possibile migliorare le prestazioni della velocità effettiva delle applicazioni usando configurazioni basate sul profilo del carico di lavoro. Il primo passaggio consiste nel comprendere il tipo di carico di lavoro in esecuzione.
| Tipo di carico di lavoro | Caratteristiche delle app di funzioni | Examples |
|---|---|---|
| Associato alle operazioni di I/O | • L'app deve gestire molte chiamate simultanee. • L'app elabora un numero elevato di eventi di I/O, ad esempio chiamate di rete e operazioni di lettura/scrittura su disco. |
Web APIs • Operazioni di database • Elaborazione file |
| vincolato alla CPU | • App effettua calcoli di lunga durata, come l'elaborazione delle immagini. • L'app effettua trasformazioni dati o calcoli complessi. |
• Elaborazione dei dati • Trasformazione dei contenuti • Esecuzione degli algoritmi |
Poiché i carichi di lavoro reali delle funzioni sono solitamente un mix di operazioni vincolate dall'I/O e dalla CPU, profila l'applicazione con carichi di produzione realistici. Le seguenti sezioni trattano ottimizzazioni specifiche:
- Schemi async/await
- Processi di lavoro multilingue
- Ottimizzazione del ciclo di eventi
- Gestione della memoria
- Ottimizzazione del codice
Modelli di async/await
Node.js è costruito su un modello di I/O basato su eventi e non bloccante, che lo rende leggero ed efficiente per operazioni I/O legate. Per applicazioni che elaborano un gran numero di eventi I/O o sono vincolate da I/O, puoi migliorare significativamente le prestazioni utilizzando modelli async/await corretti:
-
Uso
Promise.all()per operazioni parallele: quando hai più operazioni asincrone indipendenti, eseguile in parallelo invece che in sequenza. - Evita di bloccare il ciclo degli eventi: Non usare operazioni sincrone nelle funzioni asincrone.
- Usa lo streaming per dati di grandi dimensioni: elabora grandi dataset usando flussi invece di caricare tutto in memoria.
- Implementa una corretta gestione degli errori: usa blocchi try/catch e gestisci i rigetti delle Promise.
Il seguente esempio dimostra operazioni asincrone parallele con Promise.all():
const { app } = require('@azure/functions');
app.http('optimizedHttpTrigger', {
methods: ['GET', 'POST'],
handler: async (request, context) => {
// Good: Using async/await for HTTP calls
const response = await fetch('https://api.example.com/data');
const data = await response.json();
// Good: Parallel async operations
const [user, orders] = await Promise.all([
fetchUser(data.userId),
fetchOrders(data.userId)
]);
return {
jsonBody: { user, orders }
};
}
});
async function fetchUser(userId) {
const response = await fetch(`https://api.example.com/users/${userId}`);
return await response.json();
}
async function fetchOrders(userId) {
const response = await fetch(`https://api.example.com/orders?userId=${userId}`);
return await response.json();
}
module.exports = async function (context, req) {
// Good: Using async/await for HTTP calls
const response = await fetch('https://api.example.com/data');
const data = await response.json();
// Good: Parallel async operations
const [user, orders] = await Promise.all([
fetchUser(data.userId),
fetchOrders(data.userId)
]);
context.res = {
body: { user, orders }
};
};
async function fetchUser(userId) {
const response = await fetch(`https://api.example.com/users/${userId}`);
return await response.json();
}
async function fetchOrders(userId) {
const response = await fetch(`https://api.example.com/orders?userId=${userId}`);
return await response.json();
}
Usare più processi di lavoro multilingue
Per impostazione predefinita, ogni istanza host di Funzioni dispone di un singolo processo di lavoro in linguaggio. È possibile aumentare il numero di processi di lavoro per host (fino a 10) usando l'impostazione dell'applicazione FUNCTIONS_WORKER_PROCESS_COUNT . Funzioni di Azure quindi tenta di distribuire in modo uniforme le chiamate simultanee di funzione tra questi worker.
Avvertimento
Usare l'impostazione FUNCTIONS_WORKER_PROCESS_COUNT con cautela. Più processi in esecuzione nella stessa istanza possono causare un comportamento imprevedibile e aumentare i tempi di caricamento delle funzioni. Se usi questa impostazione, eseguire da un file package può compensare questi svantaggi.
Per le app limitate alla CPU, imposta il numero di lavoratori del linguaggio uguale o superiore al numero di core disponibili per ogni app funzionale. Per altre informazioni, vedere SKU di istanza disponibili.
Le applicazioni vincolate dall’I/O potrebbero anche trarre vantaggio dall’aumento del numero di processi di lavoro oltre il numero di core disponibili. Tenere presente che l'impostazione di un numero di lavoratori troppo elevato può influire sulle prestazioni complessive a causa dell'aumento del numero di cambi di contesto necessari.
Ottimizzazione del ciclo di eventi
Node.js utilizza un ciclo evento a singolo thread per gestire le operazioni di I/O. Ottimizzare il modo in cui il tuo codice interagisce con il ciclo degli eventi è fondamentale per le prestazioni.
Mantieni libero l'event loop
I cicli sincroni che effettuano calcoli pesanti bloccano il ciclo degli eventi e impediscono l'elaborazione di altre invocazioni. Invece, suddividere grandi dataset in batch e ottenere il controllo tra i batch con setImmediate:
async function processDataNonBlocking(data: any[]) {
const batchSize = 100;
for (let i = 0; i < data.length; i += batchSize) {
const batch = data.slice(i, i + batchSize);
await Promise.all(
batch.map(item => processItem(item))
);
// Allow event loop to process other tasks
await new Promise(resolve => setImmediate(resolve));
}
}
Monitoraggio del ritardo del ciclo degli eventi
Usa il seguente modello per misurare quanto tempo le attività rimangono in attesa nella coda del ciclo degli eventi:
import { performance } from 'perf_hooks';
function measureEventLoopDelay(context: any) {
const start = performance.now();
setImmediate(() => {
const delay = performance.now() - start;
context.log(`Event loop delay: ${delay}ms`);
});
}
Gestione della memoria
L'uso efficiente della memoria è fondamentale per le prestazioni di Funzioni di Azure, soprattutto per quanto riguarda gli avviamenti a freddo e la scala. Segui queste linee guida generali quando ottimizzi l'uso della memoria:
- Minimizzare le variabili globali: gli oggetti pesanti in ambito globale influenzano i tempi di avvio a freddo.
- Usa il pool di connessioni: riutilizza le connessioni al database e i client HTTP.
- Libera le risorse: chiudi correttamente le connessioni e azzera i timer.
import { app, InvocationContext, Timer } from '@azure/functions';
// Good: Connection pooling
class DatabaseManager {
private static instance: DatabaseManager;
private connectionPool: any;
private constructor() {
this.connectionPool = createConnectionPool({
max: 10,
min: 2,
idleTimeoutMillis: 30000
});
}
public static getInstance(): DatabaseManager {
if (!DatabaseManager.instance) {
DatabaseManager.instance = new DatabaseManager();
}
return DatabaseManager.instance;
}
public async query(sql: string): Promise<any> {
const client = await this.connectionPool.connect();
try {
return await client.query(sql);
} finally {
client.release();
}
}
}
async function timerTrigger(myTimer: Timer, context: InvocationContext): Promise<void> {
const db = DatabaseManager.getInstance();
const results = await db.query('SELECT * FROM users');
context.log(`Processed ${results.length} users`);
}
app.timer('timerTrigger', {
schedule: '0 */5 * * * *',
handler: timerTrigger
});
Ottimizzazione del codice
Puoi ottimizzare ulteriormente il tuo codice raggruppando l'app, usando una strategia di caricamento pigra e ottimizzando le strutture dati.
Raggruppa le tue dipendenze
Usa strumenti di aggregazione come esbuild, webpack o rollup per combinare la tua function app in meno file. Il bundle riduce il numero di file che il runtime carica durante l'avvio a freddo, migliorando direttamente il tempo di avvio, specialmente per app con alberi di dipendenza grandi. Per ulteriori informazioni, vedere Bundle before deployment.
Minimizzare il tempo di caricamento del modulo
Caricare moduli pesanti a livello globale aumenta il tempo di avvio a freddo perché ogni importazione viene eseguita prima della prima invocazione. Usa importazioni dinamiche per posticipare il caricamento finché il modulo non è effettivamente necessario:
async function processData(data: any) {
const { heavyFunction } = await import('heavy-library');
return heavyFunction(data);
}
Utilizzare strutture dati efficienti
Quando devi cercare oggetti frequentemente, usa un Map invece di Array.find().
Map fornisce ricerche O(1) rispetto a O(n) per l'analisi della matrice:
const userMap = new Map();
const users = await fetchUsers();
users.forEach(user => userMap.set(user.id, user));
const user = userMap.get(userId); // O(1) lookup
Monitoraggio e profilazione
Usalo context.log() per monitorare i tempi di esecuzione all'interno del tuo gestore di funzioni. Per metriche personalizzate e tracciamento delle dipendenze, usa direttamente l'SDK di Application Insights Node.js . Per ulteriori informazioni, vedere Monitorare i dati personalizzati.
Il seguente esempio registra la durata dell'esecuzione e l'uso della memoria:
import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
async function trackedFunction(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
const startTime = Date.now();
const memoryBefore = process.memoryUsage().heapUsed;
try {
const result = await processData(request);
const duration = Date.now() - startTime;
const memoryDelta = (process.memoryUsage().heapUsed - memoryBefore) / 1024 / 1024;
context.log(`Execution time: ${duration}ms, Memory delta: ${memoryDelta.toFixed(2)}MB`);
return { jsonBody: result };
} catch (error) {
context.error(`Function failed after ${Date.now() - startTime}ms`, error);
throw error;
}
}
app.http('trackedFunction', {
methods: ['GET', 'POST'],
handler: trackedFunction
});
Seguendo queste raccomandazioni, puoi migliorare significativamente le prestazioni e la scalabilità delle tue Node.js Funzioni di Azure applicazioni.