Сборка мусора в Erlang

Как Erlang использует преимущества многоядерных процессоров?

О том, как происходит сбор мусора в Erlang, рассказал наш ведущий программист Борис Кузнецов на выступлении на CodeBeam STO в Стокгольме, родине языка Erlang. Видео (на английском) ниже:

Erlang — язык программирования общего назначения с выраженной динамической типизацией. Одна из фишек виртуальной машины Erlang — взаимодействие на уровне процессов. На сегодняшний день компьютерные процессоры по существу остановились в росте частоты и теперь прогрессируют в числе ядер. Реализованный на Erlang софт даёт возможность использовать преимущества многоядерных процессоров.

Erlang можно использовать в реализации backend для устойчивых к отказам веб-приложений, мессенджеров, систем мониторинга и других приложений с требованиями soft-realtime. Elixir так же использует виртуальную машину Erlang для своей работы, что делает его отличным выбором для написания современных приложений.

Каждому ядру — свой планировщик

При исполнении кода вся суть преимущества кроется в планировщике, который работает на каждом ядре ОС со своей очередью процессов. Для каждого процесса установлено ограничение на индивидуальное число вызовов функций. Пока ограничение не превышено, процесс делает то, что должен, затем стартует следующий в очереди.

Erlang сборщик мусора схема

На виртуальном сервере Erlang процесс на 100% обособлен и не взаимодействует с работающими процессами в операционной системе. У него персональная память, в которой находятся предписания и правила работы (стек), и та информация, которая ему требуется для реализации поставленной задачи («куча», heap).

erlang процессы

Каждый процесс можно поделить на две составляющих:

  • Controller, в котором хранятся общие данные процесса (аналитика, счётчики, ссылки на область памяти);
  • Область памяти, где хранятся инструкции и данные необходимые для выполнения программы.

1 раз на 65 000 циклов

Сборщик мусора (Garbage Collector, GC) включается в тот момент, когда для сохранения новой сущности не хватает памяти. Он очищает ту информацию, которая больше не пригодится в программе, а потом запускает дальнейшее выполнение кода.

Сборщик мусора в Erlang делит предметы из кучи на два поколения: новые и те, что остались после предыдущих сборок мусора. Их называют «долгоживущими объектами», и GC возвращается к ним раз в 65 000 циклов.

Малый цикл

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

  • GC определяет все root objects (корневые объекты) — очередь из сообщений, информация об ошибке, словарь, все места, где можно встретить ссылку на информацию, находящуюся в памяти процесса. Сборщик фильтрует объекты и перемещает в новый сектор памяти лишь те, на которые он видит ссылку.
  • Дальше GC поочерёдно перемещает всю информацию, на которую ссылаются объекты уже в новом кластере памяти.
  • После он переносит стек в новую область, и стандартный цикл считается законченным. Все не имеющие ссылок объекты удаляются. На оставшиеся наносится идентификационная метка, позволяющая в следующей итерации перенести их в old heap — подраздел памяти, в котором живут объекты старше 2 циклов.
сборщик мусора примерerlang example
erlang пример программыerlang heap
erlang схема

Отдельно обратим внимание на то, что в памяти процесса на противоположных друг другу концах отрезка находятся одновременно и стек, и куча, которые увеличиваются к центру. Как только такая встреча происходит, планировщик понимает, что больше нет пустого пространства, останавливает работу ПО конкретно на своём ядре, и в дело вступает GC, выделяющий новый сектор памяти и перемещающий туда лишь нужные объекты.

erlang process memory

Если объём первоначальной памяти слишком велик, копирование повторяется с целью его уменьшить.

Как поступать с объектами старого поколения

Старое поколение объектов (major collection) живет в Erlang в отдельном кластере памяти. GC приходит к ним раз в 65 000 обычных итераций. Когда этот момент всё-таки наступает, сборщик действует по старинке: выделяет новый сектор памяти, перемещает туда стек, живые объекты из major collection и свежие объекты из актуальной заполненной памяти.

erlang old heaperlang major collection
Major Collection in erlang

Поскольку за один цикл сборщик работал с большим количеством объектов, иногда это отнимало очень много времени (более 1 миллисекунды). Это оказывало отрицательное воздействие на планировщик — потому что он не был способен учесть это время в своей логике (из-за того, что выставляет лимит только на количество вызовов функций). Поэтому в прошлом в Erlang был баг, приводящий к сбою процесса при его длительном выполнении. Одной из причин могла быть как раз излишне длительная сборка мусора. Другой, к примеру, — вызов внешних библиотек (NIF).

Этот баг пофиксили. На всех ядрах стал устанавливаться дополнительный вид планировщика — Dirty scheduler. Его основное отличие в том, что у него единая очередь на все ядра. И в этой очереди оказываются процессы, у которых предполагается длительное выполнение в обход лимита.

erlang dirty scheduler

Как это работает?

  • Система замечает, что текущий кластер памяти заполнен.
  • Она оценивает, сколько времени займёт вызов сборщика мусора.
  • Если анализ показывает, что много, то на процесс вешается метка «переместить в dirty job».
  • Дальше процессу выделяется отдельный маленький сектор памяти, в котором совершает дальнейшую работу и подводит свой лимит к завершению.

Процессу нужно выделить память в данный момент — ведь у него сохранился его лимит на вызовы. Для этого применяется Delay GC — сборка мусора с отсрочкой выполнения. Эта система вешает на кучу метку abandoned heap, выделяет новое место под требуемые объекты и немного места про запас.

erlang abandoned heap

Если отданное место кончится, а лимит у процесса останется неизрасходованным, то система выделит ему новую область. Она продолжит это делать, пока процесс не израсходует свой лимит. На языке Erlang подобные сектора памяти называют heap fragments.

erlang heap fragments

Рост по Фибоначчи

В 21-й версии Erlang память процесса увеличивается по следующим закономерностям:

  • 23 увеличения случаются по Фибоначчи с исходниками 12 и 38.
  • Все последующие — на +20%.

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

В основном цикле garbage collection, касающемся абсолютно всех сфер памяти процесса, нужное место выделяется сразу. Когда сборка завершена, система исправляет объём: если он больше в 4 раза, то будет снижен в два.

Конкурентное преимущество

В других языках — например, в Ruby и Python, — чтобы понять, актуальны ли объекты в процессе garbage collection, нужно прекратить работу всей системы. В Erlang (и соответственно в Elixir) — нет! Потому что все процессы работают полностью обособленно.

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

Elixir — мой любимый инструмент, в который я влюбился после Ruby и редактора кода (Emacs). Благодаря блестящей реализации виртуальной машины мы можем писать надежные, масштабируемые и распределенные приложения с отказоустойчивостью и мягкими real-time требованиями.
Борис Кузнецов
Ведущий разработчик, Evrone
Связаться с нами
Нужна команда?
Давайте обсудим ваш проект
Прикрепить файл
Максимальный размер файла: 8 МБ.
Допустимые типы файлов: jpg jpeg png txt rtf pdf doc docx ppt pptx.