Перейти к содержанию

Лучшие практики: производительность и надежность

Обзор

В статье рассматриваются лучшие практические методы обеспечения производительности и надежности приложений Express, развернутых в рабочей среде.

Рассматриваемая тема, без сомнения, относится к категории "DevOps", которая рассматривает процесс традиционной разработки программного обеспечения во взаимосвязи с эксплуатацией. Соответственно, представленную в ней информацию можно разделить на две части:

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

Ниже приведены некоторые примеры того, что можно сделать в коде для улучшения производительности приложений.

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

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

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

1
2
3
4
var compression = require('compression');
var express = require('express');
var app = express();
app.use(compression());

Для активно используемого веб-сайта в рабочей среде разумнее всего реализовать сжатие на уровне обратного прокси. В этом случае можно обойтись без промежуточного обработчика для сжатия данных. Более подробная информация об активации сжатия в Nginx приведена в разделе Модуль сжатия ngx_http_gzip_module документации по Nginx.

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

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

Модуль Node и многие другие модули поддерживают синхронную и асинхронную версию выполнения функций; однако в рабочей среде следует использовать только асинхронную версию. Синхронное выполнение функций может быть оправдано только при первоначальном запуске.

При работе с Node.js 4.0+ или io.js 2.1.0+ можно воспользоваться флагом командной строки --trace-sync-io, который выводит предупреждение и трассировку стека, если в приложении используется синхронный API. В рабочей системе это, конечно, лишнее; скорее, это позволяет убедиться, что код готов для рабочей среды. Дополнительная информация приведена в разделе Еженедельное обновление io.js 2.1.0.

Использовать промежуточный обработчик для обслуживания статических файлов

В среде разработки для обслуживания статических файлов можно использовать метод res.sendFile(). Для рабочей среды этот метод не подходит: при обработке каждого запроса файла он выполняет чтение из файловой системы, создавая большую задержку и снижая общую производительность приложения. Заметьте, что метод res.sendFile() не реализован для системного вызова sendfile, который мог бы существенно повысить его эффективность.

Рекомендуем воспользоваться промежуточным обработчиком serve-static (или аналогичным ему), оптимизированным для обслуживания файлов приложений Express.

Еще лучше воспользоваться для обслуживания статических файлов обратным прокси; дополнительная информация приведена в разделе "Использование обратного прокси-сервера".

Организовать корректное ведение протоколов

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

В целях отладки

При ведении протокола в целях отладки рекомендуется вместо console.log() воспользоваться специальным отладочным модулем типа debug. При работе с этим модулем можно использовать переменную среды DEBUG, которая определяет, какие отладочные сообщения будут переданы в console.err(). А для того чтобы приложения оставались чисто асинхронными, рекомендуется выводить результаты console.err() в другую программу. Но вы же не собираетесь заниматься отладкой в рабочей среде, верно?

В целях регистрации работы приложения

Для регистрации работы приложения (например, учета переданных данных или отслеживания вызовов API-функций) можно вместо console.log() воспользоваться библиотекой регистрации типа Winston или Bunyan. Подробное сравнение двух библиотек проведено в корпоративном блоге StrongLoop Сравнение протоколирования Node.js с использованием Winston и Bunyan.

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

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

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

  • метод try-catch
  • метод Promise

Прежде чем перейти к этим темам, необходимо понимать основные принципы обработки ошибок в Node/Express: использование функции обратного вызова, в которой первый аргумент зарезервирован за объектом ошибки, и распространение ошибок в промежуточном обработчике. В Node принято соглашение "error-first callback" для возврата ошибок асинхронных функций: первый параметр любой функции обратного вызова всегда является объектом-ошибкой, за ним следуют параметры, содержащие результат обработки. Для того чтобы сообщить об ошибке, в качестве первого параметра передайте нуль. Для правильной обработки ошибки функция обратного вызова должна соответствующим образом выполнять соглашение "error-first callback". В Express, как показала практика, лучший метод состоит в распространении ошибок по цепочке промежуточных обработчиков с использованием функции next().

Более подробная информация об основных принципах обработки ошибок приведена в разделе:

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

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

