Spec-Zone.ru › Express

Рекомендации для production: производительность и надежность

В этой статье рассматриваются рекомендации по производительности и надежности приложений Express, развернутых в production-среде.

Эта тема явно относится к сфере «DevOps», объединяющей традиционную разработку и эксплуатацию. Поэтому информация разделена на две части:

  • Что нужно сделать в коде (часть разработки):
    • Использовать сжатие gzip
    • Не использовать синхронные функции
    • Правильно вести журналирование
    • Правильно обрабатывать исключения
  • Что нужно сделать в среде / конфигурации (часть эксплуатации):
    • Установить NODE_ENV в значение «production»
    • Обеспечить автоматический перезапуск приложения
    • Запустить приложение в кластере
    • Кэшировать результаты запросов
    • Использовать балансировщик нагрузки
    • Использовать обратный прокси-сервер

Что нужно сделать в коде

Вот что можно сделать в коде, чтобы повысить производительность приложения:

  • Использовать сжатие gzip
  • Не использовать синхронные функции
  • Правильно вести журналирование
  • Правильно обрабатывать исключения

Использовать сжатие gzip

Сжатие с помощью gzip значительно уменьшает размер тела ответа и тем самым повышает скорость веб-приложения. Используйте middleware compression для сжатия gzip в приложении Express. Например:

const compression = require('compression')
const express = require('express')
const app = express()

app.use(compression())

Для веб-сайта с высокой нагрузкой в production-среде лучше всего реализовать сжатие на уровне обратного прокси-сервера (см. раздел Использовать обратный прокси-сервер). В этом случае middleware compression использовать не нужно. Подробные сведения о включении сжатия gzip в Nginx см. в разделе Модуль ngx_http_gzip_module документации Nginx.

Не использовать синхронные функции

Синхронные функции и методы блокируют выполняющийся процесс до момента своего завершения. Отдельный вызов синхронной функции может выполняться всего несколько микросекунд или миллисекунд, однако на веб-сайтах с высокой нагрузкой такие вызовы накапливаются и снижают производительность приложения. Не используйте их в production-среде.

Хотя Node и многие модули предоставляют синхронные и асинхронные версии функций, в production-среде всегда используйте асинхронную версию. Единственный случай, когда использование синхронной функции может быть оправдано, — первоначальный запуск.

Можно использовать флаг командной строки --trace-sync-io, чтобы выводить предупреждение и трассировку стека при каждом использовании приложением синхронного API. Разумеется, в production-среде этого делать не следует; используйте этот флаг, чтобы убедиться, что код готов к production-среде. Дополнительную информацию см. в документации по параметрам командной строки Node.

Правильно вести журналирование

Как правило, есть две причины вести журналирование в приложении: отладка и запись активности приложения (по сути, всего остального). В процессе разработки обычно используют console.log() или console.error() для вывода сообщений журнала в терминал. Однако эти функции выполняются синхронно, если вывод направлен в терминал или файл, поэтому они не подходят для production-среды, если только вы не передаете вывод другой программе.

Для отладки

Если вы ведете журналирование для отладки, используйте вместо console.log() специальный модуль отладки, например debug. Этот модуль позволяет использовать переменную окружения DEBUG, чтобы управлять тем, какие отладочные сообщения отправляются в console.error(), если отправляются вообще. Чтобы приложение оставалось полностью асинхронным, вывод console.error() также следует передавать другой программе. Но ведь в production-среде вы не собираетесь заниматься отладкой, верно?

Для записи активности приложения

Если вы записываете активность приложения (например, отслеживаете трафик или вызовы API), используйте вместо console.log() библиотеку журналирования, например Pino — это самый быстрый и эффективный из доступных вариантов.

Правильно обрабатывать исключения

Приложения Node аварийно завершают работу при возникновении необработанного исключения. Если не обрабатывать исключения и не принимать соответствующие меры, приложение Express завершит работу и станет недоступным. Если следовать приведенным ниже рекомендациям из раздела Обеспечить автоматический перезапуск приложения, оно восстановится после сбоя. К счастью, приложения Express обычно запускаются быстро. Тем не менее лучше не допускать сбоев, а для этого необходимо правильно обрабатывать исключения.

