Посмотрите на утечки памяти в кешировании и обещаниях

Мы живем в золотой век программного обеспечения. У нас есть инструменты для всего. У нас есть инструменты для изготовления инструментов. Мы встраиваем базовые системы в продукты высокого уровня. Но одно общее на всех уровнях: печально известная утечка памяти. Это не всегда ошибка кода; иногда это также может быть вызвано непониманием поведения среды выполнения.

Речь идет о среде выполнения Node.js. Есть несколько типов утечек памяти, которые очень распространены. О двух из них я написал в Части 1. Во второй части мы рассмотрим два других типа.

4 типа утечек памяти

  • Глобальные ресурсы
  • Закрытие
  • Кеширование
  • Обещания

Сегодня мы сосредоточимся на кешировании и обещаниях.

Кеширование

Кэширование - наиболее распространенный тип утечки памяти в Node. Мне лично не нравится кеширование на диске; Я всегда предпочитаю специализированное решение. Но поскольку это вызвало головную боль у многих разработчиков, давайте рассмотрим простой пример, чтобы увидеть, как кеширование может вызывать утечки памяти.

Теперь давайте запустим следующее:

clinic doctor --on-port 'autocannon -w 300 -c 100 -d 30 localhost:3000' -- node server.js

Чтобы узнать, как работает это нагрузочное тестирование, обратитесь к Части 1, но в основном мы используем autocannon для нагрузочного тестирования и Clinic.js для анализа графов памяти. Посмотрим, что у нас получится.

Память быстро увеличивается. Давайте разберемся, что происходит в этом сценарии. Мы знаем, что функции являются объектами в JavaScript, и используем это для хранения здесь кэшированных значений. Мы храним буферный объект размером 1 КБ в кеше и используем случайные числа в качестве ключей для кешей. Давайте возьмем дамп кучи, чтобы проверить, что утекает. (Процесс дампа кучи был объяснен в Части 1.)

Мы видим, что все буферные объекты находятся в памяти и в кэше computeTerm. Причина утечки памяти в кэше заключается в том, что мы не очищаем кеш периодически. Решение - освободить память по истечении тайм-аута. Давайте сделаем это.

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

Мы видим, что объем памяти увеличивается, но не тогда, когда мы освобождаем кеш. Красиво и модно! Нам нужно убедиться, что мы освобождаем кеш через определенное время, чтобы он не занимал место в памяти.

Мы можем улучшить это решение, используя более полный механизм кэширования, такой как LRU cache. Но сегодня мы туда не поедем. Более подходящим надежным решением, которое не хранит кеши в памяти машины, является использование выделенных серверов кэширования, таких как Redis или Memcached. Мы рассмотрим это в следующей статье.

Обещания

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

Обещания сохраняются в памяти и не сбрасываются, пока не будут обработаны или отклонены

Давайте посмотрим на простой сценарий, в котором обещаниям может потребоваться больше времени для выполнения и сохранения в памяти.

Здесь у нас есть обещание задачи, которое выполняется после определенного периода ожидания. Мы отправляем случайные числа от 100 мс до 20 с в функцию задачи, которая потребуется для выполнения обещания. Для каждого запроса мы создаем обещание, которое выполняется в пределах от 100 до 20 секунд. Давайте проверим это под нагрузкой.

Мы видим стабильное увеличение памяти. Но если обещания выполняются по прошествии определенного периода времени, почему увеличивается память? Потому что обещания остаются в памяти. Мы не можем бежать вечно, ожидая исполнения обещаний. Что, если они никогда не разрешатся? Давайте посмотрим на дамп кучи, чтобы убедиться, что это так.

И на снимке ясно видно, что обещания находятся в памяти. Программа остановилась, но обещания, которые так и не были выполнены, остались в памяти. Решение состоит в том, чтобы ограничить время выполнения обещания и тайм-аут после этого. Кроме того, в процессе производства мы должны отслеживать эти обещания и их задачи, чтобы их можно было возобновить. Но вот давайте посмотрим, как мы можем это решить.

Мы используем Promise.race, функцию, которая решает, разрешено ли какое-либо из обещаний в массиве, и отклоняет неразрешенные. Мы можем создать timeout обещание, которое будет максимальным временем выполнения обещания. Вот 500 мс, а потом вернется. Посмотрим, решит ли это нашу проблему с памятью.

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

Заключение

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

Ссылка на проект: https://github.com/Joker666/nodejs-memory-leak

Ресурсы