Сервис с распознаванием лиц «Look-A-Like» обслуживал тысячи пользователей одновременно

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


В рамках работы в MyHeritage мы разработали сервис doppelganger для Евровидения 2019 года, с помощью которого, загрузив селфи, можно узнать на кого из участников конкурса вы похожи больше всего.


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


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


  1. Надейтесь на лучшее, но готовьтесь к худшему: измерьте, сколько одновременных пользователей получится обслуживать вашим приложением за время X (одним инстансом). Например, в нашем случае тестирование показало, что мы сможем обслуживать по 200 одновременных пользователей в каждом инстансе EC2 в течение 10 секунд, поэтому когда мы узнали, что должны обслуживать 10 000 одновременных пользователей, нам просто осталось подготовить 50 серверов за балансировщиком. Для проведения теста мы воспользовались отличным инструментом под названием JMeter.

    А этот туториал сильно помог при подготовке к выполнению замеров.
  2. Избегайте блокировок: блокирующие операции (вроде fs.readSync) заманчивы, потому что код выглядит чище, но они буквально убивают производительность. Вместо них используйте async/await операции, потому что во время выполнения асинхронной работы ЦП будет доступен и для других задач (см. Цикл событий).

    До: const res = fs.readSync('file.txt');
    После: const res = await fs.readAsync('file.txt');
  3. Увеличьте лимит памяти: Node по умолчанию настроен на ограничение в 1 ГБ. Если серверу доступно, скажем, 4 ГБ специально под ваше приложение, установить максимальный предел памяти вам придется вручную, используя CLI со следующим флагом: --max-old-space-size
    Пример: node --max-old-space-size=4096 server.js
  4. Убедитесь, что вы задействовали все ядра процессора: по умолчанию Node работает в одном треде. Если вы специально не настраивали конфигурацию, которая запускала бы несколько тредов, сэкономьте деньги, выбрав сервер с 1 ядром.
  5. Сократите количество обращений к приложению: настройте принудительный HTTPS и все редиректы как можно выше (например, на уровне прокси). Это позволит приложению не отвлекаться на лишнее, а, значит, быть доступнее для запросов, которые действительно важны.
  6. Обработка ошибок: используйте логирование, например Logz.io/AWS CloudWatch для отслеживания ошибок, которые могут привести к сбою приложения. НЕ СООБЩАЙТЕ обо всём подряд службам типа Slack, потому что сообщения обычно идут скопом и легко могут забить канал. Мы использовали отличную библиотеку под названием Winston для логирования в NodeJS.

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


Спасибо, что дочитали.

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


  1. Suvitruf
    15.06.2019 23:33

    В нашем случае эти советы привели к десятикратному улучшению производительности
    (¬?¬ )

    А если по существу:
    Увеличьте лимит памяти
    А может лучше посмотреть, куда сжирается память?
    по умолчанию Node работает в одном треде
    Ага, а внутри в ноде нету своего пула потоков.

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


    1. germn Автор
      16.06.2019 02:06

      Ага, а внутри в ноде нету своего пула потоков.

      Поправьте меня, если ошибаюсь, но эти потоки обрабатывают в фоне I/O, и заметного выигрыша увеличение количества ядер не даст. Дополнительные ядра могли бы пригодиться для вычислительных задач, но последние происходят в цикле событий, который как раз по-умолчанию однопоточный. Мне всегда казалось это как-то так работает.


      1. ReklatsMasters
        16.06.2019 09:20

        Всё правильно, этот пул для io операций. И конечно разумнее выполнять эти операции в других потоках.


      1. Suvitruf
        16.06.2019 13:06

        Мой поинт в том, что многие почему-то думают, что если они поднимут инстансов сервиса по числу ядер, то оптимально утилизируют мощности (ведь нода в одном треде работает, да?), но при этом забывают, что эти же ядра используются и потоками из пула каждого поднятого инстанса ноды.


        1. taliban
          16.06.2019 16:39

          Можете перефразировать либо дать ссылку на литературу? Я все еще считаю что кластер из двух инстансов займут 2 ядра, даже если их больше. Или это не так?