
3 года назад я поверил в будущее nodejs и начал кампанию по имплементации этого языка в самые “проблемные” сервисы нашего проекта. У нас все получалось — нагрузка падала, стабильность повышалась. Но все же были грабли, о которых захотелось рассказать.
Это не исчерпывающее руководство к действию, просто я делюсь своим опытом, если вы профи в nodejs можете дописать в коментах свои рекомендации, на которые я с удовольствием сошлюсь в статье.
1. Nodejs — однопоточный
Это будет немного непривычно, потому что если у вас 4 ядра, то запущенная нода будет нагружать только одно. Т.е. чтоб загрузить все 4 ядра вам надо запустить 4 инстанса (копии) nodejs. Теперь перед этими 4-ма инстанами нужно установить балансировщик, который будет распределять нагрузку. Лучше не просто балансировщик а проксирующий сервер (с возможностью балансировки):
- Это позволит часть ответов вашей ноды положить в кеш прокси-сервера и отдавать его даже без обращения к самой ноде.
- Это позволит раздавать Вам статический контент специально предназначеным для этого софтом а не “самопальной” частью кода на JavaScript.
- Это даст возможность отдать клиенту какой-то ответ, когда с нодой что-то пошло не так.
- Вам не придется существенно менять что-либо в системе когда у вас появится еще одна нода этого же сервиса.
2. Что ставить перед нодой?
Тут, есть множество вариантов, например:
- Запустить еще один инстанс ноды и задействовать модуль cluster
- Использовать проксисервер-балансировщик:
Мы выбрали 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.
