Боремся с переполнением очередей сообщений в Erlang

17 июля 2013

Взаимодействие процессов в Erlang происходит путем обмена сообщений. И хотя сообщения в Erlang дешевы, они не бесплатны. Бездумная посылка сообщений неправо и налево может привести не только к существенному замедлению работы всего приложения, но и к его аварийному завершению.

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

#!/bin/sh

erl -name etop-`date +%s` -hidden -s etop -s erlang halt \
  -output text -node project@example.com -setcookie qwerty \
  -tracing off -sort msg_q -interval 5

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

Список процессов в etop

К сожалению, вывод etop не особо информативен. Наиболее подробный отчет можно получить, зайдя на ноду по remsh:

erl -name remsh-`date +%s` -setcookie qwerty -remsh project@example.com

… и воспользовавшись функциями processes/0 и process_info/2. Например, как-то так:

rp([ {P, RN, L, IC, ST} || P <- processes(), { _, L } <- [ process_info(P, message_queue_len) ], L >= 1000, [{_, RN}, {_, IC}, {_, ST}] <- [ process_info(P, [registered_name, initial_call, current_stacktrace]) ] ]).

Также хорошо зарекомендовал себя Observer. Для запуска Observer я использую следующий скрипт:

#!/bin/sh

erl -smp -name observer-`date +%s` -setcookie qwerty \
  -eval "net_kernel:connect('project@example.com'), observer:start()."

Вот как в Observer выглядит список процессов, отсортированный по длине очередей сообщений:

Список процессов в Observer

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

Неплохая идея — писать длины очередей сообщений и прочие метрики в какой-нибудь Graphite. Разумеется, если за метриками никто не следит 24/7, вряд ли они помогут заблаговременно предвидеть падение приложения и предотвратить его. Для решения этой проблемы вам нужен не Graphite, а некая система посылки SMS-уведомлений или вроде того. Метрики же интересны тем, что они позволяют (1) понять, что происходит с системой, если вы только что получили уведомление о ее неисправности, а также (2) разобраться в проблеме уже после ее возникновения.

Наконец, не представляет большого труда написать gen_server, который раз в несколько секунд проверяет длины очередей всех работающих процессов. Если процесс имеет слишком длинную очередь сообщений, делается запись в логе с указанием текущего стэктрейса процесса, самих сообщений и прочей информации. Мне кажется, такой gen_server должен быть в составе любого приложения на Erlang. Он избавляет от необходимости постоянно смотреть в etop или Observer, чтобы поймать и диагностировать проблему.

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

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

Можно попробовать ускорить обработку сообщений. Например, если вы видите, что процесс повисает на функции lists:member/2, возможно, вместо списков имеет смысл использовать множества. Если некий gen_server долгое время выполняет какие-то расчеты, ходите в этот gen_server только за данными, а сами расчеты выполняйте на стороне клиента.

В некоторых случаях множество сообщений можно объединить в одно большое сообщение. Например, вместо того, чтобы вызывать gen_event:notify/2 для списка событий, пошлите одно сообщение со списком всех этих событий. Вообще, имеет смысл всегда посылать списки событий, даже если этот список в большинстве случаев состоит из одного элемента. Это избавит вас от переписывания большого количества кода в будущем. Также в некоторых случаях вместо кучи событий «у пользователя X поменялось свойство Y» можно послать одно событие «у пользователя X поменялись какие-то свойства».

Часто помогает замена асинхронного вызова на синхронный. Например, cast можно заменить на call, а notify — на sync_notify. Перед посылкой сообщения можно проверять текущую длину очереди сообщений процесса, и если она больше заданного порога, делать синхронный вызов вместо асинхронного. Или делать timer:sleep(100), после чего повторять попытку асинхронного вызова. Писать код с проверками лучше сразу, а не после того, как возникнет проблема. В конце концов, программа, которая иногда немного тупит, лучше программы, аварийно завершающей свою работу в выходные, правда?

Нередко оказывается, что от посылки сообщений можно вообще отказаться. Например, вместо использования gen_server, хранящего в своем состоянии dict, лучше использовать ETS. В некоторых случаях состояние gen_server’а можно держать в protected ETS, делая чтение напрямую из ETS, а запись — через gen_server.

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

Однако следует понимать, что переполнение очередей сообщений означает, что у вас есть producer, который генерирует сообщения быстрее, чем их может обработать consumer. Обычно это означает ошибку by design. Либо consumer должен ходить за сообщениями к producer’у по мере своей готовности, либо producer должен проверять готовность consumer’а прежде, чем что-то ему посылать. Подумайте, не проще ли будет изменить дизайн вашего приложения, чем ставить подпорки.

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

Дополнение: Еще по теме борьбы с переполнением очередей — Пишем относительно простой менеджер локов на Erlang

Метки: , , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.