Чтобы убедиться, что вы обрабатываете все исключения, используйте следующие методы:

  • Использовать try-catch
  • Использовать промисы

Прежде чем перейти к этим темам, следует разобраться в основах обработки ошибок в Node/Express: использовании обратных вызовов, в которых ошибка передается первым аргументом, и передаче ошибок через middleware. В Node для передачи ошибок из асинхронных функций используется соглашение «ошибка первым аргументом»: первый параметр функции обратного вызова — объект ошибки, а следующие параметры содержат данные результата. Чтобы указать на отсутствие ошибки, передайте null в качестве первого параметра. Чтобы корректно обработать ошибку, функция обратного вызова должна соответствовать этому соглашению. В Express рекомендуется использовать функцию next() для передачи ошибок по цепочке middleware.

Дополнительные сведения об основах обработки ошибок см. в следующих материалах:

  • Обработка ошибок в Node.js

Использовать try-catch

Try-catch — это конструкция языка JavaScript, с помощью которой можно перехватывать исключения в синхронном коде. Например, используйте try-catch для обработки ошибок разбора JSON, как показано ниже.

Вот пример использования try-catch для обработки исключения, которое может привести к сбою процесса. Эта функция middleware принимает параметр поля запроса с именем “params”, представляющий собой объект 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')
    }
  })
})

Однако try-catch работает только с синхронным кодом. Поскольку платформа Node в основном асинхронная (особенно в production-среде), try-catch не перехватит многие исключения.

Использовать промисы

Если ошибка возникает в функции async или внутри функции async ожидается отклоненный промис, эти ошибки будут переданы обработчику ошибок так же, как при вызове 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 })
})

Кроме того, для middleware можно использовать асинхронные функции, и маршрутизатор обработает ошибки, если промис будет отклонен. Например:

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

Рекомендуется обрабатывать ошибки как можно ближе к месту их возникновения. Поэтому, хотя теперь ошибка обрабатывается маршрутизатором, лучше перехватить ее в middleware и обработать, не полагаясь на отдельное middleware для обработки ошибок.

Чего не следует делать

Не следует подписываться на событие uncaughtException, которое генерируется, когда исключение доходит до цикла событий. Добавление обработчика события uncaughtException изменит стандартное поведение процесса, в котором возникло исключение: процесс продолжит работу, несмотря на исключение. Это может показаться хорошим способом предотвратить сбой приложения, однако продолжать его работу после необработанного исключения опасно и не рекомендуется, поскольку состояние процесса становится ненадежным и непредсказуемым.

Кроме того, использование uncaughtException официально признано грубым решением. Поэтому подписываться на uncaughtException — плохая идея. Именно поэтому мы рекомендуем использовать несколько процессов и супервизоры: сбой с последующим перезапуском часто является самым надежным способом восстановления после ошибки.

Мы также не рекомендуем использовать домены. Они, как правило, не решают проблему и относятся к устаревшим модулям.

Что нужно сделать в среде / конфигурации

Вот что можно сделать в системной среде, чтобы повысить производительность приложения:

  • Установить NODE_ENV в значение «production»
  • Обеспечить автоматический перезапуск приложения
  • Запустить приложение в кластере
  • Кэшировать результаты запросов
  • Использовать балансировщик нагрузки
  • Использовать обратный прокси-сервер

Установить NODE_ENV в значение «production»

Переменная окружения NODE_ENV указывает среду, в которой работает приложение (обычно это среда разработки или production-среда). Один из самых простых способов повысить производительность — установить для NODE_ENV значение production.

Установка для NODE_ENV значения «production» заставляет Express:

  • Кэшировать шаблоны представлений.
  • Кэшировать CSS-файлы, сгенерированные из расширений CSS.
  • Формировать менее подробные сообщения об ошибках.

Тесты показывают, что одно это может повысить производительность приложения в три раза!

Если необходимо написать код, зависящий от среды, можно проверить значение NODE_ENV с помощью process.env.NODE_ENV. Учтите, что проверка значения любой переменной окружения снижает производительность, поэтому делать это следует как можно реже.