Кроме того, использование uncaughtException официально считается crude, и имеется вариант proposal исключить его из ядра. Значит, обработка uncaughtException - идея неудачная. Поэтому мы и рекомендуем иметь несколько процессов и супервизоров: часто самым надежным способом восстановления после ошибки является удаление и перезапуск системы.

Также мы не рекомендуем использовать модуль domains. Он очень редко помогает решить проблему и является устаревшим.

Метод try-catch

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

Инструменты типа JSHint или JSLint помогут вам найти неявные исключительные ситуации, подобные описанным в разделе Ошибки ReferenceError в неопределенных переменных.

Ниже приводится пример использования конструкции try-catch для обработки потенциальной исключительной ситуации, приводящей к отказу процесса. Этот промежуточный обработчик принимает параметр поля запроса "params", который является объектом JSON.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
app.get('/search', function (req, res) {
    // Simulating async operation
    setImmediate(function () {
        var jsonStr = req.query.params;
        try {
            var jsonObj = JSON.parse(jsonStr);
            res.send('Success');
        } catch (e) {
            res.status(400).send('Invalid JSON string');
        }
    });
});

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

Метод Promises

Промисы (promises) обрабатывают любые исключительные ситуации (явные и неявные) в блоках асинхронного кода, в которых используется метод then(). Просто добавьте .catch(next) в конце цепочки промисов. Например:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
app.get('/', function (req, res, next) {
    // do some sync stuff
    queryDb()
        .then(function (data) {
            // handle data
            return makeCsv(data);
        })
        .then(function (csv) {
            // handle csv
        })
        .catch(next);
});

app.use(function (err, req, res, next) {
    // handle error
});

Теперь все ошибки, асинхронные и синхронные, будут передаваться в промежуточный обработчик ошибок.

Однако здесь необходимо разъяснить два момента:

  1. Весь асинхронный код должен возвращать промисы (кроме отправителей). Если какая-то библиотека не возвращает промисы, преобразуйте объект base при помощи вспомогательной функции типа Bluebird.promisifyAll().
  2. Отправители событий (такие как потоки) могут вызывать необрабатываемые исключительные ситуации. Поэтому проверьте правильность обработки событий ошибки; например:
1
2
3
4
5
6
7
8
app.get(
    '/',
    wrap(async (req, res, next) => {
        let company = await getCompanyById(req.query.id);
        let stream = getLogoStreamById(company.id);
        stream.on('error', next).pipe(res);
    })
);

Дополнительная информация об обработке ошибок с использованием промисов приведена в разделе:

Что можно сделать в среде / при настройке

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

  • Задать в переменной 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. Например:

1
2
# /etc/init/env.conf
 env NODE_ENV=production

Дополнительная информация приведена в разделе Upstart: введение, справочное руководство и лучшие практические методы.

Для systemd укажите директиву Environment в своем файле юнитов. Например:

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

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

При работе с StrongLoop Process Manager можно также задать переменную среды во время установки StrongLoop PM как службы.

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

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

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

При наступлении необрабатываемой исключительной ситуации в приложениях Node происходит сбой. Поэтому самое главное, что нужно сделать, - обеспечить, чтобы приложение было тщательно протестировано и обрабатывало все исключительные ситуации (дополнительная информация приведена в разделе "Правильно обрабатывать исключительные ситуации"). А для устойчивости к отказам необходимо иметь механизм, который обеспечит автоматический перезапуск приложения, если произойдет его сбой.

Использовать диспетчер процессов

В среде разработки запустить приложение можно прямо из командной строки, указав node server.js или нечто подобное. В рабочей среде это верный путь к беде: в случае сбоя приложение будет отключено до тех пор, пока вы не выполните его перезапуск. Для того чтобы приложение перезапускалось после сбоя, используется диспетчер процессов. Диспетчер процессов - это "контейнер" для приложений, обеспечивающий развертывание и высокую готовность и позволяющий управлять приложением в среде выполнения.

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

  • получать аналитическую информацию о производительности среды выполнения и потреблении ресурсов;
  • изменять параметры в динамическом режиме в целях повышения производительности;
  • управлять кластеризацией (StrongLoop PM и pm2).

