новости сообщество форум вики полезно

Erlang, binaries и сборка мусора (и тяжелый вздох)

07/12/2012 03:18

Erlang, binaries, and garbage collection (sigh)

Однажды создатели «крутой, напичканной баззвордами облачной системы»™ столкнулись с тем, что их система начала потреблять слишком много памяти. Проблема оказалась в binaries и в том, как с ними работает Erlang.

Так, в Erlang VM есть несколько способов работы с двоичными данными:

  • heap binaries — это двоичные данные размером до 64 байтов, которые хранятся в куче процесса, и работа с которыми происходит так же, как с любыми другими данными: они копируются при передаче в другой процесс, их собирает сборщик мусора, когда они больше не используются и т.п.
  • refc-binaries (reference-counted) — это двоичные данные размером больше 64 байтов. Они хранятся в отдельной области VM, а ссылка на эти данные (которая почему-то называется ProcBin) хранится в куче процесса. Если мы передаем эти данные в другой процесс, в куче того процесса создается новый ProcBin (на деле это не совсем так, но для достаточно для текущего объяснения. Главное, что передаются не сами данные, а ссылка на них)

Более того, Erlang проводит некоторые дополнительные оптимизации. При использовании erlang:split_binary/2 или при извлечении данных при помощи сопоставления с образцом результат не обязательно является новым binary. Вместо этого VM создает новый тип под названием sub-binary, который на деле является ссылкой на оригинальные данные — на heap binary или на refc-binary.

Таким образом, у VM есть 2x2=4 способа работы с двоичными данными.

Как вы уже могли себе представить, это весьма затрудняет работу сборщика мусора, так как мы можем удалить объект из памяти только тогда, когда на него больше никто не ссылается.

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

Таким образом, если вы вызвали split_binary/2 или вытащили кусок данных сопоставлением с образцом и т.п., до тех пор, пока все эти данные не будут удалены сборщиком мусора, оригинальные данные никуда не денутся и будут висеть в памяти.

Казалось бы, достаточно очевидным решением проблемы было бы использовать binary:copy, чтобы превратить данные в heap binary. Таким образом, казалось бы, на оригинальные данные никто не будет ссылаться, и их можно будет удалить из памяти.

К сожалению, это не так, если вы передаете большие двоичные данные между процессами.

Благодаря случайному сообщению Роберта Вирдинга на тему двоичных данных выяснилось следующее:

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

И в этом и заключается основная проблема: сборщик мусора должен сработать не только на процессах, которые что-то делают с этим данными, но и на любом процессе, который к ним «прикасался».

В частности, в описываемой системе некоторые процессы работали «роутерами», передавая входящие сообщения в другие процессы в зависимости от типа сообщения. Так как этот процесс практически ничего не делал, его практически никогда не очищал сборщик мусора. Что, соответственно, приводило к тому, что двоичные данные, проходящие через него, тоже никогда не удалялись сборщиком мусора. А так как похожих процессов было достаточно много, то множество данных продолжало висеть в памяти и потребление памяти росло.

Что можно по этому поводу сделать? К сожалению, нет единого универсального хорошего решения. Есть несколько возможных подходов:

  • В зависимости от кода/решения/алгоритма вашей задачи, можно использовать binary:copy до того, как передавать данные. Таким образом, мы превращаем данные в heap binaries, а оригинальные данные будут удалены из памяти когда умрет создававший их процесс. Это не всегда возможно, не говоря уже о накладных расходах на создание копии данных в куче процесса.
  • Если вы точно знаете, какой процесс является виновником происходящего, можно вручную вызвать erlang:garbage_collect. Это, правда, может привести к непредсказуемой потере производительности системы.
  • Вместо вызова grabage_collect можно так же использовать spawn_opt (при создании процессов вручную или через gen_*) и установить опцию {fullsweep_after, 0}, что позволит удалять большие данные настолько быстро, насколько это возможно. Безусловно, вместо нулевого значения можно использовать люое другое, если вы знаете, когда вы хотите запустить сборщик мусора.

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


 
 
 
 

так же

Авторы

Сюда ссылаются

twitter