Статьи

Отладка утечки памяти на Heroku

Это один из самых частых вопросов, которые мне задают клиенты Heroku Ruby: «Как отладить утечку памяти?»

Память важна. Если вам этого не достаточно, вы в конечном итоге будете использовать  память подкачки  и действительно замедляете работу своего сайта. Так что вы делаете, когда думаете, что у вас утечка памяти?

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

«У вас, вероятно, нет утечки памяти. Но что, если ты это сделаешь? — через @codeship
Click To Tweet

Когда ваше приложение загружается, оно потребляет базовый объем оперативной памяти. Он продолжает расти, пока обслуживает страницы, пока не выровняется Эта кривая выглядит как  горизонтальная асимптотическая функция :

Горизонтально-асимптотическая функция

В то время как значение со временем возрастает, оно плато и в конечном итоге колеблется вокруг стабильной точки. Истинная утечка памяти не выравнивается и продолжает бесконечно увеличиваться с течением времени. Причина, по которой большинство людей думает, что это утечка памяти, заключается в том, что они просматривают только небольшой кусочек данных. Если вы увеличите масштаб до той же кривой, это не похоже на то, что вы планируете остановиться:

Не просто просматривать небольшой раздел данных

Если ваше приложение использует больше памяти, чем должно, но оно не будет бесконечно долго потреблять память, значит, у вас раздувание памяти.

Сокращение количества процессов: самые низкие висячие фрукты

Если вы запускаете приложение Ruby в рабочей среде, оно должно быть запущено на параллельном веб-сервере. Это веб-сервер, способный обрабатывать несколько запросов одновременно. Heroku рекомендует   веб-сервер Puma . Большинство современных одновременных веб-серверов позволяют запускать несколько процессов для достижения параллелизма. Puma называет их «работниками», и вы можете установить это значение в своем  config/puma.rb файле:

workers Integer(ENV['WEB_CONCURRENCY'] || 2)

Каждый раз, когда вы увеличиваете свою  WEB_CONCURRENCY ценность, вы получаете дополнительную вычислительную мощность, но за счет большего объема памяти. Если ваше приложение выходит за пределы вашей оперативной памяти и переходит в режим свопинга, самое простое — уменьшить количество работников, которых вы используете; скажем, переход от четырех рабочих к трем или от двух рабочих к одному (при условии, что вы все еще получаете параллелизм через потоки).

Однако вы заметите, что переход с четырех процессов на три не сократит использование вашей памяти на четверть. Это потому, что современные версии Ruby   дружественны для копирования при записи . То есть несколько процессов могут совместно использовать память, если они не пытаются изменить ее. Например:

# Ruby 2.2.3

array = [] 
100_000_000.times do |i| 
  array << i 
end

ram = Integer(`ps -o rss= -p #{Process.pid}`) * 0.001 
puts "Process: #{Process.pid}: #{ram} mb"

Process.fork do 
  ram = Integer(`ps -o rss= -p #{Process.pid}`) * 0.001 
  puts "Process: #{Process.pid}: #{ram} mb" 
end

Когда мы запускаем это, первый процесс использует сотни мегабайт памяти. Однако наш раздвоенный процесс очень мал:

Process: 99526: 788.584 mb 
Process: 99530: 1.032 mb

Это надуманный пример, который показывает, что разветвленные процессы меньше. В случае с Puma раздвоенные рабочие будут меньше, но, безусловно, не в 1/788 раза.

Работа с меньшим количеством работников имеет свои недостатки — самое главное, ваша пропускная способность снизится. Если вы не хотите работать с меньшим количеством рабочих, то вам нужно найти горячие точки создания объектов и выяснить, как их обойти.

Роллинг Рестартс

Поскольку ваше приложение со временем будет медленно расти в памяти, мы можем остановить его слишком большое увеличение, периодически перезапуская работников. Вы можете сделать это с помощью  Puma Worker Killer . Сначала добавьте драгоценный камень в свой Gemfile:

gem 'puma_worker_killer'

Затем в инициализаторе, например  config/initializers/puma_worker_killer.rb, добавьте это:

PumaWorkerKiller.enable_rolling_restart

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

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

Вот и все для моих трюков с веб-сервером. Давайте посмотрим, как на самом деле уменьшить использование памяти.

«Как бороться с раздуванием памяти против утечки памяти» — через @codeship
Click To Tweet

Память при загрузке

Самый простой способ поместить ваше приложение Ruby на диету — это удалить библиотеки, которые вы не используете. Полезно видеть влияние памяти, которое каждая библиотека оказывает на вашу память во время загрузки. Чтобы помочь с этим, я написал инструмент под названием «  сорванные тесты» . Добавьте это в свой Gemfile:

gem 'derailed'

После запуска  $ bundle installвы можете увидеть использование памяти каждой из ваших библиотек:

