Отказоустойчивость системы на 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.

Комментарии (41)


  1. arvitaly
    09.11.2015 13:39
    +1

    Пожалуйста, тестируйте все.


    1. GrizliK1988
      09.11.2015 14:19

      Довольно резкая книга. После ее прочтения нет никаких шансов найти оправдания, чтобы не тестировать весь код.


  1. xGromMx
    09.11.2015 13:39
    +1

    Меньше мутабельности) Вместо Array.prototype.push.apply(arr1, arr2) лучше [...a, ...b]


    1. hell0w0rd
      09.11.2015 18:20
      +1

      чем лучше?


      1. xGromMx
        09.11.2015 18:55

        Тем, что не меняет массив «a»


        1. hell0w0rd
          09.11.2015 20:13
          +3

          Я понимаю, что не меняет. Только зачем, если нужно изменить?


  1. pfactum
    09.11.2015 13:41

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

    habrahabr.ru/post/268583/#comment_8608261


    1. apelsyn
      09.11.2015 13:57

      Если Вы сервис создали сами «с-нуля» (как я в примере), то после апгрейда ему ничего не будет. Если модифициворали существующий, то точно затрет, ну тогда вы дали правильную ссылку.


      1. pfactum
        09.11.2015 15:18
        +1

        Если сервис создан с нуля, его нужно помещать в /etc/systemd/system, а не /usr/lib/systemd/system.


        1. mirrr
          09.11.2015 16:03
          -1

          Туда он поместится при systemctl enable [service]


          1. pfactum
            09.11.2015 16:11
            +1

            Нет. enable только создаёт симлинк, например, в цель multi-user (multi-user.target.wants) и ничего никуда не копирует. Вопрос в том, где лежит то, на что будет симлинк.


          1. mirrr
            09.11.2015 16:11
            +1

            ps. Хотя, да, если имеется в виду ложить в корень /etc/systemd/system, то все будет работать аналогично.


        1. evg_krsk
          09.11.2015 16:07
          +2

          Ну, если автор ноду пакует в RPM, то вероятно он и свои сервисы раскатывает из пакетов. Тогда класть юниты в {/usr,}/lib/systemd/systemd как раз правильно. Чтобы локально, per-machine, админ мог перекрыть настройки drop-in-ами.


          1. pfactum
            09.11.2015 16:11
            +1

            Если пакет, то, конечно, /usr/lib.


  1. uSide
    09.11.2015 14:43
    +4

    Мы это делаем путем внесения правок в конфиг и релоада nginx до и после перезагрузки инстанса.

    а почему не через pm2 graceful reload? кстати говоря, у них есть ещё keymetrics.io, из которой можно делать ручной graceful reload и прочие полезные штуки


    1. apelsyn
      09.11.2015 15:12
      +1

      Потому что я не в курсе что там есть такой функционал :), обязательно попробую. И keymetrics.io тоже мне понравился, буду тестить.


  1. heilage
    09.11.2015 19:00
    +5

    Несколько сумбурно написано и структурированности не хватает. По пунктам:

    Теперь перед этими 4-ма инстанами нужно установить балансировщик, который будет распределять нагрузку
    Тут, есть множество вариантов, например: Запустить еще один инстанс ноды и задействовать модуль cluster

    Cluster-модуль содержит балансировку по воркерам внутри себя, ничего дополнительно ставить не нужно. А вот для статики — действительно, неплохо иметь nginx сверху. Но это опять же ничуть не исключает наличия кластер-модуля в апстриме.

    Периодичность перезагрузки (в зависимости от разных причин) от 1 раза в час до 1 раза в сутки.

    Я могу понять когда сервис жрет память и не отдает (от чего кстати хорошо помогают лимиты по использованию памяти в pm2), но от чего сервис может становиться более медленным с течением времени — непонятно. Так или иначе, выше уже упомянули про graceful reload. И да, перезагружать весь инстанс довольно странно, если есть кластер-модуль и можно, грубо говоря, послать SIGTERM медленному воркеру.

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

    Не могу сказать за остальные три, но могу сказать за pm2 — падает как миленький весь God Daemon :) Особенно на node/io от 0.11 до 4.1.2, из-за бага, имевшего место быть в тех версиях в модуле кластера. Так что за pm2 тоже желательно следить, через upstart-скрипт, например. А можно вообще впилить голый кластер-модуль и мониторить/перезагружать его опять же через upstart.

    По пункту 10 — чот вообще вcё в кучу смешалось. Да, мониторинг нужен. Да, юнит-тестирование нужно. Но из пункта сложилось впечатление, что вы предлагаете использовать mocha/jasmine на продакшене для проверки окружения о.0 Это довольно странный способ использования этих фреймворков.


    1. uSide
      10.11.2015 13:45

      Не могу сказать за остальные три, но могу сказать за pm2 — падает как миленький весь God Daemon :) Особенно на node/io от 0.11 до 4.1.2, из-за бага, имевшего место быть в тех версиях в модуле кластера.

      Вы говорите о встроенной кластеризации pm2? У меня сейчас на проекте запускаются воркеры через apps.json с записанными апстримами в nginx, ни одного вылета не заметил


      1. heilage
        10.11.2015 14:45

        Да. Она там правда не совсем встроенная и тоже использует node-cluster, правда с весьма солидным обвесом. Поэтому и падало, собственно. Был даже вот такой замечательный баг: github.com/nodejs/node-v0.x-archive/issues/9261
        И я кажется наврал насчет версии, вроде в io v3 фикс уже приземлился :)


    1. apelsyn
      10.11.2015 13:56

      Я могу понять когда сервис жрет память и не отдает (от чего кстати хорошо помогают лимиты по использованию памяти в pm2), но от чего сервис может становиться более медленным с течением времени — непонятно. Так или иначе, выше уже упомянули про graceful reload.

      Если даже код идеален, сервис теряет приоритет для операционной системы и начинает работать медленее и это есть проблема. В nginx, например, можно задать принудительно приоритет для процесса параметром worker_priority.

      И да, перезагружать весь инстанс довольно странно, если есть кластер-модуль и можно, грубо говоря, послать SIGTERM медленному воркеру.


      Вы невнимательно читали, можно использовать модуль cluster, и я об этом написал. Это менее стабильное решение чем балансировщик nginx или ha-proxy. Я не могу понять чем так плохо раз в сутки ребутнуть инстанс ноды?

      По пункту 10 — чот вообще вcё в кучу смешалось. Да, мониторинг нужен. Да, юнит-тестирование нужно. Но из пункта сложилось впечатление, что вы предлагаете использовать mocha/jasmine на продакшене для проверки окружения о.0 Это довольно странный способ использования этих фреймворков.


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


      1. heilage
        10.11.2015 14:55

        Я не могу понять чем так плохо раз в сутки ребутнуть инстанс ноды?
        Зависит от нагрузки :) Ребут инстанса означает, что некоторые клиенты могут отвалиться от сервиса или не смогут достучаться до него. Если ночью инстанс пустует — почему бы и нет.

        именно на продакшине надо запустить npm test и убедиться что все работает
        Вы не думали о внедрении continuous integration с прогоном всех тестов на тестовых средах, идентичных боевым?


  1. zharikovpro
    09.11.2015 20:03
    +3

    Помимо интеграции через REST/API есть еще и интеграция через очереди сообщений, это очень важный архитектурный паттерн.


    1. uSide
      10.11.2015 13:46

      redis с publish/subscribe, полет отличный


    1. apelsyn
      10.11.2015 13:59

      Да согласен — это хороший подход.


  1. ReklatsMasters
    09.11.2015 21:32

    > И мы не нашли ничего лучшего чем с определенной периодичностью ребутить инстансы.
    Тоже так приходится делать. А всё потому что у приложения «медленные» утечки. Сколько не мониторил код дебаггером — полностью не избавился.

    > Самостоятельно собираем свежие релизы ноды
    В этом смысла нет. Берёте из докера дистрибутив ноды mhart/alpine-node и накатываете на него своё приложение.


  1. bumbay
    09.11.2015 21:50

    Спасибо за полезную информацию.


  1. dgstudio
    09.11.2015 21:51

    Молодцы!

    Уходите от REST-а, вы его переросли. Переходите на AMQP (а именно RabbitMQ с роутингом). Это даст вам асинхронность и все преимущества event-based loose-coupling системы. В том числе такие приятные вещи, как параллелизация нагруженных узлов, кворумные воутеры, возможность рестарта любого сервиса без потерь, dry-run новых версий сервисов, debug «на ходу» и т.д.

    А во вторых — очень любопытно, как вы решаете вопросы совместимости API при внесении breaking changes? Когда у вас десятки сервисов, предполагаю, вам нужно или передепловать всю кучу на новую версию API или поддерживать две версии одновременно. Как именно вы поступаете?


    1. apelsyn
      10.11.2015 14:12

      Мы используем RabbitMQ, там где это уместно (точнее RabbitMQ Cluster из 3-х инстансов — так спокойнее).

      А во вторых — очень любопытно, как вы решаете вопросы совместимости API при внесении breaking changes?

      Поддерживаем и старое и новое API и плавно все сервисы перетекают на новое.


  1. bakhirev
    10.11.2015 08:08
    +1

    Нода под нагрузкой требует увеличения ограничения nofile limit

    Зачем он вам? Вы же не отдаете статику через ноду?

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

    Можете примерно сказать нагрузку? У нас тоже есть такое, но начинают тормозить инстансы с говнокодом. За нормальными такого замечено не было.


    1. apelsyn
      10.11.2015 14:18

      При асинхронном обращении к большому числу REST-сервисов проблема существует.

      У нас проект среднего размера. Мы обслуживаем чуть более полмиллиона пользователей в день. На нагруженых сервисах в пики бывает до 200 req/sec.


  1. yurash
    10.11.2015 12:20

    9. Не бойтесь использовать ECMAScript 2015 (ES6)

    Извините, но пример совершенно не впечатляет, т.к. можно использовать
    arr1 = arr1.concat(arr2);

    без всяких ES6


    1. apelsyn
      10.11.2015 14:26

      Вдохновение можно поискать по ранее приведенной ссылке: https://nodejs.org/en/docs/es6/


    1. ReklatsMasters
      10.11.2015 15:26
      +1

      Вы потеряли ссылку на изначальный массив, и если в нём были объекты, на которые вы где-то ещё ссылаетесь, массив не удалится и будет лежать в памяти. При использовании push из примера автора просто происходит выделение дополнительной памяти под текущий массив.


  1. lui
    10.11.2015 12:48
    +2

    По поводу шестого пункта.
    Может кто-нибудь посоветовать пример проекта с грамотно построенной микросервисной архитектурой?


    1. lotas
      11.11.2015 01:21

      Из последнего Берлинского митапа по node.js могу порекомендовать отличную презентацию от автора coyno.
      Хотя там ориентация была в сторону контейнеризации и использования tutum сервиса для CI


  1. BVadim
    10.11.2015 21:15
    +2

    По поводу утечек памяти и перезагрузок. В конечном счёте обычно выясняется, что виноват код, либо ваш, либо npm. У нас игра на nodejs, очень интенсивная, сетевое взаимодействие через websocket с клиентами. Были «детские» проблемы, которые тоже время от времени забивали память и приходилось перезагружать инстанс, но под пристальным анализом они все были выявлены и исправлены, и да, все они были в коде приложения, хотя далеко не все были так очевидны, т.е. на первый взгляд казалось, что всё в коде хорошо. Но анализ дампов heap и примерных ситуаций, в которых они возникали, помог их выявить. Сейчас инстансы работают спокойно столько, сколько нужно. Неделю недавно работали, т.к. долго не апдейтили код.
    У меня ещё есть сайт написанный на node.js, там тоже всё отлажено, писал давно его и долго мучился сначала, но после того, как все проблемы устранил — работает без перезагрузок вообще, поскольку код там не обновляется, аптайм там месяцами измеряется. Ну и все относительно популярные модули npm довольно серьёзно относятся к проблемам утечек памяти, в целом с ними там ситуация хорошая, их либо нет, либо их быстро исправляют.


    1. ReklatsMasters
      10.11.2015 22:31
      +2

      А поделитесь опытом устранения утечек в ноде, с примерами если можно.


      1. BVadim
        10.11.2015 22:40
        +2

        Все ситуации разные. Подход один: когда видим, что память расходуется не так, как должна, делаем дамп heap и разбираем его в профайлере (Google Chrome). Когда нормальный расход процесса, например, 100Мб, а фактический — гигабайт, не сложно будет увидеть, что за объекты наполняют heap. Ну а далее — думаем, почему GC их не собрал. Причины появления в программе разные, но причина не сбора обычно одна — где-то осталась ссылка на объект, возможно и не явная. Мог где-то быть запущен setInterval или установлен EventListener, в область видимости которого попадает объект. Может быть взаимная ссылка между объектами — очень коварный баг. Ещё могут заполняться буферы, если куда-то пишут быстрее, чем читают. Вообще ситуаций полно, каждую раскручивать нужно исходя из того, что именно утекло.


        1. heilage
          11.11.2015 04:57
          +1

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

          В общем, присоединюсь к просьбе поделиться опытом, возможно даже в виде статьи, а не комментария :)


          1. evg_krsk
            11.11.2015 15:53

            Я конечно JS только начал изучать и могу ляпнуть чушь, но не помогут ли для профилирования проблемных инстансов FlameGraphs? Я понимаю, что они как бы для профилирования onCPU/offCPU, но может быть имеет смысл сравнить срезы нормальных и проблемных инстансов на предмет подозрительных изменений цепочек вызовов?


            1. heilage
              11.11.2015 17:09

              Пожалуй, но проблема тут скорее в том, что нормальных инстансов нет, есть только проблемные %)