Esta tradução fornecida pelo StrongLoop / IBM.

Este documento pode estar desatualizado em relação à documentação em Inglês. Para obter as atualizações mais recentes, consulte a documentação em Inglês.

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.

You can use the --trace-sync-io command-line flag to print a warning and a stack trace whenever your application uses a synchronous API. 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. But these functions are synchronous when the destination is a terminal or a file, so they are not suitable for production, unless you pipe the output to another program.

For debugging

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

If you’re logging app activity (for example, tracking traffic or API calls), instead of using console.log(), use a logging library like Pino, which is the fastest and most efficient option available.

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:

Use 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

When an error is thrown in an async function or a rejected promise is awaited inside an async function, those errors will be passed to the error handler as if calling 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 })
})

Also, you can use asynchronous functions for your middleware, and the router will handle errors if the promise fails, for example:

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.
})

Best practice is to handle errors as close to the site as possible. So while this is now handled in the router, it’s best to catch the error in the middleware and handle it without relying on separate error-handling middleware.

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.

Things to do in your environment / setup

{#in-environment}

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). One of the simplest things you can do to improve performance is to set NODE_ENV to production.

Configurando NODE_ENV para “produção” faz com que o Express:

Tests indicate that just doing this can improve app performance by a factor of three!

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. But in general, you shouldn’t do that on a production server; instead, use your OS’s init system (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

For more information, see Using Environment Variables In systemd Units.

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

Historically, it was popular to use a Node.js process manager like PM2. See their documentation if you wish to do this. However, we recommend using your init system for process management.

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. The main init system in use today is 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): Replace the values enclosed in <angle brackets> for your system and app:

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

Balancing between application instances using the cluster API

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

Clustering is made possible with Node’s cluster module. Isto permite que um processo principal faça o spawn de processos de trabalho e distribua conexões recebidas entre os trabalhadores.

Using PM2

If you deploy your application with PM2, then you can take advantage of clustering without modifying your application code. You should ensure your application is stateless first, meaning no local data is stored in the process (such as sessions, websocket connections and the like).

When running an application with PM2, you can enable cluster mode to run it in a cluster with a number of instances of your choosing, such as the matching the number of available CPUs on the machine. You can manually change the number of processes in the cluster using the pm2 command line tool without stopping the app.

To enable cluster mode, start your application like so:

# 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

This can also be configured within a PM2 process file (ecosystem.config.js or similar) by setting exec_mode to cluster and instances to the number of workers to start.

Once running, the application can be scaled like so:

# Add 3 more workers
$ pm2 scale my-app +3
# Scale to a specific number of workers
$ pm2 scale my-app 2

For more information on clustering with PM2, see Cluster Mode in the PM2 documentation.

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 a caching server like Varnish or Nginx (see also Nginx Caching) to greatly improve the speed and performance of your app.

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. You can easily set up a load balancer for your app by using Nginx or 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. For this reason, it is recommended to run Express behind a reverse proxy like Nginx or HAProxy in production.

Edit this page