Повышаем отказоустойчивость системы на nodejs
3 года назад я поверил в будущее nodejs и начал кампанию по имплементации этого языка в самые “проблемные” сервисы нашего проекта. У нас все получалось — нагрузка падала, стабильность повышалась. Но все же были грабли, о которых захотелось рассказать.
Это не исчерпывающее руководство к действию, просто я делюсь своим опытом, если вы профи в nodejs можете дописать в коментах свои рекомендации, на которые я с удовольствием сошлюсь в статье.
1. Nodejs — однопоточный
Это будет немного непривычно, потому что если у вас 4 ядра, то запущенная нода будет нагружать только одно. Т.е. чтоб загрузить все 4 ядра вам надо запустить 4 инстанса (копии) nodejs. Теперь перед этими 4-ма инстанами нужно установить балансировщик, который будет распределять нагрузку. Лучше не просто балансировщик а проксирующий сервер (с возможностью балансировки):
- Это позволит часть ответов вашей ноды положить в кеш прокси-сервера и отдавать его даже без обращения к самой ноде.
- Это позволит раздавать Вам статический контент специально предназначеным для этого софтом а не “самопальной” частью кода на JavaScript.
- Это даст возможность отдать клиенту какой-то ответ, когда с нодой что-то пошло не так.
- Вам не придется существенно менять что-либо в системе когда у вас появиться еще одна нода этого же сервиса.
2. Что ставить перед нодой?
Тут, есть множество вариантов, например:
- Запустить еще один инстанс ноды и задействовать модуль cluster
- Использовать проксисервер-балансировщик:
Не ставьте apache, он тоже все умеет, но не очень оптимален с точки зрения задействованных ресурсов.
Мы выбрали nginx.
3. Инстанс под нагрузкой, который работает продолжительное время начинает «тормозить»
Это связано не только с особенностью ядра, которое понижает приоритет процессу, работающему слишком долго, это и часть проблем со сборкой мусора в самой ноде, это и часть проблем допущенных в Javascript-коде. И мы не нашли ничего лучшего чем с определенной периодичностью ребутить инстансы. Периодичность перезагрузки (в зависимости от разных причин) от 1 раза в час до 1 раза в сутки.
Чтоб не было приостановки работы сервиса, нужно запустить 2 инстанса (копии) сервиса. Во время ребута инстанса в балансировщике нужно снимать с него нагрузку.
Мы это делаем путем внесения правок в конфиг и релоада nginx до и после перезагрузки инстанса.
4. Нода под нагрузкой требует увеличения ограничения nofile limit (LimitNOFILE)
Во многих дистрибутивах, по умолчанию там стоят очень скромные цифры. Я рекомендую ставить больше 16000 (у мнея стоит 131070). Это можно задать командой ulimit -n 131070, или подправить /etc/security/limits.conf
Мы описываем сервер в стандарте systemd, там ограничение задается переменной LimitNOFILE и выглядит это приблизительно так (файл /usr/lib/systemd/system/nodejs1936.service):
[Unit]
Description=Nodejs instance 1936
After=syslog.target network.target
[Service]
PIDFile=/var/run/nodejs1936.pid
Environment=NODE_ENV=production
Environment=NODE_APP_INSTANCE=1936
WorkingDirectory=/var/www/myNodejsApp
# node_program supervisor
ExecStart=/usr/bin/supervisor --harmony -- /var/www/myNodejsApp/index.js -p 1936
User=node
Group=node
LimitNOFILE=131070
PrivateTmp=true
[Install]
WantedBy=multi-user.target
5. У инстанса ноды память ограничена и это ограничение не очень большое
У nodejs по умолчанию установлен лимит на максимальный размер памяти, которую может «отъедать» каждый инстанс. Цитирую Faq по v8:
Currently, by default v8 has a memory limit of 512MB on 32-bit systems, and 1.4GB on 64-bit systems.
Но есть способ это поменять с помощью ключа --max-old-space-size, память указываем в M, например чтоб увеличить до 4G пишем --max-old-space-size=4096
Также можно влиять на размер стека ключем --stack-size, например --stack-size=512
Увеличение памяти может быть полезно при написании периодически запускающегося процесса, который спроектирован так, чтоб максимально использовать RAM для работы с данными. Ну например, скрипт рассылки писем, анализатор логов и т.д.
6. Код создан в виде большого связанного моноблока
Не далайте “три в одном” или “десять в одном” — это может работать, но любая нештатная ситуация завалит весь проект. Наоборот — делите все на 3, 5, 10 независимых сервисов. Чем проще и легче сервис тем стабильнее его работа. “Вылетание” одного сервиса приведет к вылетанию части функционала но не всего проекта.
Применяйте декомпозицию, делите сложные сервисы на несколько простых. Взаимодействуйте между сервисами по REST-протоколу. Это и будет технологичным фундаментом для роста вашего проекта. “Распухший” сервис всегда можно легко переселить на более производительный сервер без изменения архитектуры приложения.
Тут есть и обратная сторона медали, упрощая сами сервисы мы усложняем связи между ними и это тоже может быть проблемой, когда приложение насчитывает десятки а то и сотни сервисов, между ними могут быть сотни а то и тысячи связей. Это повышает отказоустойчивость системы но также повышает сложность разработки, деплоя проекта а также порог вхождения разработчика в проект.
7. Используйте софт, который может перезапустить упавший процесс ноды
Как бы грамотно не был написан код все равно придет тот момент. когда он упадет, после непредусмотренной вами ошибки. Для того чтоб решить эту стандартную проблему написано множество cli утилит, которые следят за процессом ноды и в случае необходимости его перегружают:
Мы используем supervisor.
Эту же функциональность можно организовать с помощью настроек сервиса в Systemd (недавно была статья).
8. Самостоятельно собираем свежие релизы ноды
Не ждем когда нода обновиться в вашем дистрибутиве. Это будет крайне неоперативно. Я рекомендую научиться собирать пакеты для своего дистрибутива Linux самостоятельно, тем более что в этом нету ничего сложного.
Мы собираем свежие версии ноды в виде rpm пакетов для дистрибутива Fedora буквально через 1-3 дня после свежего релиза. После непродолжительных тестов переводим на новую ноду все продакшн сервисы.
Не забудьте пересобрать node_modules, при смене версии ноды (вернее мажорной или минорной версии), проблемы могут возникать с модулями, часть кода которого написана на с.
9. Не бойтесь использовать ECMAScript 2015 (ES6)
Сейчас у нас в на production серверах установлена nodejs 5.0.0, а еще год назад стояла 0.11.6.
Еще в 4.2.2 для того чтоб дописать в конец массива arr1, элементы массива arr2, нужно было писать вот так
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
// Append all items from arr2 onto arr1
Array.prototype.push.apply(arr1, arr2);
В 5.0.0 мы это можно сделать так
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
arr1.push(...arr2);
Напрашивается вопрос: «Как это влияет на стабильность?». Будущее уже пришло, генераторы, классы, промисы... это то что делает код более понятным и прогнозируемым, чем понятнее код тем легче заметить в нем ненормальность и ее устранить. Использование ECMAScript 2015 (ES6) помогло нам полностью избавиться от CallBack hell-а и существенно сократить количество кода.
10. Тестируем (ну хоть часть кода)
Не спешите в меня бросать всем что сейчас у вас под руками. Да, я тоже не фанат тестирования на начальном этапе разработки, но ведь мы уже находимся на стадии стабилизации продукта раз читаем такие статьи, не так ли? :)
Я не тестирую все подряд. Обычно я пишу простые тесты, которые дают мне уверенность что у меня все Ok с “внешними” сервисами, и критически важными функциями самого сервиса. Например: записал, отредактировал и удалил ключ в memcached или redis, аналогично с mysql, mongodb, каким-нибудь внешним сервисом.
Часто возникает соблазн сказать: “так я ж мониторю MySQL забиксом, зачем мне проверять что-то с самого сервиса”. Не раз в моей практике была ситуация когда с сервисом вроде как все Ok, с MySQL все Ok, а вот со связью между сервисом и MySQL есть проблема, например, админ случайно добавил не совсем корректное правило в iptables, шнурок между сервером и свичем перегнули или зацепили и он плохо работает и возникают ошибки при передаче по сети, слишком «умное» ядро на базе MySQL решило что сервис производит SYN-flood и начало дропать пакеты и т.п.
Тестировать, по большому счету, можно и без фреймворка, просто вашему забиксу вы возвращаете какое-то значение (0 или 1), или какое-то число и он, при получении значения за пределами допустимого, шлет вам SMS о проблеме.
Если потребность в фреймворке есть, тогда присмотритесь к этим:
Это не все о чем хотел сказать, но уже достаточно для статьи. В следующих сериях мы рассмотрим тюнинг ядра для нужд ноды, грабли виртуализации, как правильно закэшировать nginx-ом ответы от инстансов nodejs.