Страницы

Поиск по вопросам

четверг, 30 мая 2019 г.

rails puma постоянный рост используемой памяти

Имеется небольшое приложение на rails(4.2.6), в качестве веб-сервера используется nginx+puma(3.4.0). Сервер Debian 8.1 64bit. Заметил одну странную вещь, с каждым запросом, используемое процессами puma количество памяти растет, и никогда не снижается(во время отсутствия каких либо запросов). Это приводит к тому, что ОЗУ на сервере (512 мб) заканчивается, и останавливается служба postgres(9.4). Пробовал разное количество воркеров, нитей, работа в кластерном режиме и в обычном - результат всегда один и тот же. После нескольких тяжелых запросов ОЗУ заканчивается. Неужели так и должно быть? Какие есть пути устранения такой проблемы? По информации, которую я нашел, Puma - это один из лучших выборов для избежания проблемы медленных клиентов и долгих запросов. Текущий конфиг такой:
#!/usr/bin/env puma
...
threads 2,4
... workers 1
preload_app!
on_restart do puts 'Refreshing Gemfile' ENV["BUNDLE_GEMFILE"] = "/home/.../current/Gemfile" end
on_worker_boot do ActiveSupport.on_load(:active_record) do ActiveRecord::Base.establish_connection end end


Ответ

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

Да, это из-за особенностей работы GC в самом Ruby. Он держит собственный пул памяти, увеличивая его при необходимости, но никогда не уменьшая. И тому есть причина.
Объекты, "подметённые" сборщиком мусора, освобождают память с точки зрения интерпретатора, но не с точки зрения ОС. Посему, только тот факт, что память занята процессом, не означает, что в ней действительно есть что-то ценное для программы, скорее всего, это просто "запас", в котором интерпретатор будет размещать новые объекты сам, не дёргая аллокатор ОС.
Соответственно, в каждый момент времени процесс будет занимать максимум того, что ему было нужно за всё время его жизни. Размер пула будет с небольшим запасом равен пиковому потреблению памяти.

Беда с большими объектами в том, что они требуют большие последовательные области памяти. И если такой блок в пуле не находится, то... пул ещё увеличивается на такую величину, чтобы большой объект влез!
Эту проблему можно было б частично победить, используя сборщик мусора с "уплотнением" (compaction), когда GC в процессе работы "перекладывает" мелкие объекты поближе друг к другу, тем самым образуя более крупные последовательные области свободной памяти. Это же могло бы позволить отдавать крупные "хвосты" незанятой памяти обратно в ОС. Но это здорово усложняет работу С-шных расширений, которые запоминают, где был объект, непосредственно по адресу в памяти. Нельзя просто сказать им "я вон тот объект подвинул, имей в виду".

Сделать можно много чего.
Пол-гигабайта на целое рельсовое приложение и хранилища данных это... крайне немного. Есть смысл добавить, если не физической ОЗУ, то хотя бы подкачки. Подкрутить garbage collector на более осторожное расширение пула и частые срабатывания. Это вряд ли поможет, но может немножко отсрочить неизбежное. Это довольно обширная и опасная тема, требующая тщательного стресс-тестирования на каждое изменение. (Самый богатый на приключения) Реально снизить пиковое потребление памяти, отдавая большие ответы по кусочкам, чтобы GC успевал подчищать то, что уже отослано клиенту. Есть ActionController::Live, с помощью которого можно писать ответ в поток по кусочкам, не загружая все исходные данные для него в память.

Комментариев нет:

Отправить комментарий