От переводчика: Это пятая статья из цикла о Node.js от команды Mozilla Identity, которая занимается проектом Persona.
Как написать приложение Node.js, которое будет продолжать работать даже под невозможной нагрузкой? В этой статье показана методика и библиотека node-toobusy, её воплощающая, суть которой наиболее кратко может быть передана этим фрагментом кода:
var toobusy = require('toobusy');
app.use(function(req, res, next) {
if (toobusy()) res.send(503, "I'm busy right now, sorry.");
else next();
});
В чём заключается проблема?
Если ваше приложение выполняет важную задачу для людей, стоит потратить немного времени на раздумья над самыми катастрофичными сценариями. Это может быть катастрофа в хорошем смысле — когда ваш сайт попадает в фокус внимания социальных медиа, и вместо десяти тысяч посетителей за сутки к вам вдруг приходит миллион. Подготовившись заранее, вы можете создать сайт, который выдержит внезапный всплеск посещаемости, превышающий обычную нагрузку на порядки. Если же этими приготовлениями пренебречь, сайт ляжет именно тогда, когда вы меньше всего этого хотите, когда он у всех на виду.
Это может быть и злонамеренный всплеск трафика, например от DoS-атаки. Первый шаг к борьбе с такими атаками — написание сервера, который не падает.
Ваш сервер под нагрузкой
Чтобы показать, как ведёт себя обычный, не подготовленный к всплеску посещаемости, сервер, я написал демонстрационное приложение, которое на каждый запрос делает пять асинхронных вызовов, в сумме затрачивая пять миллисекунд процессорного времени.
Это примерно соответствует обычному приложению, которое может при каждом запросе писать что-то в логи, обращаться к БД, рендерить шаблон и отправлять клиенту ответ. Ниже приведён график зависимости задержек и ошибок TCP от количества соединений.
Анализ этих данных вполне очевиден:
- Сервер нельзя назвать отзывчивым. Под нагрузкой, в шесть раз превышающей штатную (1200 запросов в секунду) он со скрипом выдаёт ответ в среднем через 40 секунд.
- Отказы выглядят ужасно. В 80% случаев пользователь получает сообщение об ошибке после почти минуты томительного ожидания.
Учимся отказывать вежливо
Для сравнения, я применил к демонстрационному приложению подход, описанный в начале статьи. Он позволяет определять, когда нагрузка превышает допустимую и сразу отвечать сообщением об ошибке. Вот аналогичный первому примеру график для него:
На графике не показано количество ошибок с кодом 503 (service unavailable) — оно плавно возрастает по мере роста нагрузки. Какие выводы можно сделать, глядя на этот график?
- Упреждающие сообщения об ошибках добавляют надёжности. Под нагрузкой, в десять раз превышающей штатную, приложение ведёт себя вполне отзывчиво.
- Успешный ответ или отказ происходит быстро. Среднее время ответа почти всегда меньше 10 секунд.
- Отказы происходят вежливо. Заблаговременно отклоняя запрос при перегрузке, мы заменяем неуклюжее отваливание по таймауту на мгновенный ответ с 503-й ошибкой.
Для того, чтобы заставить сервер аккуратно сообщать о 503-й ошибке, надо лишь создать небольшой шаблон с сообщением для пользователя. Пример такого ответа можно увидеть на множестве популярных сайтов.
Как использовать node-toobusy
Модуль node-toobusy доступен в виде пакета npm и на Гитхабе. После установки (npm install toobusy
), он обычным образом включается в приложение:
var toobusy = require('toobusy');
После подключения модуль начинает активно мониторить процесс, чтобы определять, когда он перегружен. Проверить его состояние можно в любом месте приложения, но лучше это делать на ранних стадиях обработки запроса.
// Регистрируем в самом начале стека middleware, чтобы
// отклонить запрос до того, как мы потратим на него хоть какие-то ресурсы
app.use(function(req, res, next) {
// проверяем состояние занятости - вызов toobusy() очень быстр,
// так как состояние кэшируется на фиксированный интервал времени
if (toobusy()) res.send(503, "I'm busy right now, sorry.");
else next();
});
Уже в таком виде модуль node-toobusy значительно повышает стойкость приложения под нагрузкой. Остаётся подобрать значение чувствительности, наиболее подходящее именно для вашего приложения.
Как это работает?
Как можно надёжно определять, что приложение Node слишком занято?
Это более интересный вопрос, чем можно было бы ожидать, особенно учитывая, что node-toobusy работает в любом приложении «из коробки». Рассмотрим некоторые подходы к решению этой задачи:
Отслеживание использования процессора для текущего процесса. Мы могли бы использовать цифру, которую показывает команда top
— процент времени, которое тратит процессор на работу приложения. Если, к примеру, эта цифра превышает 90%, мы можем сделать вывод, что приложение перегружено. Но если на машине работают несколько процессов, уже нельзя быть уверенным, что именно процесс с приложением Node имеет возможность использовать процессор на 100%. При таком сценарии приложение может никогда не достигнуть этих 90% и при этом практически лежать.
Отслеживание общей загрузки системы. Мы могли бы вычислять общую системную загрузку, чтобы учитывать её при определении перегрузки приложения. Нам бы пришлось учитывать так же число доступных ядер процессора и т.д. Очень быстро такой подход оказывается слишком сложным, требует зависимых от платформы расширений, а ведь ещё надо бы учитывать приоритет процессов!
Нам нужно решение, которое просто работает. Всё что нам нужно — это определять, что наше приложение неспособно отвечать на запросы с приемлемой скоростью. Этот критерий не зависит ни от платформы, ни от других процессов в системе.
В node-toobusy используется замер задержки основного цикла обработки событий. Этот цикл лежит в основе любого приложения Node.js. Вся работа ставится в очередь, и в основном цикле задачи из этой очереди выполняются последовательно. Когда процесс перегружен, очередь начинает разрастаться — работы становится больше, чем можно сделать. Чтобы узнать степень перегрузки, достаточно замерить время, которое требуется крошечной задаче, чтобы отстоять всю очередь. Для этого node-toobusy использует колбэк, который должен вызываться каждые 500 миллисекунд. Отнимая 500 мс от реально измеренного значения интервала, можно получить время, в течение которого задача стояла в очереди, то есть искомую задержку.
Таким образом node-toobusy для определения перегрузки процесса постоянно замеряет задержку основного цикла событий — это простой и надёжный способ, работающий в любом окружении на любом сервере.
Продолжение следует...
Автор: ilya42