Наиболее популярные диспетчеры процессов перечислены ниже:

Сравнение трех диспетчеров процессов по каждой характеристике можно найти в разделе http://strong-pm.io/compare/. Более подробное представление трех диспетчеров приведено в разделе Диспетчеры процессов для приложений Express.

Наличие любого из этих диспетчеров процессов позволит обеспечить работоспособность приложения даже в случае возможных сбоев.

Однако StrongLoop PM имеет массу характеристик, рассчитанных специально на развертывание в среде выполнения. StrongLoop и связанные с ним инструменты позволяют:

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

Как объясняется ниже, при установке StrongLoop PM в качестве службы операционной системы с помощью системы инициализации, этот диспетчер будет автоматически выполнять перезапуск после перезагрузки системы. То есть, поддерживать постоянную активность процессов и кластеров.

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

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

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

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

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

Файл конфигурации службы systemd имеет имя unit file с расширением .service. Ниже приведен пример файла юнитов для непосредственного управления приложением Node (вместо выделенного жирным шрифтом текста укажите значения для своей системы и приложения):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[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 (страница справки).

StrongLoop PM как служба systemd

Диспетчер процессов StrongLoop можно легко установить как службу systemd. В этом случае во время перезапуска сервера автоматически выполняется перезапуск StrongLoop PM; он, в свою очередь, перезапускает все приложения, которыми он управляет.

Для установки StrongLoop PM как службы systemd выполните следующие действия:

1
$ sudo sl-pm-install --systemd

Затем запустите службу в следующем порядке:

1
$ 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/ со следующим содержимым (вместо выделенного жирным шрифтом текста укажите значения для своей системы и приложения):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 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 можно легко установить как службу Upstart. В этом случае во время перезапуска сервера автоматически выполняется перезапуск StrongLoop PM; он, в свою очередь, перезапускает все приложения, которыми он управляет.

Для установки StrongLoop PM как службы Upstart 1.4 выполните следующие действия:

1
$ sudo sl-pm-install

Затем запустите службу в следующем порядке:

1
$ sudo /sbin/initctl start strong-pm

ПРИМЕЧАНИЕ. В системах, не поддерживающих Upstart 1.4, команды будут иметь некоторые отличия. Дополнительная информация приведена в разделе Настройка хоста рабочей среды (документация по StrongLoop).

Выполнять приложение в кластере

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

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

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

Использовать модуль cluster Node

Поддержка кластеров возможна благодаря модулю Node cluster module. Он позволяет главному процессу порождать процессы экземпляра приложения и распределять входящие соединения между экземплярами приложения. Но лучше использовать не сам этот модуль, а один из его инструментов, который будет выполнять необходимые действия автоматически, например node-pm или cluster-service.

Использовать StrongLoop PM

Если приложение развернуто в диспетчере процессов StrongLoop Process Manager (PM), вы можете пользоваться поддержкой кластеров, не изменяя код приложения.

Когда диспетчер процессов StrongLoop Process Manager (PM) выполняет приложение, то приложение автоматически будет выполняться в кластере с числом экземпляров приложения, равным числу ядер ЦП в системе. В кластере число процессов экземпляра приложения невозможно изменить вручную при помощи инструмента командной строки slc без остановки приложения.

Например, если вы развернули приложение на prod.foo.com и StrongLoop PM слушает соединения на порте 8701 (значение по умолчанию), укажите размер кластера, равный восьми, используя slc:

1
$ slc ctl -C http://prod.foo.com:8701 set-size my-app 8

Дополнительная информация о поддержке кластеров при помощи StrongLoop PM приведена в разделе Кластеризация документации по StrongLoop.

Сохранять результаты запросов в кэше

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

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

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

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

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

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

Использовать StrongLoop PM с распределителем нагрузки Nginx

StrongLoop Process Manager интегрируется с Nginx Controller, позволяя легко настраивать конфигурации рабочих сред на нескольких хостах. Дополнительная информация приведена в разделе Масштабирование на нескольких серверах (документация по StrongLoop).

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

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

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