Или как взорвать ваше приложение за 100 простых шагов

Впервые опубликовано на сайте The Data Wrangler.

Следите за автором в Твиттере, чтобы узнать больше об этом.

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

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

Если у вас нет времени читать пост, посмотрите видео.

Этот пост и видео основаны на моем недавнем выступлении для BrisJS: Встреча по JavaScript в Брисбене. Слайды выступления доступны онлайн.

Если вам когда-либо не хватало памяти в Node.js, вы должны об этом знать. Вы не можете легко забыть, что видели ФАТАЛЬНУЮ ОШИБКУ Node.js, подобную показанной ниже.

Главы 7 и 8 моей книги Обработка данных с помощью JavaScript посвящены тому, как втиснуть огромный набор данных в память, не разрушая ваше приложение.

При написании глав 7 и 8 я задавался вопросом

Каков предел использования памяти в Node.js?

Что ж, я мог бы просто поискать в Интернете эту информацию - что я все равно сделал - но я также хотел проверить пределы на себе. Итак, я написал код, чтобы довести Node.js до предела своих возможностей. Вы увидите это через мгновение и сможете попробовать сами.

Но сначала давайте разберемся

Почему нам вообще не хватает памяти?

Это может произойти, когда мы загружаем слишком много данных за один раз. Если вы попытаетесь загрузить набор данных, размер которого превышает доступную память, у вас наверняка не хватит памяти - вы получите ФАТАЛЬНУЮ ОШИБКУ, которую мы видели ранее.

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

В Node.js нам не нужно явно освобождать наши выделения - сборщик мусора v8 делает это за нас, но его несложно обмануть, заставив удерживать выделенную память.

Давайте посмотрим код

Вот какой-то псевдокод, который исчерпает память в Node.js. Также ознакомьтесь с реальным кодом тестирования памяти, доступным на GitHub.

Как видите, код зацикливается и распределяется, пока Node.js не испустит последний вздох. Ключевым моментом здесь является вызов функции process.memoryUsage (), которая позволяет нам определить, сколько кучи было использовано.

Выполнение кода дает результат, как показано ниже:

Allocated since start 1.26 GB     
Allocated since start 1.27 GB     
Allocated since start 1.28 GB     
Allocated since start 1.29 GB     
Allocated since start 1.3 GB      
Allocated since start 1.31 GB     
Allocated since start 1.32 GB     
Allocated since start 1.33 GB     
Allocated since start 1.34 GB     
Allocated since start 1.35 GB     
Allocated since start 1.35 GB

Я протестировал этот код на своем ноутбуке с Windows 10 объемом 8 ГБ, и он почти достигает 1,4 ГБ до того, как произойдет ФАТАЛЬНАЯ ОШИБКА. Я могу точно настроить размер шага выделения, и я все ближе и ближе к 1,4 ГБ (хотя тест занимает все больше и больше времени). Кажется, я не могу продвинуть его дальше 1,4 ГБ. Похоже, это встроенное ограничение по умолчанию в Node.js

Так почему же ограничение в 1,4 ГБ?

Честно говоря, не знаю. Это кажется очень маленьким, ведь 64-битное приложение теоретически должно иметь доступ к 16 ТБ, что намного больше 1,4 ГБ!

Я подозреваю, что ограничение в 1,4 ГБ существует по историческим причинам. Код V8 изначально запускался в 32-битном браузере с очень строгими и принудительными ограничениями памяти (JavaScript для каждой вкладки должен работать с каждой другой вкладкой). Позже на нем был построен Node.js, который был перенесен на 64-битную версию. Я думаю, что нынешнее ограничение - пережиток давно минувших дней. Если вы знаете об этом больше, оставьте комментарий ниже и просветите меня!

Архитектура памяти Node.js

Итак, объем памяти ограничен, но у меня в рукаве есть хитрость, чтобы поднять лимит памяти, но перед этим вам понадобится краткий обзор архитектуры памяти Node.js, чтобы понять, как работает эта уловка.

Память, потребляемая приложением Node.js, попадает в одну из трех областей:

  • Код
  • Куча
  • Куча

Код, очевидно, - это место, где хранится ваш код.

Стек - это место, где хранится стек вызовов функций. Здесь размещены локальные переменные.

Сегодня я говорю о куче: именно там хранятся динамические выделения. Здесь вы найдете строки, объекты и массивы, с которыми вы работаете в своем коде JavaScript.