В процессе разработки переменные окружения обычно задают в интерактивной оболочке, например с помощью export или файла .bash_profile. Но, как правило, на production-сервере так делать не следует; вместо этого используйте систему инициализации ОС (systemd). В следующем разделе подробнее рассказывается об использовании систем инициализации в целом, но установка NODE_ENV настолько важна для производительности (и так проста), что мы отдельно упоминаем ее здесь.

При использовании systemd задайте директиву Environment в файле юнита. Например:

# /etc/systemd/system/myservice.service
Environment=NODE_ENV=production

Дополнительную информацию см. в разделе Использование переменных окружения в юнитах systemd.

Обеспечить автоматический перезапуск приложения

В production-среде приложение ни в коем случае не должно быть недоступно. Это значит, что нужно обеспечить его перезапуск как при сбое приложения, так и при сбое самого сервера. Хотя вы и надеетесь, что ни одно из этих событий не произойдет, необходимо быть готовым к обоим случаям:

  • Используйте менеджер процессов, чтобы перезапускать приложение (и Node) при сбое.
  • Используйте систему инициализации ОС, чтобы перезапускать менеджер процессов после сбоя ОС. Систему инициализации можно использовать и без менеджера процессов.

Приложения Node аварийно завершают работу при возникновении необработанного исключения. Прежде всего необходимо убедиться, что приложение хорошо протестировано и обрабатывает все исключения (подробности см. в разделе Правильно обрабатывать исключения). Но на случай сбоя следует предусмотреть механизм, который будет автоматически перезапускать приложение.

Использовать менеджер процессов

В процессе разработки приложение запускалось из командной строки простой командой node server.js или похожей. Но делать так в production-среде — верный путь к катастрофе. Если приложение завершит работу, оно будет недоступно, пока вы не перезапустите его вручную. Чтобы приложение перезапускалось после сбоя, используйте менеджер процессов. Менеджер процессов — это «контейнер» для приложений, который упрощает развертывание, обеспечивает высокую доступность и позволяет управлять приложением во время выполнения.

Помимо перезапуска приложения после сбоя, менеджер процессов позволяет:

  • Получать данные о производительности во время выполнения и потреблении ресурсов.
  • Динамически изменять настройки для повышения производительности.
  • Управлять кластеризацией (pm2).

Раньше было популярно использовать менеджер процессов Node.js, например PM2. Если вы хотите поступить так же, ознакомьтесь с документацией к нему. Однако для управления процессами мы рекомендуем использовать систему инициализации.

Использовать систему инициализации

Следующий уровень надежности — обеспечить перезапуск приложения при перезапуске сервера. Системы могут выходить из строя по разным причинам. Чтобы приложение перезапускалось после сбоя сервера, используйте встроенную в ОС систему инициализации. Наиболее распространенная сегодня система инициализации — systemd.

Есть два способа использовать системы инициализации с приложением Express:

  • Запустить приложение в менеджере процессов, а затем установить менеджер процессов как службу системы инициализации. Менеджер процессов будет перезапускать приложение после его сбоя, а система инициализации будет перезапускать менеджер процессов после перезапуска ОС. Это рекомендуемый подход.
  • Запустить приложение (и Node) непосредственно с помощью системы инициализации. Это несколько проще, но вы не получите дополнительных преимуществ от использования менеджера процессов.
Systemd

Systemd — это диспетчер систем и служб Linux. Большинство крупных дистрибутивов Linux выбрали systemd в качестве системы инициализации по умолчанию.

Файл конфигурации службы systemd называется файлом юнита; его имя заканчивается на .service. Ниже приведен пример файла юнита для непосредственного управления приложением Node. Замените значения в <angle brackets> на соответствующие для вашей системы и приложения:

[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

Дополнительную информацию о systemd см. в справочнике systemd (странице руководства).

Запустить приложение в кластере

В многоядерной системе можно многократно повысить производительность приложения Node, запустив кластер процессов. Кластер запускает несколько экземпляров приложения — в идеале по одному на каждое ядро процессора, — распределяя нагрузку и задачи между экземплярами.

Balancing between application instances using the cluster API

ВАЖНО: поскольку экземпляры приложения работают как отдельные процессы, они не используют общую область памяти. То есть объекты принадлежат отдельным экземплярам приложения. Поэтому в коде приложения нельзя хранить состояние. Однако для хранения данных и состояния, связанных с сеансами, можно использовать хранилище в памяти, например Redis. Это ограничение относится практически ко всем способам горизонтального масштабирования — как к кластеризации с несколькими процессами, так и к использованию нескольких физических серверов.

В кластерных приложениях рабочие процессы могут завершаться по отдельности, не затрагивая остальные процессы. Помимо преимуществ в производительности, изоляция сбоев — еще одна причина запускать кластер процессов приложения. При сбое рабочего процесса обязательно зарегистрируйте это событие и создайте новый процесс с помощью cluster.fork().

Использование модуля cluster в Node

Кластеризация возможна благодаря модулю cluster в Node. Он позволяет главному процессу создавать рабочие процессы и распределять между ними входящие подключения.

Использование PM2

Если вы развертываете приложение с помощью PM2, кластеризацию можно использовать без изменения кода приложения. Сначала убедитесь, что приложение не хранит состояние, то есть в процессе не сохраняются локальные данные (например, сеансы, подключения WebSocket и тому подобное).

При запуске приложения с помощью PM2 можно включить режим кластера и запустить приложение в кластере с выбранным количеством экземпляров, например с количеством, равным числу доступных процессоров на компьютере. Количество процессов в кластере можно вручную изменить с помощью инструмента командной строки pm2, не останавливая приложение.

Чтобы включить режим кластера, запустите приложение следующим образом:

# 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

Это также можно настроить в файле процессов PM2 (ecosystem.config.js или аналогичном), установив exec_mode в значение cluster, а instances — в количество запускаемых рабочих процессов.

После запуска приложение можно масштабировать следующим образом:

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

Дополнительную информацию о кластеризации с PM2 см. в разделе Режим кластера документации PM2.

Кэшировать результаты запросов

Еще один способ повысить производительность в production-среде — кэшировать результаты запросов, чтобы приложению не приходилось повторять операции для обработки одинаковых запросов.

Используйте сервер кэширования, например Varnish или Nginx (см. также Кэширование в Nginx), чтобы значительно повысить скорость и производительность приложения.

Использовать балансировщик нагрузки

Как бы хорошо ни было оптимизировано приложение, один его экземпляр может обрабатывать лишь ограниченный объем нагрузки и трафика. Один из способов масштабирования приложения — запустить несколько его экземпляров и распределять трафик с помощью балансировщика нагрузки. Настройка балансировщика может повысить производительность и скорость приложения, а также обеспечить масштабирование, недоступное при использовании одного экземпляра.

Балансировщик нагрузки обычно представляет собой обратный прокси-сервер, распределяющий трафик между несколькими экземплярами приложения и серверами. Балансировщик нагрузки для приложения можно легко настроить с помощью Nginx или HAProxy.

При балансировке нагрузки может потребоваться обеспечить подключение запросов, связанных с определенным идентификатором сеанса, к процессу, который их инициировал. Это называется привязкой сеанса или липкими сеансами. Эту проблему можно решить, следуя приведенной выше рекомендации использовать хранилище данных, например Redis, для хранения данных сеансов (в зависимости от приложения). Обсуждение этой темы см. в разделе Использование нескольких узлов.

Использовать обратный прокси-сервер

Обратный прокси-сервер располагается перед веб-приложением и выполняет вспомогательные операции с запросами, помимо их перенаправления в приложение. Он может обрабатывать страницы ошибок, сжатие и кэширование, обслуживать файлы, балансировать нагрузку и выполнять другие задачи.

Передача обратному прокси-серверу задач, для которых не требуется знание состояния приложения, освобождает Express для выполнения специализированных задач приложения. Поэтому в production-среде рекомендуется запускать Express за обратным прокси-сервером, например Nginx или HAProxy.

Изменить эту страницу

© 2017 StrongLoop, IBM, and other expressjs.com contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v3.0.
https://expressjs.com/en/advanced/best-practice-performance.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API