$ bundle exec derailed bundle:mem 
TOP: 54.1836 MiB 
  mail: 18.9688 MiB 
    mime/types: 17.4453 MiB 
    mail/field: 0.4023 MiB 
    mail/message: 0.3906 MiB 
  action_view/view_paths: 0.4453 MiB 
    action_view/base: 0.4336 MiB

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

В верхней части вашего Gemfile добавьте:

gem 'mime-types', '~> 2.6.1', require: 
'mime/types/columnar'

Сокращение вашего Gemfile помогает уменьшить вашу начальную память. Вы можете думать об этом как о минимальном объеме оперативной памяти, необходимой для загрузки вашего приложения. Как только ваше приложение начнет отвечать на запросы, память будет только расти. Чтобы лучше понять, как эти два типа памяти взаимодействуют, вы можете посмотреть мой доклад, который  описывает, как Ruby использует память .

! Новый призыв к действию

Меньше объектов во время выполнения

После сокращения Gemfile, следующий шаг — выяснить, откуда берется основная часть ваших выделений времени выполнения. Вы можете использовать размещенные сервисы профилирования, такие  как трассировка памяти Skylight . В качестве альтернативы, вы можете попытаться воспроизвести увеличение памяти локально с помощью Derailed.

После того, как ваше  приложение настроено на загрузку по сбою , вы сможете увидеть, какие строки вашего приложения выделяют больше всего объектов при достижении конечной точки:

$ bundle exec derailed exec perf:objects

Это будет использовать  memory_profiler  для вывода набора информации о распределении. Самый интересный раздел для меня это  allocated memory by location. Это покажет номера строк, где выделен большой объем памяти.

Хотя часто это будет в некоторой степени соответствовать  allocated objects by location, важно отметить, что не все объекты распределяются одинаково. Многоуровневый вложенный хеш займет больше места, чем короткая строка. Лучше сосредоточиться на уменьшении выделения, а не на уменьшении количества объектов.

Скорее всего, вы получите много шума от библиотек при запуске этой команды:

$ bundle exec derailed exec perf:objects
# ...

## allocated memory by location
---------------------------------------
  1935548 /Users/richardschneeman/.gem/ruby/2.2.3/gems/actionpack-4.2.3/lib/action_dispatch/journey/formatter.rb:134 
  896100 /Users/richardschneeman/.gem/ruby/2.2.3/gems/pg-0.16.0/lib/pg/result.rb:10 
  741488 /Users/richardschneeman/.gem/ruby/2.2.3/gems/activerecord-4.2.3/lib/active_record/result.rb:116 
  689299 /Users/richardschneeman/.gem/ruby/2.2.3/gems/activesupport-4.2.3/lib/active_support/core_ext/string/output_safety.rb:172 
  660672 /Users/richardschneeman/.gem/ruby/2.2.3/gems/actionpack-4.2.3/lib/action_dispatch/routing/route_set.rb:782 
  606384 /Users/richardschneeman/Documents/projects/codetriage/app/views/repos/\_repo.html.slim:1 
  579384 /Users/richardschneeman/.gem/ruby/2.2.3/gems/activesupport-4.2.3/lib/active\_support/core_ext/string/output_safety.rb:260 
  532800 /Users/richardschneeman/.gem/ruby/2.2.3/gems/actionpack-4.2.3/lib/action_dispatch/routing/route_set.rb:275 
  391392 /Users/richardschneeman/.gem/ruby/2.2.3/gems/activerecord-4.2.3/lib/active_record/attribute.rb:5 
  385920 /Users/richardschneeman/.gem/ruby/2.2.3/gems/temple-0.7.5/lib/temple/utils.rb:47

Несмотря на то, что большая часть выходных данных поступает из драгоценных камней, таких как  actionpack и  pg, вы можете видеть, что некоторые из них получены из моего приложения, на котором работает /Users/richardschneeman/Documents/projects/codetriage. Это приложение с открытым исходным кодом; Вы можете запустить его локально, если хотите:  CodeTriage .

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

$ env ALLOW_FILES=codetriage bundle exec derailed exec perf:objects
# ...

## allocated memory by location
------------------------------------
    606384  /Users/richardschneeman/Documents/projects/codetriage/app/views/repos/_repo.html.slim:1
    377145  /Users/richardschneeman/Documents/projects/codetriage/app/views/layouts/application.html.slim:1
    377145  /Users/richardschneeman/Documents/projects/codetriage/app/views/application/_repos.html.slim:1
     71040  /Users/richardschneeman/Documents/projects/codetriage/app/views/repos/_repo.html.slim:2
     35520  /Users/richardschneeman/Documents/projects/codetriage/app/models/repo.rb:84
     19272  /Users/richardschneeman/Documents/projects/codetriage/app/views/application/_repos.html.slim:6
      1785  /Users/richardschneeman/Documents/projects/codetriage/app/views/application/_head.html.slim:1
      1360  /Users/richardschneeman/Documents/projects/codetriage/app/views/application/_repos.html.slim:10
      1049  /Users/richardschneeman/Documents/projects/codetriage/app/views/application/_footer.html.slim:1

