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 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

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”

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:

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:

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ê:

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:

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.

Balanceamento entre instâncias de aplicação usando a API de cluster

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