Устранение утечки больших двоичных файлов
У меня есть приложение elixir/OTP, которое выходит из строя из-за проблемы с памятью. Функция, вызывающая сбой, вызывается каждые 6 часов в специальном процессе. Для запуска требуется несколько минут (~ 30) и выглядит следующим образом:
def entry_point do
get_jobs_to_scrape()
|> Task.async_stream(&scrape/1)
|> Stream.map(&persist/1)
|> Stream.run()
end
На моей локальной машине я вижу постоянный рост потребления памяти больших двоичных файлов при выполнении функции:
![Использование памяти наблюдателя показывает постоянный рост памяти больших двоичных файлов]()
Обратите внимание, что когда я вручную запускаю сбор мусора в процессе, который выполняет эту функцию, потребление памяти значительно падает, поэтому это определенно не проблема с несколькими разными процессами, неспособными к GC, но только с тем, что не GC должным образом. Кроме того, важно сказать, что каждые несколько минут процесс справляется с GC, но иногда этого недостаточно. Производственный сервер имеет только 1 ГБ оперативной памяти, и он сработает до того, как GC запустится.
Пытаясь решить проблему, я столкнулся с Erlang in Anger (см. страницы 66-67). Одно из предложений - положить все большие манипуляции с бинарниками в одноразовых процессах. Возвращаемое значение функции scrape
- это карта, содержащая большие двоичные файлы. Поэтому они распределяются между Task.async_stream
"рабочими" и процессом, выполняющим функцию. Поэтому теоретически я мог бы поставить persist
вместе с scrape
внутри Task.async_stream
. Я предпочитаю не делать этого и поддерживать вызовы persist
в процессе.
Другое предложение - периодически называть :erlang.garbage_collect
. Похоже, он решает проблему, но чувствует себя слишком хаки. Автор этого также не рекомендует. Здесь мое текущее решение:
def entry_point do
my_pid = self()
Task.async(fn -> periodically_gc(my_pid) end)
# The rest of the function as before...
end
defp periodically_gc(pid) do
Process.sleep(30_000)
if Process.alive?(pid) do
:erlang.garbage_collect(pid)
periodically_gc(pid)
end
end
И приведенная загрузка памяти:
![использование памяти наблюдателя после GC hack]()
Я не совсем понимаю, как другие предложения в книге соответствуют этой проблеме.
Что бы вы порекомендовали в этом случае? Храните хакерское решение или есть лучшие варианты.
Ответы
Ответ 1
Виртуальная машина erlang имеет механизм сбора мусора, который по умолчанию оптимизирован для коротких данных. Недолговечный процесс может вообще не собираться с мусором до тех пор, пока он не умрет, и большая часть сборок мусора проверяет только новые предметы. Элементы, пережившие прогон GC, снова не будут проверяться до тех пор, пока не будет выполнена полная развертка.
Я предлагаю вам попробовать настроить флаг fullsweep_after. Его можно установить глобально через :erlang.system_flag(:fullsweep_after, value)
или для вашего конкретного процесса, используя :erlang.spawn_opt/4
.
Из документов:
В системе времени выполнения Erlang используется схема сбора мусора поколения, использующая "старую кучу" для данных, сохранивших хотя бы одну сборку мусора. Когда на старой куче больше нет места, производится сборка мусора с полной загрузкой.
Опция fullsweep_after позволяет указать максимальное количество коллекций поколений до форсирования полной паузы, даже если есть место на старой куче. Установка числа в ноль отключает общий алгоритм сбора, т.е. Все живые данные копируются в каждой сборке мусора.
Несколько случаев, когда может быть полезно изменить fullsweep_after:
- Если бинарные файлы, которые больше не используются, должны быть выброшены как можно скорее. (Установите номер в ноль.)
- Процесс, который в основном имеет недолговечные данные, заполняется редко или никогда, т.е. старая куча содержит в основном мусор. Чтобы обеспечить полную паузу, установите Number на подходящее значение, например 10 или 20.
- Во встроенных системах с ограниченным объемом оперативной памяти и без виртуальной памяти вы можете сохранить память, установив Number в ноль. (Значение можно задать глобально, см. Erlang: system_flag/2.)
Значение по умолчанию - 65535 (если вы уже не изменили его с помощью переменной окружения ERL_FULLSWEEP_AFTER
), поэтому любое меньшее значение сделает сборку мусора более агрессивной.
Это хорошее чтение по теме: https://www.erlang-solutions.com/blog/erlang-19-0-garbage-collector.html