Melhores Práticas de Produção: desempenho e confiabilidade
Este artigo discute as melhores práticas de desempenho e de confiabilidade para aplicativos Express implementados para produção.
Este tópico se enquadra claramente no mundo de “devops”, abordando o desenvolvimento tradicional e as operações. Assim, as informações são divididas em duas partes:
- Itens a fazer no seu código (a parte do dev).
- Use a compactação gzip
- Não use funções síncronas
- Faça o registro de logs corretamente
- Tratar exceções corretamente
- Itens a fazer no seu ambiente / configuração (a parte de ops).
- Configure o NODE_ENV para “produção”
- Executar o seu aplicativo (e Node) diretamente com o sistema de inicialização. Isto é de certa forma mais simples, mas você não obtém as vantagens adicionais do uso de um gerenciador de processos.
- Execute seu aplicativo em um cluster
- Use um balanceador de carga
- Use um proxy reverso
Itens a fazer no seu código
A seguir serão apresentados alguns itens que podem ser feitos no seu código para melhorar o desempenho dos aplicativos:
- Use a compactação gzip
- Não use funções síncronas
- Faça o registro de logs corretamente
- Tratar exceções corretamente
Use a compactação gzip
A compactação Gzip pode diminuir bastante o tamanho do corpo de resposta e assim aumentar a velocidade de um aplicativo da web. Use o middleware compression para fazer a compactação gzip no seu aplicativo do Express. Por exemplo:
const compression = require('compression')
const express = require('express')
const app = express()
app.use(compression())
Para um website com tráfego intenso na produção, a melhor maneira de colocar a compactação em prática, é implementá-la em um nível de proxy reverso (consulte Use um proxy reverso). Neste caso, não é necessário usar o middleware de compactação. Para obter detalhes sobre a ativação da compactação gzip no Nginx, consulte o Módulo ngx_http_gzip_module na documentação do Nginx.
Não use funções síncronas
Funções e métodos síncronos impedem o avanço da execução do processo até que eles retornem. Uma única chamada a uma função síncrona pode retornar em poucos microssegundos ou milissegundos, entretanto, em websites com tráfego intenso, essas chamadas se somam e reduzem o desempenho do aplicativo. Evite o uso delas na produção.
Apesar de o Node e muitos módulos fornecerem versões síncronas e assíncronas de suas funções, sempre use as versões assíncronas na produção. O único momento em que o uso de uma função síncrona pode ser justificado é na primeira inicialização.
Se estiver usando o Node.js + ou o .+, é possível usar a sinalização --trace-sync-io da linha de comandos para imprimir um aviso e um rastreio de pilha sempre que o seu aplicativo usar uma API síncrona. Obviamente, não seria desejado usar isto na produção, mas sim antes, para garantir que seu código está pronto para produção. Consulte a Atualização
semanal para o io.js 2.1.0 para obter mais informações.
Lide com exceções adequadamente
Em geral, existem duas razões para registrar logs em seu aplicativo: Para depuração e para registro de logs de atividade do aplicativo (essencialmente, todo o resto). Usar
o console.log() ou o console.err() para imprimir mensagens de log no
terminal é uma prática comum em desenvolvimento. Mas essas
funções são síncronas quando o destino é um terminal ou um arquivo, portanto elas não são adequadas para produção, a não ser que
a saída seja canalizada para outro programa.
Para depuração
Se estiver registrando logs com o propósito de depuração, então ao invés de usar o console.log(), use um módulo
especial para depuração como o debug. Este
módulo permite que seja usada a variável de ambiente DEBUG para controlar quais mensagens de depuração são enviadas para o
console.err(), se houver. Para manter o seu aplicativo puramente assíncrono, você deverá canalizar o
console.err() para outro programa. Mas nesse ponto, você não fará a depuração na produção, não é?
Para atividade do aplicativo
Se estiver registrando logs de atividade do aplicativo (por
exemplo, rastreamento de tráfico ou chamadas de API), ao invés de
usar o console.log(), use uma biblioteca de
registro de logs como Winston ou Bunyan.
Lide com exceções adequadamente
Aplicativos do Node caem ao encontrarem uma exceção não capturada. O não tratamento de exceções e a não tomada das ações apropriadas irão fazer com que o seu aplicativo do Express caia e fique off-line. Se seguir os conselhos em Assegurando que o seu aplicativo reinicie automaticamente abaixo, então seu aplicativo se recuperará de uma queda. Felizmente, aplicativos Express tipicamente possuem um tempo curto de inicialização. Contudo, é desejável evitar quedas em primeiro lugar e, para fazer isso, é necessário tratar exceções adequadamente.
Para garantir que está tratando todas as exceções, use as seguintes técnicas:
Antes de se aprofundar nestes tópicos, você deveria ter um entendimento básico de manipulação de erros do Node/Express: usando retornos de chamada erros-first, e propagação de erros no middleware. O Node usa uma convenção “retorno de chamada erros-first” para retorno de erros de funções assíncronas, onde o primeiro parâmetro para a função de retorno de chamada é o objeto de erro, seguido dos dados de resultado nos parâmetros subsequentes. Para indicar que não ocorreram erros, passe null como o primeiro parâmetro. A função de retorno de chamada deve correspondentemente seguir a convenção de retorno de chamada erros-first para tratar o erro de forma significativa. E no Express, a melhor prática é usar a função next() para propagar erros pela cadeia de middlewares.
Para obter mais informações sobre os fundamentos de manipulação de erros, consulte:
Usar try-catch
Try-catch é uma construção da linguagem JavaScript que pode ser usada para capturar exceções em um código síncrono. Use try-catch, por exemplo, para tratar erros de análise sintática de JSON como mostrado abaixo.
Aqui está um exemplo de uso de try-catch para tratar uma potencial exceção causadora de queda de processo. Esta função middleware aceita um parâmetro de campo de consulta chamado “params” que é um objeto JSON.
app.get('/search', (req, res) => {
// Simulating async operation
setImmediate(() => {
const jsonStr = req.query.params
try {
const jsonObj = JSON.parse(jsonStr)
res.send('Success')
} catch (e) {
res.status(400).send('Invalid JSON string')
}
})
})
Entretanto, o try-catch funciona apenas para códigos síncronos. Como a plataforma Node é a princípio assíncrona (particularmente em um ambiente de produção), o try-catch deixará de capturar muitas exceções.
Use promessas
Quando um erro é lançado em uma função async ou uma promessa rejeitada é aguardada dentro de uma função async, esses erros serão passados para o manipulador de erros como se chamando next(err)
app.get('/', async (req, res, next) => {
const data = await userData() // If this promise fails, it will automatically call `next(err)` to handle the error.
res.send(data)
})
app.use((err, req, res, next) => {
res.status(err.status ?? 500).send({ error: err.message })
})
Além disso, você pode usar funções assíncronas para o seu middleware, e o roteador irá lidar com erros se a promessa falhar, por exemplo:
app.use(async (req, res, next) => {
req.locals.user = await getUser(req)
next() // This will be called if the promise does not throw an error.
})
A melhor prática é lidar com os erros o mais próximo possível do site. Então enquanto isso é manipulado no roteador, É melhor encontrar o erro no middleware e lidar com ele sem depender de um middleware separado para manipular erros.
O que não fazer
Uma coisa que não deveria fazer é escutar a eventos uncaughtException, emitidos quando uma exceção
emerge regressando ao loop de eventos. Incluir um listener de eventos para uncaughtException irá mudar o comportamento
padrão do processo que está encontrando uma exceção; o processo irá continuar a execução apesar da exceção. Essa pode parecer como uma boa maneira de prevenir que o seu
aplicativo caia, mas continuar a execução do aplicativo após uma
exceção não capturada é uma prática perigosa e não é recomendada, porque o estado do processo se torna não confiável e imprevisível.
Adicionalmente, usar o uncaughtException é oficialmente reconhecido como grosseiro
e existe uma proposta
de removê-lo do núcleo. Portando escutar por um uncaughtException é simplesmente uma má ideia. É
por isso que recomendamos coisas como múltiplos processos e
supervisores: o processo de queda e reinicialização é frequentemente a
forma mais confiável de se recuperar de um erro.
Também não recomendamos o uso de domínios. Ele geralmente não resolve o problema e é um módulo descontinuado.
Coisa a se fazer no seu ambiente / configuração
A seguir serão apresentados alguns itens que podem ser feitos no seu ambiente de sistema para melhorar o desempenho dos seus aplicativos:
- Configure o NODE_ENV para “produção”
- Executar o seu aplicativo (e Node) diretamente com o sistema de inicialização. Isto é de certa forma mais simples, mas você não obtém as vantagens adicionais do uso de um gerenciador de processos.
- Execute seu aplicativo em um cluster
- Use um balanceador de carga
- Use um proxy reverso
Configure o NODE_ENV para “produção”
A variável de ambiente NODE_ENV especifica o ambiente no qual um aplicativo está executando (geralmente, desenvolvimento ou produção). Uma das coisas mais simples que podem ser feitas para melhorar o desempenho é configurar NODE_ENV para “production”.
Configurando NODE_ENV para “produção” faz com que o Express:
- Armazene em Cache os modelos de visualização.
- Armazene em Cache arquivos CSS gerados a partir de extensões CSS.
- Gere menos mensagens de erro detalhadas
Testes indicam que apenas fazendo isso pode melhorar o desempenho por um fator de três!
Se precisar escrever código específico por ambiente, é possível verificar o valor de NODE_ENV com process.env.NODE_ENV. Esteja
ciente de que verificar o valor de qualquer variável de ambiente incorre em perda de desempenho, e por isso deve ser feito raramente.
Em desenvolvimento, você tipicamente configura variáveis de ambiente no seu shell interativo, por exemplo, usando o
export ou o seu arquivo .bash_profile. Mas
em geral você não deveria fazer isto em um servidor de produção; ao invés disso, use o sistema de inicialização do seu sistema
operacional (systemd). A próxima seção fornece mais detalhes sobre a utilização do seu sistema de inicialização em geral,
mas configurando NODE_ENV é tão importante para o desempenho (e fácil de fazer), que está destacado aqui.
Com o systemd, use a diretiva Environment no seu arquivo de unidade. Por exemplo:
# /etc/systemd/system/myservice.service
Environment=NODE_ENV=production
Para obter mais informações, consulte Usando Variáveis de Ambiente em Unidades systemd.
Assegure que o seu aplicativo reinicie automaticamente
Em produção, não é desejado que seu aplicativo fique off-line, nunca. Isto significa que é necessário certificar-se de que ele reinicie tanto se o aplicativo cair quanto se o próprio servidor cair. Apesar de se esperar que nenhum desses eventos ocorram, realisticamente você deve considerar ambas as eventualidades:
- Usando um gerenciador de processos para reiniciar o aplicativo (e o Node) quando ele cair.
- Usando o sistema de inicialização fornecido pelo seu sistema operacional para reiniciar o gerenciador de processos quando o sistema operacional cair. Também é possível usar o sistema de inicialização sem um gerenciador de processos.
Aplicativos do Node caem se encontrarem uma exceção não capturada. A principal coisa que precisa ser feita é assegurar que o seu aplicativo esteja bem testado e trate todas as exceções (consulte tratar exceções adequadamente para obter detalhes). Mas por segurança, posicione um mecanismo para assegurar que se e quando o seu aplicativo cair, ele irá automaticamente reiniciar.
Use um gerenciador de processos
Em desenvolvimento, você iniciou o seu aplicativo de forma simples a partir da linha de comandos com o node server.js ou
algo similar. Mas fazer isso na produção é uma receita para o desastre. Se o aplicativo cair, ele ficará off-line até ser reiniciado. Para
assegurar que o seu aplicativo reinicie se ele cair, use um gerenciador de processos. Um
gerenciador de processos é um “contêiner” para aplicativos que facilita a implementação, fornece alta disponibilidade, e permite o
gerenciamento do aplicativo em tempo real.
Em adição à reinicialização do seu aplicativo quando cai, um gerenciador de processos pode permitir que você:
- Ganhe insights sobre o desempenho em tempo de execução e o consumo de recursos.
- Modifique configurações dinamicamente para melhorar o desempenho.
- Controle de agrupamento (pm2).
Historicamente, foi popular usar um gerente de processo Node.js, como PM2. Veja a documentação deles, se você quiser fazer isso. No entanto, recomendamos a utilização de seu sistema de inicio para gerenciamento de processos.
Use um sistema de inicialização
A próxima camada de confiabilidade é para assegurar que o seu aplicativo reinicie quando o servidor reiniciar. Os sistemas podem ainda assim cair por uma variedade de razões. Para assegurar que o seu aplicativo reinicie se o servidor cair, use o sistema de inicialização integrado no seu sistema operacional. O sistema principal de iniciação em uso hoje é systemd.
Existem duas formas de usar sistemas de inicialização com o seu aplicativo Express:
- Executar o seu aplicativo em um gerenciador de processos, e instalar o gerenciador de processos com o sistema de inicialização. O gerenciador de processos irá reiniciar seu aplicativo quando o aplicativo cair, e o sistema de inicialização irá reiniciar o gerenciador de processos quando o sistema operacional reiniciar. Esta é a abordagem recomendada.
- Execute seu aplicativo (e Node) diretamente com o sistema iniciação. Isto é um pouco mais simples, mas você não obtém as vantagens adicionais de usar um gerente de processo.
Systemd
O Systemd é um sistema Linux e gerenciador de serviço. A maioria das distribuições principais do Linux adotaram o systemd como sistema de inicialização padrão.
Um arquivo de configuração de serviço do systemd é chamado
de arquivo de unidade, com um nome de arquivo
terminando em .service. Aqui está um exemplo de arquivo de unidade
para gerenciar um aplicativo Node diretamente (substitua o texto em
negrito com valores para o seu sistema e aplicativo): Substitua os valores colocados em <angle brackets> do seu sistema e aplicativo:
[Unit]
Description=<Awesome Express App>
[Service]
Type=simple
ExecStart=/usr/local/bin/node </projects/myapp/index.js>
WorkingDirectory=</projects/myapp>
User=nobody
Group=nogroup
# Environment variables:
Environment=NODE_ENV=production
# Allow many incoming connections
LimitNOFILE=infinity
# Allow core dumps for debugging
LimitCORE=infinity
StandardInput=null
StandardOutput=syslog
StandardError=syslog
Restart=always
[Install]
WantedBy=multi-user.target
Para obter mais informações sobre o systemd, consulte a referência do systemd (página do manual).
Execute seu aplicativo em um cluster
Em um sistema com múltiplos núcleos, é possível aumentar o desempenho de um aplicativo Node em muitas vezes ativando um cluster de processos. Um cluster executa múltiplas instâncias do aplicativo, idealmente uma instância em cada núcleo da CPU, assim distribuindo a carga e as tarefas entre as instâncias.