Куча Node.js состоит из двух разделов:

  • Новое пространство
  • Старое пространство

Это связано с тем, что распределения делятся на два поколения.

Новое распределение происходит в новом пространстве, также известном как молодое поколение. Это небольшой объем памяти от 1 до 8 мегабайт. Выделить и освободить место очень быстро. Сборка мусора здесь происходит часто.

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

Итак, вот трюк, который я обещал…

Давайте просто увеличим объем памяти в старом пространстве

Мы можем использовать параметр механизма V8 –max-old-space-size, как показано ниже, чтобы увеличить доступный нам предел кучи.

Теперь у нас есть память, которую нужно сжечь!

Я снова запускаю тестовый код с параметром 6000 или 6 ГБ. Вы можете видеть, что теперь я увеличил его размер до 1,4 ГБ, и я приближаюсь к 6 ГБ, прежде чем произойдет ФАТАЛЬНАЯ ОШИБКА:

Allocated since start 5.74 GB
Allocated since start 5.75 GB
Allocated since start 5.76 GB
Allocated since start 5.77 GB
Allocated since start 5.78 GB
Allocated since start 5.79 GB
Allocated since start 5.8 GB
Allocated since start 5.81 GB
Allocated since start 5.82 GB
Allocated since start 5.83 GB
Allocated since start 5.84 GB
Allocated since start 5.85 GB

Как далеко мы можем зайти с этим?

Теоретически в 64-битном приложении мы можем получить доступ к 16 ТБ памяти. К сожалению, я не могу даже приблизиться к этому, и поверьте мне, я действительно пытался! Я установил для параметра очень большое число. Последняя метрика выделения - 47 ГБ. В какой-то момент после этого мой ноутбук действительно перестал отвечать, и мне пришлось принудительно перезагрузить компьютер. Пожалуйста, не пытайтесь делать это дома, дети! Вы действительно можете найти способ повредить свой компьютер.

Конечно, я никогда не смогу выделить до 16 ТБ, по крайней мере, на этом ноутбуке. У моего ноутбука всего 8 ГБ физической памяти. Как только физическая память исчерпана, память приложения будет выгружена на жесткий диск через виртуальную память.

По сути, я ограничен размером жесткого диска ноутбука. Даже если бы у меня был 1 ТБ дискового пространства (что сейчас довольно часто), я бы даже не смог выделить его больше. Конечно, на жестком диске уже есть файлы, и они должны использоваться всеми приложениями, запущенными на ноутбуке.

Уместить больше в памяти

Конечно, нам не следует полагаться на параметр двигателя V8, чтобы наше производственное приложение поместилось в памяти. Это похоже на небольшой риск! Что касается Node.js, этот параметр кажется недокументированным, и все мы знаем, что недокументированные функции могут исчезнуть без предупреждения.

Намного безопаснее сосредоточиться на размещении нашего приложения в памяти без использования старого параметра пробела.

Но как обеспечить надежное размещение нашего приложения в памяти?

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

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

Вы также должны контролировать использование производственной памяти. Это не сложно. Регулярно вызывайте вышеупомянутую функцию process.memoryUsage (), регистрируйте метрику heapUsed и отображайте ее на диаграмме (не стесняйтесь использовать мою библиотеку с открытым исходным кодом Data-Forge Plot. Затем вы можете легко увидеть, есть ли восходящий тренд со временем.

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

Это хорошо для масштабируемости:

  • Каждый процесс потенциально может выполняться на отдельном процессоре.
  • Каждый процесс имеет свое собственное отдельное пространство памяти.

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

Чтобы узнать больше, см. Главы 7 и 8 моей книги Data Wrangling с помощью JavaScript. Я могу еще многое сказать по этой теме, чтобы ваше приложение поместилось в памяти. Вы также можете узнать, как анализировать и составлять диаграммы таких показателей приложения, как использование памяти.

И последний совет ... просто помните, что ваше приложение не должно работать вечно! Он просто должен работать в течение всего вашего непрерывного цикла доставки. Это дает вам некий примерный ориентир для вашего испытания на выдержку. Если вы развертываете каждые две недели, убедитесь, что ваше приложение может без проблем работать в течение двух недель под нагрузкой!

Ресурсы

Интересные статьи

Https://blog.codeship.com/understanding-garbage-collection-in-node-js/ фактическиhttp://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection https://blog.risingstack.com/finding-a-memory-leak-in-node-js/