Это приложение уже довольно оптимизировано, но даже при этом мы можем сделать некоторые микрооптимизации. В  app/models/repo.rb строке 84 мы видим это:

def path 
  "#{user_name}/#{name}" 
end

Здесь мы берем две строки для создания пути GitHub, как "schneems/derailed_benchmarks". Нам не нужно выделять эту новую строку, поскольку мы уже храним эту информацию в базе данных в поле,  full_name которое уже загружено из базы данных и выделено. Мы можем оптимизировать этот код, изменив его на:

def path 
  full_name 
end

Теперь, когда мы перезапустим наш тест:

allocated memory by location
-----------------------------
    606384  /Users/richardschneeman/Documents/projects/codetriage/app/views/repos/_repo.html.slim:1
    377145  /Users/richardschneeman/Documents/projects/codetriage/app/views/layouts/application.html.slim:1
    377145  /Users/richardschneeman/Documents/projects/codetriage/app/views/application/_repos.html.slim:1
     71040  /Users/richardschneeman/Documents/projects/codetriage/app/views/repos/_repo.html.slim:2
     # models/repo.rb:84 is not present, because we optimized it
     19272  /Users/richardschneeman/Documents/projects/codetriage/app/views/application/_repos.html.slim:6
      1785  /Users/richardschneeman/Documents/projects/codetriage/app/views/application/_head.html.slim:1
      1360  /Users/richardschneeman/Documents/projects/codetriage/app/views/application/_repos.html.slim:10
      1049  /Users/richardschneeman/Documents/projects/codetriage/app/views/application/_footer.html.slim:1

Вы можете видеть, что  models/repo.rb:84 линии больше нет. В этом примере мы экономим 35 520 байт при каждой загрузке страницы. Если вы смотрели на количество объектов:

888 /Users/richardschneeman/Documents/projects/codetriage/app/models/repo.rb:84

Мы экономим 888 строк из выделенных. В этом случае изменение настолько простое, что нет недостатка в совершении оптимизации. Однако не все изменения памяти или производительности настолько просты. Как вы можете сказать, когда оно того стоит?

Я бы порекомендовал проверить до и после использования  бенчмарка / ips с derailed . По моим подсчетам, на MacBook Air с 8 ГБ оперативной памяти и 1,7 ГГц процессором требуется около 70 000 небольших выделенных строк, чтобы добавить до 1 мс среды Ruby. Возможно, это намного больше энергии, чем у вашего рабочего сервера, если вы работаете на общей или «облачной» вычислительной платформе, так что вы, вероятно, увидите большую экономию. Если есть сомнения по поводу изменений, всегда ориентируйтесь.

«Если сомневаешься в переменах, всегда проверяй». — @ schneems
Click To Tweet

Вы можете повторить выполнение этих тестов для разных конечных точек, используя  PATH_TO_HITпеременную.

$ env ALLOW_FILES=codetriage PATH_TO_HIT=/repos bundle exec derailed exec perf:objects

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

Хорошо, вы можете увидеть утечку памяти

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

Вы можете сделать это, выполнив команду:

$ bundle exec derailed exec perf:mem_over_time

Booting: production 
Endpoint: "/" 
PID: 78675 
103.55078125 
178.45703125 
179.140625 
180.3671875 
182.1875 
182.55859375
# ...
183.65234375 
183.26171875 
183.62109375

Вы можете увеличить количество итераций, которые выполняет этот тест, используя  TEST_COUNT переменную окружения:

$ env TEST_COUNT=20_000 bundle exec derailed exec perf:mem_over_time

Это также создает разделенный новой строкой набор значений в файле в вашей  tmp/ папке. Мне нравится копировать эти значения и вставлять их в электронную таблицу Google. Вот пример:

Память против времени

Не забывайте всегда маркировать свою ось; в частности, не забудьте включить единицы измерения. Здесь мы видим, что нет утечки в CodeTriage на главной странице.

Однако, если ваш график идет вверх и вправо, что делать? К счастью, устранение утечки памяти — это то же самое, что обнаружение и удаление раздувания памяти. Если ваша утечка занимает много времени, чтобы стать значительным, вам может потребоваться выполнить  perf:objects команду с более высоким значением, TEST_COUNT чтобы сигнал вашей утечки был выше, чем шум нормальной генерации объекта.

В заключении

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

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

Если вы использовали все приемы, упомянутые здесь, и у вас по-прежнему возникают ошибки памяти ( R14 ), возможно, пришло время  перейти на больший динамометрический блок  с большим объемом памяти. Если вы перейдете к «производительному» dyno, у вас будет собственный выделенный экземпляр среды выполнения, и вам не придется беспокоиться о «шумных соседях».

«Отладка утечки памяти в @Heroku» — через @codeship
Click To Tweet

Пост «  Отладка утечки памяти» на Heroku  впервые появился в  @codeship .