Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Ao desenvolver para Funções do Azure usando TypeScript e Node.js, precisa de compreender como as suas funções funcionam e como esse desempenho afeta a escala da sua aplicação de funções. Esta compreensão é mais importante ao desenhar aplicações de alto desempenho. Os principais fatores a considerar ao desenhar, escrever e configurar as suas aplicações funcionais são escalabilidade horizontal, configurações de desempenho de throughput e otimizações específicas Node.js.
Escalabilidade horizontal
Por padrão, o Funções do Azure monitoriza automaticamente a carga na sua aplicação e cria mais instâncias de host para o Node.js conforme necessário. O Funções do Azure utiliza limiares incorporados para diferentes tipos de gatilho para decidir quando adicionar instâncias, como a idade das mensagens e o tamanho da fila para QueueTrigger. Não podes configurar estes limiares. Para mais informações, consulte Escalabilidade orientada por eventos em Funções do Azure.
Para melhorar a escalabilidade da sua aplicação Node.js funções, siga esta abordagem por ordem:
- Começa pelas definições padrão e otimiza primeiro o teu código usando padrões de I/O assíncronos.
- Expanda horizontalmente (adicione mais instâncias) antes de aumentar o número de processos de trabalho.
- Aumentar
FUNCTIONS_WORKER_PROCESS_COUNTapenas depois de os testes de carga mostrarem saturação do processador em cada instância. - Para cargas de trabalho ligadas a I/O, mantenha a contagem de trabalhadores conservadora e concentre-se em reduzir chamadas bloqueantes e latência de dependência.
- Teste novamente após cada alteração e monitorize o débito, a latência e as taxas de erro para validar as melhorias.
Melhorando a performance do throughput
As configurações padrão são adequadas para a maioria das aplicações Funções do Azure. No entanto, pode melhorar o desempenho do rendimento das suas aplicações utilizando configurações baseadas no seu perfil de carga de trabalho. O primeiro passo é perceber o tipo de carga de trabalho que estás a ter.
| Tipo de carga de trabalho | Características da Function App | Examples |
|---|---|---|
| ligados a E/S | • A aplicação precisa de lidar com muitas invocações simultâneas. • A aplicação processa um grande número de eventos de E/S, como chamadas de rede e leituras/gravações em disco. |
• APIs Web • Operações de base de dados • Processamento de ficheiros |
| dependente da CPU | • A aplicação realiza cálculos de longa duração, como processamento de imagens. • A aplicação realiza transformação de dados ou cálculos complexos. |
• Processamento de dados • Transformação de conteúdos • Execução de algoritmos |
Como as cargas de trabalho de funções reais costumam ser uma mistura de I/O e CPU, projete a aplicação sob cargas de produção realistas. As secções seguintes abordam otimizações específicas:
- Padrões async/await
- Processos de trabalho multilingues
- Otimização de ciclos de eventos
- Gestão de memória
- Otimização do código
Padrões de 'async/await'
Node.js é construído sobre um modelo de E/S orientado por eventos e não bloqueante, o que o torna leve e eficiente para operações de I/O limitadas. Para aplicações que processam um grande número de eventos de E/S ou que estão limitadas por E/S, pode melhorar significativamente o desempenho usando padrões async/await adequados:
-
Use
Promise.all()para operações paralelas: Quando tiver múltiplas operações assíncronas independentes, execute-as em paralelo em vez de sequencialmente. - Evite bloquear o ciclo de eventos: Não use operações síncronas em funções assíncronas.
- Use streaming para grandes volumes de dados: Processe grandes conjuntos de dados usando fluxos em vez de carregar tudo na memória.
- Implemente o tratamento adequado de erros: Utilize blocos try/catch e trate as rejeições de Promise.
O exemplo seguinte demonstra operações assíncronas paralelas com 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();
}
Utilizar processos de trabalhadores em múltiplas línguas
Por predefinição, cada instância de host do Functions tem um único processo de trabalho da linguagem. Pode aumentar o número de processos de trabalho por host (até 10) usando a FUNCTIONS_WORKER_PROCESS_COUNT configuração da aplicação. Funções do Azure tenta então distribuir uniformemente as invocações simultâneas de funções entre estes trabalhadores.
Warning
Use a FUNCTIONS_WORKER_PROCESS_COUNT configuração com cuidado. Vários processos em execução na mesma instância podem levar a um comportamento imprevisível e aumentar os tempos de carregamento da função. Se usares esta configuração, executar a partir de um ficheiro de pacote pode compensar estas desvantagens.
Para aplicações limitadas pela CPU, defina o número de processos de linguagem para que seja igual ou superior ao número de núcleos disponíveis por aplicação de funções. Para saber mais, consulte SKUs de instância disponíveis.
As aplicações limitadas por operações de E/S também podem beneficiar com o aumento do número de processos de trabalho para um valor superior ao número de núcleos disponíveis. Tenha em mente que definir o número de trabalhadores demasiado alto pode afetar o desempenho global devido ao aumento do número de mudanças de contexto necessárias.
Otimização de ciclos de eventos
Node.js utiliza um ciclo de eventos de um único thread para tratar operações de E/S. Otimizar a forma como o seu código interage com o ciclo de eventos é crucial para o desempenho.
Mantenha o event loop desimpedido
Laços síncronos que realizam cálculos pesados bloqueiam o ciclo de eventos e impedem o processamento de outras invocações. Em vez disso, divida grandes conjuntos de dados em lotes e produza controlo entre lotes com 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));
}
}
Monitorizar o atraso do ciclo de eventos
Use o seguinte padrão para medir quanto tempo as tarefas esperam na fila do ciclo de eventos:
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`);
});
}
Gestão da memória
O uso eficiente da memória é fundamental para o desempenho do Funções do Azure, especialmente no que diz respeito a arranques a frio e escalabilidade. Siga estas orientações gerais ao otimizar o uso da sua memória:
- Minimizar variáveis globais: Objetos pesados em âmbito global afetam os tempos de arranque a frio.
- Utilize o agrupamento de ligações: Reutilize ligações à base de dados e clientes HTTP.
- Limpar recursos: Fechem corretamente as ligações e limpem temporizadores.
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
});
Otimização do código
Pode otimizar ainda mais o seu código agrupando a sua aplicação, usando uma estratégia de carregamento preguiçosa e otimizando as suas estruturas de dados.
Agrupe as suas dependências
Usa ferramentas de agrupamento como esbuild, webpack ou rollup para combinar a tua aplicação de funções em menos ficheiros. O empacotamento reduz o número de ficheiros que o ambiente de execução carrega durante o arranque a frio, o que melhora diretamente o tempo de inicialização, especialmente para aplicações com árvores de dependências extensas. Para mais informações, consulte Agrupamento antes da implementação.
Minimizar o tempo de carregamento do módulo
Carregar módulos pesados no âmbito global aumenta o tempo de arranque a frio porque cada importação é executada antes da primeira invocação. Use importações dinâmicas para adiar o carregamento até que o módulo seja realmente necessário:
async function processData(data: any) {
const { heavyFunction } = await import('heavy-library');
return heavyFunction(data);
}
Utilizar estruturas de dados eficientes
Quando precisar de consultar itens frequentemente, use um Map em vez de Array.find(). A Map proporciona pesquisas O(1) em comparação com O(n) para percorrer um array:
const userMap = new Map();
const users = await fetchUsers();
users.forEach(user => userMap.set(user.id, user));
const user = userMap.get(userId); // O(1) lookup
Monitorização e perfilação
Use context.log() para acompanhar o tempo de execução dentro do seu gestor de funções. Para métricas personalizadas e acompanhamento de dependências, use diretamente o SDK do Application Insights Node.js . Para mais informações, consulte Rastrear dados personalizados.
O exemplo seguinte regista a duração da execução e o uso de memória:
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
});
Ao seguir estas recomendações, pode melhorar significativamente o desempenho e a escalabilidade das suas Node.js Funções do Azure aplicações.