Рекомендации по работе в продакшене: производительность и надёжность
Обзор
В этой статье рассматриваются рекомендации по производительности и надёжности приложений Express, развернутых в продакшене.
Эта тема явно относится к области DevOps, охватывая как традиционное развитие, так и работу. Соответственно, информация разделена на две части:
- Действия, которые нужно выполнить в коде (часть dev):
- Действия, которые нужно выполнить в среде/настройках (часть ops):
Действия в коде
Вот несколько действий, которые можно выполнить в коде для повышения производительности приложения:
- Использование сжатия gzip
- Не использовать синхронные функции
- Правильная обработка логирования
- Правильная обработка исключений
Использование сжатия gzip
Сжатие gzip может значительно уменьшить размер тела ответа и, следовательно, повысить скорость веб-приложения. Используйте middleware compression для сжатия gzip в вашем приложении Express. Например:
const compression = require('compression')
const express = require('express')
const app = express()
app.use(compression())
Для веб-сайта с высоким трафиком в продакшене лучшим способом внедрения сжатия является его реализация на уровне обратного прокси (см. Использование обратного прокси-сервера). В этом случае вам не нужно использовать middleware для сжатия. Подробные сведения о включении gzip-сжатия в Nginx см. в документации Nginx в разделе Модуль ngx_http_gzip_module.
Не использовать синхронные функции
Синхронные функции и методы блокируют выполнение процесса до возврата результата. Один вызов синхронной функции может возвратить результат за несколько микросекунд или миллисекунд, но на сайтах с высоким трафиком эти вызовы суммируются и снижают производительность приложения. Избегайте их использования в продакшене.
Хотя Node и многие модули предоставляют синхронные и асинхронные версии своих функций, всегда используйте асинхронные версии в продакшене. Единственный случай, когда синхронная функция может быть оправдана, — это запуск приложения.
Если вы используете Node.js 4.0+ или io.js 2.1.0+, вы можете использовать флаг командной строки --trace-sync-io, чтобы выводить предупреждение и стек вызовов всякий раз, когда ваше приложение использует синхронный API. Конечно, вы не захотите использовать это в продакшене, а скорее для того, чтобы убедиться, что ваш код готов к продакшену. Более подробную информацию см. в документации командной строки node.
Правильная обработка логирования
В целом, есть две причины для ведения журнала из вашего приложения: для отладки и для логирования действий приложения (по существу, всё остальное). Использование console.log() или console.error() для вывода сообщений в терминал является распространённой практикой при разработке. Но эти функции синхронны, когда местом назначения является терминал или файл, поэтому они не подходят для продакшена, если вы не направите вывод в другую программу.
Для отладки
Если вы ведёте журнал для целей отладки, то вместо console.log(), используйте специальный модуль отладки, например debug. Этот модуль позволяет использовать переменную среды DEBUG для управления тем, какие сообщения отладки отправляются в console.error(), если таковые имеются. Чтобы сохранить чисто асинхронную работу приложения, вы по-прежнему хотели бы перенаправить console.error() в другую программу. Но тогда вы вряд ли будете отлаживать в продакшене, не так ли?
Для действий приложения
Если вы ведёте журнал действий приложения (например, отслеживания трафика или API-вызовов), вместо использования console.log(), используйте библиотеку ведения журнала, такую как Winston или Bunyan. Для подробного сравнения этих двух библиотек см. статью StrongLoop в блоге Сравнение Winston и Bunyan для логирования в Node.js.
Правильная обработка исключений
Приложения NodeJS завершаются при обнаружении необработанного исключения. Необработка исключений и отсутствие соответствующих действий приведут к падению вашего приложения Express и его отключению. Если вы следуете рекомендациям в разделе Автоматическое перезапуск приложения ниже, приложение восстановится после сбоя. К счастью, приложения Express обычно имеют короткий период запуска. Тем не менее, вы хотите предотвратить сбои в первую очередь, и для этого нужно правильно обрабатывать исключения.
Чтобы убедиться в обработке всех исключений, используйте следующие методы:
Прежде чем углубиться в эти темы, вам следует иметь базовое понимание обработки ошибок Node/Express: использование обратных вызовов с ошибкой в качестве первого параметра и распространение ошибок в middleware. Node использует соглашение «обратный вызов с ошибкой в качестве первого параметра» для возврата ошибок из асинхронных функций, где первым параметром функции обратного вызова является объект ошибки, а последующими параметрами — данные результата. Для указания отсутствия ошибки передайте null в качестве первого параметра. Функция обратного вызова должна соответственно следовать соглашению обратного вызова с ошибкой в качестве первого параметра, чтобы осмысленно обработать ошибку. А в Express наилучшей практикой является использование функции next() для распространения ошибок по цепочке middleware.
Дополнительную информацию об основах обработки ошибок см.:
Что не следует делать
Одного действия вы должны избегать: прослушивания события uncaughtException, которое генерируется, когда исключение возвращается обратно в цикл событий. Добавление обработчика событий для uncaughtException изменит стандартное поведение процесса, который сталкивается с исключением; процесс будет продолжать работу, несмотря на исключение. Это может показаться хорошим способом предотвратить сбой приложения, но продолжение работы приложения после необработанного исключения — опасная практика и не рекомендуется, так как состояние процесса становится ненадёжным и непредсказуемым.
Кроме того, использование uncaughtException официально считается неэффективным. Поэтому прослушивание uncaughtException — просто плохая идея. Вот почему мы рекомендуем использовать несколько процессов и надсмотрщиков: перезапуск процесса — часто наиболее надёжный способ восстановления после ошибки.
Мы также не рекомендуем использовать домены. В общем случае это не решает проблему и является устаревшим модулем.
Использование try-catch
Try-catch — это конструкция языка JavaScript, которую можно использовать для перехвата исключений в синхронном коде. Используйте try-catch, например, для обработки ошибок при парсинге JSON, как показано ниже.
Используйте инструмент, такой как JSHint или JSLint, чтобы помочь вам найти неявные исключения, такие как ошибки ссылок на неопределённые переменные.
Вот пример использования 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 в основном асинхронна (особенно в продакшене), try-catch не перехватит много исключений.
Использование промисов
Промисы будут обрабатывать любые исключения (явные и неявные) в асинхронных блоках кода, которые используют then(). Просто добавьте .catch(next) в конец цепочек промисов. Например:
app.get('/', (req, res, next) => {
// do some sync stuff
queryDb()
.then((data) => makeCsv(data)) // handle data
.then((csv) => { /* handle csv */ })
.catch(next)
})
app.use((err, req, res, next) => {
// handle error
})
Теперь все ошибки, асинхронные и синхронные, распространяются в middleware для обработки ошибок.
Однако есть два нюанса:
- Весь ваш асинхронный код должен возвращать промисы (за исключением emitters). Если конкретная библиотека не возвращает промисы, преобразуйте базовый объект, используя вспомогательную функцию, например, Bluebird.promisifyAll().
- Генераторы событий (например, потоки) всё ещё могут вызывать необработанные исключения. Поэтому убедитесь, что вы правильно обрабатываете событие об ошибке; например:
const wrap = fn => (...args) => fn(...args).catch(args[2])
app.get('/', wrap(async (req, res, next) => {
const company = await getCompanyById(req.query.id)
const stream = getLogoStreamById(company.id)
stream.on('error', next).pipe(res)
}))
Функция wrap() — это обёртка, которая перехватывает отклоненные промисы и вызывает next() с ошибкой в качестве первого аргумента. Подробности см. в Обработка асинхронных ошибок в Express с помощью промисов, генераторов и ES7.
Для получения дополнительной информации об обработке ошибок с помощью промисов см. Промисы в Node.js с Q — альтернатива обратным вызовам.
Действия в среде/настройках
Вот несколько действий, которые можно выполнить в вашей системной среде, чтобы улучшить производительность приложения:
- Установка NODE_ENV в «production»
- Автоматическое перезапуск приложения
- Запуск приложения в кластере
- Кэширование результатов запросов
- Использование балансировщика нагрузки
- Использование обратного прокси-сервера
Установка NODE_ENV в «production»
Переменная среды NODE_ENV определяет среду, в которой работает приложение (обычно это разработка или продакшен). Одним из простейших способов повышения производительности является установка NODE_ENV в «production».
Установка NODE_ENV в «production» приводит к тому, что Express:
- Кэширует шаблоны представлений.
- Кэширует файлы CSS, сгенерированные из расширений CSS.
- Генерирует менее подробные сообщения об ошибках.
Тесты показывают, что это может улучшить производительность приложения втрое!
Если вам нужно написать код, специфичный для среды, вы можете проверить значение NODE_ENV с помощью process.env.NODE_ENV. Имейте в виду, что проверка значения любой переменной среды влечет за собой штрафные накладные расходы на производительность, поэтому следует делать это экономно.
Во время разработки вы обычно задаете переменные среды в интерактивной оболочке, например, с помощью export или вашего файла .bash_profile. Но, как правило, вы не должны делать этого на сервере в режиме производства; вместо этого используйте систему инициализации вашей операционной системы (systemd или Upstart). Следующий раздел содержит более подробную информацию об использовании вашей системы инициализации в целом, но настройка NODE_ENV очень важна для производительности (и проста), поэтому она выделена здесь.
С Upstart используйте ключевое слово env в вашем файле задания. Например:
# /etc/init/env.conf env NODE_ENV=production
Дополнительную информацию см. в Учебнике Upstart, руководстве по разработке и лучших практиках.
В systemd используйте директиву Environment в вашем файле единицы. Например:
# /etc/systemd/system/myservice.service Environment=NODE_ENV=production
Дополнительную информацию см. в Использовании переменных среды в единицах systemd.
Обеспечение автоматического перезапуска приложения
В режиме производства вы не хотите, чтобы ваше приложение было недоступно ни на одну секунду. Это означает, что вам нужно убедиться, что оно перезапускается как при аварийном завершении работы приложения, так и при аварии самого сервера. Хотя вы надеетесь, что ни одно из этих событий не произойдет, в реальности вы должны учитывать обе возможности, выполнив:
- Использование диспетчера процессов для перезапуска приложения (и Node) при его аварийном завершении.
- Использование системы инициализации, предоставленной вашей ОС, для перезапуска диспетчера процессов при аварии ОС. Также возможно использование системы инициализации без диспетчера процессов.
Приложения Node аварийно завершают работу, если они сталкиваются с необработанным исключением. Прежде всего, необходимо убедиться, что ваше приложение хорошо протестировано и обрабатывает все исключения (подробности см. в разделе Обработка исключений должным образом). Но как средство защиты, внедрите механизм, гарантирующий автоматическое перезапуск приложения при его аварийном завершении.
Использование диспетчера процессов
Во время разработки вы запускали приложение непосредственно из командной строки с помощью node server.js или аналогичной команды. Но делать это в режиме производства – рецепт катастрофы. Если приложение аварийно завершит работу, оно будет недоступно до тех пор, пока вы его не перезапустите. Чтобы гарантировать перезапуск приложения при его аварийном завершении, используйте диспетчер процессов. Диспетчер процессов – это «контейнер» для приложений, который облегчает развертывание, обеспечивает высокую доступность и позволяет управлять приложением во время выполнения.
В дополнение к перезапуску приложения при его аварийном завершении диспетчер процессов может позволить вам:
- Получать информацию о производительности во время выполнения и потреблении ресурсов.
- Динамически изменять параметры для повышения производительности.
- Управлять кластеризацией (StrongLoop PM и pm2).
Наиболее популярные диспетчеры процессов для Node следующие:
Сравнение диспетчеров процессов по отдельным функциям см. на http://strong-pm.io/compare/. Более подробное введение во все три диспетчера процессов см. в Диспетчеры процессов для приложений Express.
Использование любого из этих диспетчеров процессов будет достаточно для поддержания работоспособности приложения, даже если оно время от времени будет аварийно завершать работу.
Однако StrongLoop PM обладает множеством функций, ориентированных на развертывание в режиме производства. Вы можете использовать его и связанные с ним инструменты StrongLoop для:
- Создание и упаковка приложения локально, а затем безопасное его развертывание в вашей производственной системе.
- Автоматический перезапуск приложения, если оно аварийно завершит работу по любой причине.
- Удаленное управление вашими кластерами.
- Просмотр профилей CPU и снимков памяти для оптимизации производительности и диагностики утечек памяти.
- Просмотр метрик производительности вашего приложения.
- Легко масштабироваться на несколько хостов с интегрированным контролем балансировщика нагрузки Nginx.
Как описано ниже, при установке StrongLoop PM как службы операционной системы с использованием вашей системы инициализации, он будет автоматически перезапускаться при перезапуске системы. Таким образом, он будет поддерживать работоспособность процессов и кластеров вашего приложения постоянно.
Использование системы инициализации
Следующий уровень надежности – обеспечение перезапуска приложения при перезапуске сервера. Системы могут по-прежнему выходить из строя по разным причинам. Чтобы гарантировать перезапуск приложения при аварии сервера, используйте встроенную в вашу ОС систему инициализации. Две основные системы инициализации, используемые сегодня, это systemd и Upstart.
Есть два способа использования систем инициализации с приложением 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 (страница man).
StrongLoop PM как служба systemd
Вы можете легко установить StrongLoop Process Manager как службу systemd. После этого при перезапуске сервера он автоматически перезапустит StrongLoop PM, который затем перезапустит все управляемые им приложения.
Для установки StrongLoop PM как службы systemd:
$ sudo sl-pm-install --systemd
Затем запустите службу с помощью:
$ sudo /usr/bin/systemctl start strong-pm
Дополнительную информацию см. в Настройка хоста в режиме производства (документация StrongLoop).
Upstart
Upstart – это системный инструмент, доступный во многих дистрибутивах Linux для запуска задач и служб во время запуска системы, остановки их во время выключения и наблюдения за ними. Вы можете настроить ваше приложение Express или диспетчер процессов как службу, и Upstart автоматически перезапустит ее при ее аварийном завершении.
Служба Upstart определена в файле конфигурации задания (также называемом «заданием») с именем файла, заканчивающимся на .conf. Следующий пример показывает, как создать задание с именем «myapp» для приложения с именем «myapp» с основным файлом, расположенным в /projects/myapp/index.js.
Создайте файл с именем myapp.conf в /etc/init/ со следующим содержимым (замените выделенный текст значениями для вашей системы и приложения):
# When to start the process start on runlevel [2345] # When to stop the process stop on runlevel [016] # Increase file descriptor limit to be able to handle more requests limit nofile 50000 50000 # Use production mode env NODE_ENV=production # Run as www-data setuid www-data setgid www-data # Run from inside the app dir chdir /projects/myapp # The process to start exec /usr/local/bin/node /projects/myapp/index.js # Restart the process if it is down respawn # Limit restart attempt to 10 times within 10 seconds respawn limit 10 10
ПРИМЕЧАНИЕ. Этот скрипт требует Upstart 1.4 или новее, поддерживаемый в Ubuntu 12.04-14.10.
Поскольку задание настроено на запуск при запуске системы, ваше приложение будет запущено вместе с операционной системой и автоматически перезапущено в случае аварийного завершения работы приложения или сбоя системы.
Помимо автоматического перезапуска приложения, Upstart позволяет использовать следующие команды:
-
start myapp– Запустить приложение -
restart myapp– Перезапустить приложение -
stop myapp– Остановить приложение.
Дополнительную информацию об Upstart см. в Учебнике Upstart, руководстве по разработке и лучших практиках.
StrongLoop PM как служба Upstart
Вы можете легко установить StrongLoop Process Manager как службу Upstart. После этого при перезапуске сервера он автоматически перезапустит StrongLoop PM, который затем перезапустит все управляемые им приложения.
Для установки StrongLoop PM как службы Upstart 1.4:
$ sudo sl-pm-install
Затем запустите службу с помощью:
$ sudo /sbin/initctl start strong-pm
ПРИМЕЧАНИЕ. На системах, которые не поддерживают Upstart 1.4, команды немного отличаются. См. Настройка хоста в режиме производства (документация StrongLoop) для получения дополнительной информации.
Запуск приложения в кластере
В многоядерной системе вы можете многократно увеличить производительность приложения Node, запустив кластер процессов. Кластер запускает несколько экземпляров приложения, желательно по одному экземпляру на каждый ядро процессора, тем самым распределяя нагрузку и задачи между экземплярами.
ВАЖНО: Поскольку экземпляры приложения выполняются как отдельные процессы, они не используют одну и ту же область памяти. То есть объекты локальны для каждого экземпляра приложения. Поэтому вы не можете сохранять состояние в коде приложения. Однако вы можете использовать хранилище в оперативной памяти, например Redis, для хранения данных и состояния, связанных с сеансом. Это ограничение относится практически ко всем формам горизонтального масштабирования, будь то кластеризация с несколькими процессами или несколькими физическими серверами.
В приложениях с кластеризацией рабочие процессы могут аварийно завершаться индивидуально, не влияя на остальные процессы. Помимо преимуществ производительности, изоляция сбоев является еще одной причиной запуска кластера процессов приложения. Всякий раз, когда рабочий процесс аварийно завершается, всегда записывайте событие в журнал и запускайте новый процесс с помощью cluster.fork().
Использование модуля cluster Node
Кластеризация становится возможной с помощью модуля cluster Node. Это позволяет главному процессу запускать рабочие процессы и распределять входящие соединения между рабочими. Однако вместо непосредственного использования этого модуля лучше использовать один из многочисленных инструментов, которые делают это автоматически; например, node-pm или cluster-service.
Использование StrongLoop PM
Если вы развертываете свое приложение в StrongLoop Process Manager (PM), вы можете использовать кластеризацию, не изменяя код приложения.
Когда StrongLoop Process Manager (PM) запускает приложение, он автоматически запускает его в кластере с количеством рабочих процессов, равным количеству ядер процессора в системе. Вы можете вручную изменить количество рабочих процессов в кластере с помощью командной строки slc, не останавливая приложение.
Например, предположим, что вы развернули свое приложение на prod.foo.com, и StrongLoop PM прослушивает порт 8701 (по умолчанию). Чтобы установить размер кластера в восемь с помощью slc:
$ slc ctl -C http://prod.foo.com:8701 set-size my-app 8
Дополнительную информацию о кластеризации с StrongLoop PM см. в разделе Кластеризация в документации StrongLoop.
Использование 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.
Кэширование результатов запросов
Другая стратегия повышения производительности в рабочей среде — кэширование результатов запросов, чтобы приложение не повторяло операции для обслуживания одного и того же запроса повторно.
Используйте сервер кэширования, например Varnish или Nginx (также см. Nginx Caching), чтобы значительно повысить скорость и производительность вашего приложения.
Использование балансировщика нагрузки
Независимо от того, насколько оптимизировано приложение, один экземпляр может обрабатывать только ограниченное количество нагрузки и трафика. Один из способов масштабирования приложения — запуск нескольких экземпляров и распределение трафика с помощью балансировщика нагрузки. Настройка балансировщика нагрузки может повысить производительность и скорость вашего приложения, а также позволит ему масштабироваться больше, чем с одним экземпляром.
Балансировщик нагрузки обычно представляет собой обратный прокси, который управляет трафиком к нескольким экземплярам приложений и серверам. Вы можете легко настроить балансировщик нагрузки для своего приложения, используя Nginx или HAProxy.
При использовании балансировки нагрузки может потребоваться обеспечить, чтобы запросы, связанные с определенным идентификатором сеанса, подключались к процессу, который их инициировал. Это известно как аффинити сеансов или липкие сеансы и может быть решено, как указано выше, с помощью хранилища данных, например Redis, для данных сеанса (в зависимости от вашего приложения). Обсуждение см. в Использовании нескольких узлов.
Использование обратного прокси
Обратный прокси располагается перед веб-приложением и выполняет вспомогательные операции с запросами, помимо перенаправления запросов к приложению. Он может обрабатывать страницы ошибок, сжатие, кэширование, предоставление файлов и балансировку нагрузки, среди прочего.
Передача задач, не требующих знаний о состоянии приложения, обратному прокси позволяет Express выполнять специализированные задачи приложения. По этой причине рекомендуется запускать 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