Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Ao desenvolver para Azure Functions usando TypeScript e Node.js, você precisa entender como suas funções funcionam e como esse desempenho afeta a escala do seu aplicativo de funções. Esse entendimento é ainda mais importante ao projetar aplicativos de alto desempenho. Os principais fatores a considerar ao projetar, escrever e configurar seus aplicativos de função são a escalabilidade horizontal, as configurações de desempenho da taxa de transferência e as otimizações específicas do Node.js.
Escalabilidade horizontal
Por padrão, Azure Functions monitora automaticamente a carga em seu aplicativo e cria mais instâncias de host para Node.js conforme necessário. O Azure Functions usa limiares embutidos para diferentes tipos de gatilho para decidir quando adicionar instâncias, como a idade das mensagens e o tamanho da fila para QueueTrigger. Você não pode configurar esses limites. Para obter mais informações, consulte o dimensionamento controlado por eventos no Azure Functions.
Para melhorar a escalabilidade do seu aplicativo de funções do Node.js, siga esta abordagem na ordem indicada:
- Comece pelas configurações padrão e otimize seu código primeiro usando padrões de E/S assíncronos.
- Aumente a escala (adicione mais instâncias) antes de aumentar a contagem de processos de trabalho.
- Aumente
FUNCTIONS_WORKER_PROCESS_COUNTsomente após os testes de carga mostrarem saturação da CPU em cada instância. - Para cargas de trabalho limitadas por E/S, mantenha a contagem de trabalhadores conservadora e foque em reduzir chamadas bloqueantes e latência de dependência.
- Reteste após cada alteração e monitore a taxa de transferência, latência e erro para validar melhorias.
Melhorando o desempenho da taxa de transferência
As configurações padrão são adequadas para a maioria das aplicações do Azure Functions. No entanto, você pode melhorar o desempenho da taxa de transferência de seus aplicativos empregando configurações com base em seu perfil de carga de trabalho. A primeira etapa é entender o tipo de carga de trabalho que você está executando.
| Tipo de carga de trabalho | Características do aplicativo de funções | Exemplos |
|---|---|---|
| Limite de E/S | • O aplicativo precisa lidar com muitas invocações simultâneas. • O aplicativo processa um grande número de eventos de E/S, como chamadas de rede e leitura/gravações em disco. |
• APIs da Web • Operações de banco de dados • Processamento de arquivos |
| Limitado pela CPU | • O aplicativo realiza cálculos de longa duração, como processamento de imagens. • O aplicativo realiza transformação de dados ou cálculos complexos. |
• Processamento de dados • Transformação de conteúdo • Execução de algoritmos |
Como as cargas de trabalho de função do mundo real geralmente são uma combinação de E/S e limite de CPU, crie o perfil do aplicativo em cargas de produção realistas. As seções a seguir cobrem otimizações específicas:
- Padrões de async/await
- Processos de trabalho para várias linguagens
- Otimização de loops de eventos
- Gerenciamento de memória
- Otimização de código
Padrões assíncronos ou de espera
Node.js é construído sobre um modelo de E/S orientado a 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 a E/S, você pode melhorar significativamente o desempenho usando padrões async/await adequados:
-
Use
Promise.all()para operações paralelas: Quando você tem múltiplas operações assíncronas independentes, execute-as em paralelo em vez de sequencialmente. - Evite bloquear o loop de eventos: Não use operações síncronas em funções assíncronas.
- Use streaming para grandes dados: Processe grandes conjuntos de dados usando fluxos em vez de carregar tudo na memória.
- Implemente o tratamento adequado de erros: Use blocos try/catch e trate as rejeições de Promise.
O exemplo a seguir 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();
}
Usar vários processos de trabalho de linguagem
Por padrão, cada instância de host do Functions tem um processo de trabalho de linguagem único. Você pode aumentar o número de processos de trabalho por host (até 10) usando a configuração do FUNCTIONS_WORKER_PROCESS_COUNT aplicativo. Azure Functions tenta distribuir uniformemente invocações de função simultânea entre esses trabalhadores.
Warning
Use a configuração FUNCTIONS_WORKER_PROCESS_COUNT com cautela. 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 você usar essa configuração, executar de um arquivo de pacote pode compensar essas desvantagens.
Para aplicativos com limite de CPU, defina o número de trabalhos de linguagem como igual ou superior ao número de núcleos disponíveis por cada aplicativo de funções. Para saber mais, consulte SKUs de instância disponíveis.
Aplicativos vinculados a I/O também podem se beneficiar ao aumentar o número de processos de trabalho além do número de núcleos disponíveis. Tenha em mente que definir o número de trabalhadores muito alto pode afetar o desempenho geral devido ao aumento do número de opções de contexto necessárias.
Otimização de loops de eventos
Node.js utiliza um loop de eventos de thread única para lidar com operações de E/S. Otimizar como seu código interage com o ciclo de eventos é crucial para o desempenho.
Mantenha o event loop desobstruído
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 ceda o controle entre os 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));
}
}
Monitoramento do atraso do loop de eventos
Use o padrão a seguir para medir quanto tempo as tarefas esperam na fila do loop 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`);
});
}
Gerenciamento de memória
O uso eficiente da memória é fundamental para o desempenho do Azure Functions, especialmente em relação a cold starts e escalabilidade. Siga estas orientações gerais ao otimizar o uso da sua memória:
- Minimize o uso de variáveis globais: Objetos grandes no escopo global aumentam o tempo de inicialização a frio.
- Use o pooling de conexões: reutilize conexões de banco de dados e clientes HTTP.
- Limpar recursos: Feche corretamente as conexões e limpe 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 de código
Você pode otimizar ainda mais seu código agrupando seu app, usando uma estratégia de carregamento preguiçosa e otimizando suas estruturas de dados.
Agrupe suas dependências
Use ferramentas de agrupamento como esbuild, webpack ou rollup para combinar seu aplicativo de funções em menos arquivos. O agrupamento reduz a quantidade de arquivos que o tempo de execução carrega durante a partida a frio, o que melhora diretamente o tempo de inicialização, especialmente para aplicativos com grandes árvores de dependência. Para ver mais informações, consulte Pacote antes da implantação.
Minimizar o tempo de carregamento do módulo
Carregar módulos pesados no escopo global aumenta o tempo de partida 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);
}
Use estruturas de dados eficientes
Quando você precisar pesquisar itens com frequência, use um Map em vez de Array.find(). A Map fornece buscas O(1), em comparação com O(n) na varredura de matrizes:
const userMap = new Map();
const users = await fetchUsers();
users.forEach(user => userMap.set(user.id, user));
const user = userMap.get(userId); // O(1) lookup
Monitoramento e perfilamento
Use context.log() para acompanhar o tempo de execução dentro do seu gerenciador de funções. Para métricas personalizadas e acompanhamento de dependências, use o SDK do Application Insights Node.js diretamente. Para mais informações, veja Rastrear dados personalizados.
O exemplo a seguir registra 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 essas recomendações, você pode melhorar significativamente o desempenho e a escalabilidade das suas Node.js Azure Functions aplicações.
Artigos relacionados
- Azure Functions guia de referência para desenvolvedores de aplicativos Node.js
- Opções de build do TypeScript
- dimensionamento baseado em eventos no Azure Functions
- Monitor Azure Functions