IMPORTANTE: Como as instâncias do aplicativo são executadas em processos separados, elas não compartilham o mesmo espaço de memória. Isto é, os objetos são locais para cada instância do aplicativo. Portanto, não é possível manter o estado no código do aplicativo. Entretanto, é possível usar um armazenamento de dados em memória como o Redis para armazenar dados relativos à sessão e ao estado. Este alerta aplica-se a essencialmente todas as formas de escalonamento horizontal, seja a clusterização com múltiplos processos ou múltiplos servidores físicos.
Em aplicativos clusterizados, processos de trabalho podem cair individualmente sem afetar o restante dos processos. Fora as vantagens de desempenho, o isolamento de falhas é outra razão para executar um cluster de processos de aplicativos. Sempre que processo de trabalho cair, certifique-se de registrar os logs do evento e spawn um novo processo usando cluster.fork().
Usando o módulo de cluster do Node
É possível agrupar com o módulo cluster do Node. Isto permite que um processo principal faça o spawn de processos de trabalho e distribua conexões recebidas entre os trabalhadores.
Usando PM2
Se você publicar sua aplicação com PM2, então você pode aproveitar o clustering without para modificar o código da sua aplicação. Você deve garantir sua application is stateless primeiro, significando que nenhum dado local é armazenado no processo (como sessões, conexões de websocket e coisas parecidas).
Ao executar um aplicativo com PM2, você pode habilitar o cluster mode para executá-lo em um cluster com várias instâncias de sua escolha, como o número de CPUs disponíveis na máquina. Você pode alterar manualmente o número de processos no cluster usando a ferramenta de linha de comando pm2 sem parar o aplicativo.
Para ativar o modo de agrupamento, inicie seu aplicativo assim:
# Start 4 worker processes
$ pm2 start npm --name my-app -i 4 -- start
# Auto-detect number of available CPUs and start that many worker processes
$ pm2 start npm --name my-app -i max -- start
Isto também pode ser configurado dentro de um arquivo de processo PM2 (ecosystem.config.js ou similar) configurando exec_mode para cluster e instances para o número de workers para começar.
Quando em execução, o aplicativo pode ser alterado assim:
# Add 3 more workers
$ pm2 scale my-app +3
# Scale to a specific number of workers
$ pm2 scale my-app 2
Para obter mais informações sobre clustering com PM2, consulte Cluster Mode na documentação PM2.
Armazene em cache os resultados das solicitações
Outra estratégia para melhorar o desempenho na produção é armazenar em cache o resultado de solicitações, para que o seu aplicativo não repita a operação para entregar a mesma solicitação repetidamente.
Use um servidor de cache como Varnish ou Nginx (veja também Nginx Caching) para melhorar muito a velocidade e o desempenho de sua aplicação.
Use um balanceador de carga
Não importa o quão otimizado um aplicativo é, uma única instância pode manipular apenas uma quantidade limitada de carga e tráfego. Uma maneira de escalar um aplicativo é executar múltiplas instâncias do mesmo e distribuir o tráfego através de um balanceador de carga. Configurar um balanceador de carga pode melhorar o desempenho e velocidade do aplicativo, e permiti-lo escalar mais do que é possível com uma instância única.
Um balanceador de carga é geralmente um proxy reverso que orquestra o tráfego para e de múltiplas instâncias de aplicativo e servidores. Você pode facilmente configurar um balanceador de carga ‘load balancer’ para o seu aplicativo usando Nginx ou HAProxy.
Com o balanceamento de carga, você pode ter que garantir que solicitações que estão associadas com um ID de sessão em particular conectam ao processo que as originou. Isto é conhecido como afinidade de sessão, ou sessões pegajosas, e podem ser endereçadas pela sugestão acima para usar um armazenamento de dados como o Redis para os dados da sessão (dependendo do seu aplicativo). Para uma discussão, consulte por Usando múltiplos nós.
Use um proxy reverso
Um proxy reverso fica em frente a um aplicativo web e executa operações de suporte nas solicitações, fora o direcionamento de solicitações para o aplicativo. Ele pode lidar com páginas de erro, compactação, armazenamento em cache, entrega de arquivos, e balanceamento de carga entre outras coisas.
Entregar tarefas que não requerem conhecimento do estado do aplicativo para um proxy reverso libera o Express para executar tarefas especializadas de aplicativos. Por este motivo, é recomendado executar Express atrás de um proxy reverso como Nginx ou HAProxy em produção.
Edit this page