memory-leaks #garbage-collection #v8 #embedded-v8
#утечки памяти #сбор мусора #версия 8 #встроенный -v8
Вопрос:
Я встроил версию 8 9.5 в свое приложение (HTTP-сервер C ). Когда я начал использовать необязательную цепочку в своих JS-скриптах, я заметил ненормальный рост потребления памяти при большой нагрузке (CPU), приводящий к ООМ. Пока есть немного свободного процессора, использование памяти нормальное. Я отобразил HeapStats V8 в grafana (это только для 1 изолят, которых у меня 8 в моем приложении).
При большой нагрузке наблюдается всплеск peak_malloced_memory
, в то время как другие показатели страдают гораздо меньше и кажутся нормальными. Я передал --expose-gc
флаг в V8 и вызвал gc()
в конце моего скрипта. Это полностью решило проблему и peak_malloced_memory
не поднимается подобным образом. Кроме того, при повторном вызове gc()
я мог бы освободить всю дополнительную память, потребляемую без него. --gc-global
тоже работает. Но эти подходы кажутся скорее обходным путем, чем готовым к производству решением. --max-heap-size=64
и --max-old-space-size=64
не имело никакого эффекта — потребление памяти по-прежнему значительно превышало 8 (количество изолятов в моем приложении) * 64 МБ (> 2 ГБ физической оперативной памяти).
Я не использую в своем приложении какой-либо связанный с GC API V8.
Мое приложение создает v8::Isolate
и v8::Context
один раз и использует их для обработки HTTP-запросов.
Такое же поведение в версии 9.7.
Ubuntu ubuntu
Встроенный V8 с этими args.gn
dcheck_always_on = false
is_debug = false
target_cpu = "x64"
v8_static_library = true
v8_monolithic = true
v8_enable_webassembly = true
v8_enable_pointer_compression = true
v8_enable_i18n_support = false
v8_use_external_startup_data = false
use_thin_lto = true
thin_lto_enable_optimizations = true
x64_arch = "sandybridge"
use_custom_libcxx = false
use_sysroot = false
treat_warnings_as_errors = false # due to use_custom_libcxx = false
use_rtti = true # for sanitizers
А затем вручную превратил статическую библиотеку в динамическую с помощью этого (возникли некоторые проблемы со связыванием со статической библиотекой из-за LTO, с которыми я не хотел иметь дело в будущем):
../../../third_party/llvm-build/Release Asserts/bin/clang -shared -o libv8_monolith.so -Wl,--whole-archive libv8_monolith.a -Wl,--no-whole-archive -flto=thin -fuse-ld="lld"
Я провел некоторое нагрузочное тестирование (поскольку проблема возникает только под нагрузкой) с вызовом вручную и без gc()
него, и это график использования ОЗУ во время нагрузочного тестирования с временными метками:
- Запущенное нагрузочное тестирование с
gc()
вызовом: «утечки» нет - Удален
gc()
вызов и запущен другой сеанс нагрузочного тестирования: «утечка» - Возвращен ручной
gc()
вызов при низкой нагрузке: использование памяти начало постепенно уменьшаться. - Запущен еще один сеанс нагрузочного тестирования (
gc()
все еще в сценарии): использование памяти быстро снизилось до базовых значений.
Мои вопросы:
- Нормально ли, что peak_malloced_memory может превышать total_heap_size?
- Почему это может произойти только при использовании необязательной цепочки JS?
- Существуют ли какие-либо другие, более правильные решения этой проблемы, кроме постоянного принудительного выполнения полного GC?
Ответ №1:
(Разработчик версии 8 здесь.)
- Нормально ли, что peak_malloced_memory может превышать total_heap_size?
Выделенная память не связана с кучей, так что да, когда куча крошечная, то выделенная память (которая обычно также невелика) может значительно превышать ее, возможно, только на короткое время. Обратите внимание, что пиковая объемная память (53 Мбайт на вашем скриншоте) не является текущей объемной памятью (24 КБ на вашем скриншоте); это самый большой объем, который использовался в любой момент в прошлом, но с тех пор был освобожден (и, следовательно, не является утечкой и не приведет к переполнению ООМвремя).
Не являясь частью кучи, выделенная память не зависит ни от --max-heap-size
or --max-old-space-size
, ни от gc()
вызовов вручную.
- Почему это может произойти только при использовании необязательной цепочки JS?
Это не имеет смысла, и я уверен, что происходит что-то еще.
- Существуют ли какие-либо другие, более правильные решения этой проблемы, кроме постоянного принудительного выполнения полного GC?
Я не уверен, что такое «эта проблема». Краткий пик выделенной памяти (которая вскоре снова освобождается) должен быть в порядке. В заголовке вашего вопроса упоминается «утечка», но я не вижу никаких признаков утечки. В вашем вопросе также упоминается ООМ, но на графике не показано ничего связанного (текущее потребление памяти менее 10 МБАЙТ в конце отображаемого временного окна с физической памятью 2 ГБ), поэтому я не уверен, что с этим делать.
Принудительное выполнение GC вручную, безусловно, не очень хорошая идея. Тот факт, что это вообще влияет на (не-GC’ed!) выделенную память, вызывает удивление, но может иметь вполне обыденное объяснение. Например (и я дико размышляю здесь, поскольку вы не предоставили пример воспроизведения или другие более конкретные данные), может случиться так, что краткосрочный пик вызван оптимизированной компиляцией, а при принудительном запуске GC вы уничтожаете столько обратной связи по типу, что оптимизированная компиляция никогда не происходит.
С удовольствием рассмотрю подробнее, если вы предоставите больше данных, например, пример воспроизведения. Если единственная «проблема», которую вы видите, это то, что peak_malloced_memory
больше размера кучи, то решение просто не беспокоиться об этом.
Комментарии:
1. Спасибо за помощь. Приложение потребляло> 2 ГБ оперативной памяти без увеличения размера кучи V8, вот почему вы не видели этого на графике — это меня тоже смущает. Интересно, что вы упомянули оптимизированную компиляцию. Когда я профилировал свое приложение с помощью jemalloc, единственное, что я там увидел, что было связано с V8, была оптимизированная компиляция, но она показала небольшой процент, поэтому я отказался от нее. Вот ссылка , если вы хотите увидеть символы версии 8, которые появились в отчете профилировщика. Но как оптимизированная компиляция может занимать так много памяти?
2. Я также обновил свой вопрос. Я добавил график использования оперативной памяти, который иллюстрирует, как вызов
gc()
при каждом выполнении JS (обработка запросов) влияет на потребление памяти.3. Все это создало у меня впечатление, что некоторые объекты в куче не учитываются, и V8 считает, что он не потребляет память, следовательно, нет необходимости запускать глобальный / полный GC. Я понятия не имею, как это могло произойти, но это может объяснить, почему принудительный сборщик данных помогает вернуть память. (Скорее всего, это очень далеко от истины, поскольку я не знаю, как работает V8, просто делюсь своими мыслями)
4. Связанный профиль jemalloc показывает 14 МБ памяти компилятора, что не похоже на проблему. Возможно, вы выделяете огромные объемы внешней памяти (внешние строки или массивы большого типа), поддерживаемые объектами JS? Если вы выделяете пользовательские объекты, обязательно используйте
v8::Isolate::AdjustAmountOfExternalAllocatedMemory
их соответствующим образом. Чтобы сэкономить процессор, GC не будет работать много, когда есть много свободной кучи; если вы хотите, чтобы он работал усерднее, несмотря на использование всего ~ 5 МБ, попробуйте поиграть с максимальным размером кучи. Я подозреваю, что проблема где-то совсем в другом месте (понятия не имею, где).5. Да, именно поэтому я тоже отказался от этого… Я не использую V8 для управления временем жизни моих объектов, поэтому я не думаю, что это проблема, также не объясняет, почему это происходит только при использовании
?.
синтаксиса. До этого я был на версии 6.0 и, поскольку тогда не было поддержки дополнительной цепочки, использовал простую функциюfunction __safe_get__(value, key) { if (typeof value === 'object' amp;amp; value !== null amp;amp; key in value) { return value[key]; } return undefined; }
, но после того, как я заменил это на?.
/?.[]
memory, попрощался)
Ответ №2:
Я думаю, что я добрался до сути этого…
Оказывается, это было вызвано --concurrent-recompilation
функцией V8 в сочетании с нашей конфигурацией jemalloc.
Похоже, что при использовании дополнительной цепочки вместо функции, написанной вручную, V8 более агрессивно пытается оптимизировать код одновременно и выделяет для этого гораздо больше памяти (статистика зоны показала> 70 МБ памяти на изолят). И это происходит специально при высокой нагрузке (возможно, только тогда V8 замечает горячие функции).
jemalloc, в свою очередь, по умолчанию имеет 128 арен и background_thread
отключен. Поскольку при параллельной перекомпиляции оптимизация выполняется в отдельном потоке, оптимизатор TurboFan версии 8 в конечном итоге выделил много памяти на отдельной арене jemalloc, и хотя V8 освободил эту память, из-за стратегии распада jemalloc и из-за того, что к этой арене больше нигде не обращались, страницы не были очищены, таким образомувеличение резидентной памяти.
Статистика Jemalloc:
перед утечкой памяти:
Allocated: 370110496, active: 392454144, metadata: 14663632 (n_thp 0), resident: 442957824, mapped: 570470400, retained: 240078848
После утечки памяти:
Allocated: 392623440, active: 419590144, metadata: 22934240 (n_thp 0), resident: 1712504832, mapped: 1840152576, retained: 523337728
Как вы можете видеть, хотя выделенная память составляет менее 400 МБ, RSS составляет 1,7 Гб из-за ~ 300000 грязных страниц (~ 1,1 ГБ). И все эти грязные страницы разбросаны по нескольким аренам с 1 связанным потоком (тем, на котором оптимизатор TurboFan V8 выполнял параллельную перекомпиляцию).
--no-concurrent-recompilation
решена проблема, и я думаю, что это оптимально в нашем случае использования, когда мы выделяем изоляцию для каждого ядра процессора и равномерно распределяем нагрузку, поэтому нет особого смысла выполнять перекомпиляцию одновременно с точки зрения пропускной способности.
Это также можно решить на стороне jemalloc с MALLOC_CONF="background_thread:true"
помощью (что, предположительно, может привести к сбою) или путем уменьшения количества арен MALLOC_CONF="percpu_arena:percpu"
(что может увеличить конкуренцию). MALLOC_CONF="dirty_decay_ms:0"
также исправлена проблема, но это неоптимальное решение.
Не уверен, как принудительный сбор данных помог восстановить память, возможно, это каким-то образом вызвало доступ к этим аренам jemalloc, не выделяя на них много памяти.