metronome


Аннотация

В статье рассматривается легковесная библиотека для работы с graphite-метриками в Erlang. Сравнивается с уже существующими библиотеками, такими как synrc/mtx, boundary/folsom, campanja/folsomite и feuerlabs/exometer. Описываются варианты использования библиотеки в составе полноценного Erlang-проекта.


Введение

В жизненном цикле большинства проектов наступает момент, когда нужно писать не только log-файлы, но и производить инструментарий кода и бизнес-логики, собирая различные метрики в runtime и представлять их графически. Преимущества такого подхода очевидны — человеческий глаз воспринимает графическую информацию намного проще чем текст. Графики вариативны (линейные, столбчатые, круговые, etc.), позволяют легко отслеживать тренд, одним графиком можно покрыть большой временной отрезок, двигаясь во времени как вперед так и назад, чего, конечно же, не могут позоволить log-файлы.

Cобирать метрики и строить графики — только полдела, главное — делать это правильно. Важно понимать природу метрик, применять правильные функции-агрегаты и уже на основании этого уметь их корректно отображать. Соответственно Coda Hale существует 5 типов метрик: counters, meters, gauges, histograms и timers. В этой статье я предлагаю сосредоточится на первых трех.

  • counters — обычные счетчики, представляющие простой инкремент и декремент над скалярной величиной. Если абстрагироваться, то можно представить это в виде говорящей кофемашины, у которой отшибло память, и все, что она помнит — это, например, последние 5 секунд и количество кружек ароматного кофе, которые она наполнила за это время. В результате все, что будет производить говорящая кофемашина (конечно же кроме кофе), это фразы вроде: “налито 0 кружек кофе, налита 1 кружка кофе, налито 0 кружек кофе, налита 1 кружка кофе!” и т.д. Если вдуматься в природу данного типа метрик, то сразу станет понятно, чтобы агрегировать их на временном интервале, например 60 секунд, лучше всего подойдет функция суммы.
  • meters — тот же самый счетчик, что и counter, но с постоянно растущим значением, c инкрементом над скалярной величиной. В интуитивном представлении данный тип метрик можно представить как счетчик водопотребления, значение которого измеряется в кубических метрах и постоянно растет с потреблением воды. Или это все та же говорящая кофемашина с хорошей памятью, которую таки удалось починить, и теперь, кроме изготовления кофе, она еще в состоянии помнить количество кружек, наполненныx с того момента, как ее включили в электрическую сеть. В результате кофемашина будет выдавать фразы: “налито 0 кружек кофе, налито 0 кружек кофе, налита 1 кружка кофе, налито 2 кружки кофе, …, налито 100 кружек кофе!”. После выключения и включения кофемашины: “налито 0 кружек кофе, налита 1 кружка кофе, налито 2 кружки кофе, …, налито 15 кружек кофе, …”. Как правило, все зависит от того, как используется данная величина, например, чтобы узнать сколько кубических метров воды потребляется за сутки, достаточно использовать производную.
  • gauges — мгновенное измерение, привязанное к определенной точке во времени. В интуитивном представлении это спидометр в машине, показывающий скорость движения автомобиля. Если записывать показания спидометра каждые 5 секунд, например, тогда в момент времени T1 скорость машины составит 30 км/ч, в момент времени T2 — 50 км/ч, и, вполне может быть, что через 2 секунды скорость автомобиля увеличится до 70 км/ч, однако через 3 секунды к моменту времени T3, скорость машины опять будет равна 50 км/ч — именно эта скорость и будет зафиксирована. Подобрать правильную функцию-агрегат для данного типа метрик не составляет большого труда, достаточно вычислить среднее значение или медиану на временном интервале.

Мотивация

В опеределенный момент времени (полтора года назад), возникла необходимость производить инструментарий кода и бизнес-логики, а также собирать различные метрики, описывающие состояние виртуальной машины Erlang, такие как: потребление памяти, очередь процессов, сборки мусора, переключение контекста, etc. В качестве быстрого решения была написана библиотека, представляющая собой сборщик predefined-метрик на основе произвольных пользовательских функций, и клиент для отправки метрик в систему ответственную за хранение, обработку и отображение полученных метрик в виде графиков (далее целевую систему). Решение было временное, и, предлолагалось, что в дальнейшем будет заменено на boundary/folsom вкупе с надежным клиентом, доставляющим метрики в целевую систему по TCP-протоколу. Однако со временем библиотека была интегрирована в несколько внутренних проектов на Erlang, и успешно справлялась со своими задачами. Не так давно к функциональности библиотеки были предъявлены новые бизнес-требования: библиотека должна уметь принимать пользовательские метрики из runtime, в зависимости от типа метрики правильно аккумулировать (накапливать) в течении определенного времени ее значение, а после передавать в целевую систему. Из бизнес-требований вытекают технические, одно из них — надежный клиент, доставляющий метрики по TCP-протоколу в целевую систему. Если графики, построенные на основе отправленных метрик, показывают только тренд, то надежностью доставки в какой-то степени можно пренебречь. С другой стороны, если необходимо, чтобы тренд был максимально приближен к реальности и погрешность была минимальной, надежностью доставки пренебрегать не стоит. Следующее техническое требование вытекает из бизнес-требования аккумулирования метрик — агрегация аккумулированных метрик на определенном временном интервале. Данное требование становится весьма актуальным, когда количество серверов переваливает за 100+ и простой graphite-сервер перестает справляться с такими нагрузками. И последнее — метрики после их передачи в целевую систему должны автоматически инвалидироваться только в том случае, если данные были доставлены без каких-либо явных ошибок.

Теперь стоит разобраться, каким образом реализована работа с сетью в Erlang и почему это важно. Ни graphite- ни statsd-протокол не подразумевают уведомления о доставке данных, работая как текстовый протокол поверх TCP/IP и всецело на него полагаясь. В Erlang работа с сетью реализована через сетевой port driver (далее сетевой драйвер). Сетевой драйвер устанавливает соединение с удаленной стороной, работая непосредственно с сокетом и имеет собственные очереди приема и отправки. Работа с драйвером происходит через модуль prim_inet, с помощью функций send для TCP-протокола и sendto для UDP-протокола. После отправки данных в сетевой драйвер функция блокируется и ждет сообщения {inet_reply, S, Status}. Если при установке соединения с удаленной стороной таймуат отправки данных не был указан, может возникнуть ситуация, когда функция заблокируется, ожидая сообщение, которое никогда не придет. Подобное может случиться, например, если удаленная сторона перестала вычитывать данные из сетевых буферов. Сначала на удаленной стороне переполняется буфер приема (receive buffer). Затем TCP-протокол на своем уровне регулирует отправку данных “закрывая окно”, устанавливая значение window в 0, после этого на отправляющей стороне начинает копиться буфер отправки (send buffer). При заполнении буфера отправки, сетевой драйвер перестает отвечать сообщением {inet_reply, S, Status} после того, как ему были переданы данные на отправку. Как результат — потеря данных, т.к. часть сообщений, которая была отправлена в драйвер, “осела” в его сетевых буферах и вряд ли будет уже отправлена.

Единственный выход — более вдумчивая настройка сетевого соединения. В первую очередь необходимо установить таймаут на отправку данных в сокет с помощью параметра {send_timeout, Timeout In Ms}. Таким образом, если prim_inet:send по каким-либо причинам заблокируется, в большинстве случаев по истечению указанного таймаута функция вернет значение {error, timeout}. Следующие два параметра, которые нужно включить — {delay_send, false} и {nodelay, true}. Первый параметр предписывает драйверу не накапливать данные в своих очередях, а отправлять их как можно быстрее в сокет. Второй параметр, это непосредственно параметр сокета, включающий опцию TCP_NODELAY, предписывающую немедленно передавать полученные данные (даже малого объема) удаленной стороне без буферизации. Подобная настройка сетевого соединения позволит значительно сократить вероятность потери данных при доставке в целевую систему.


Сравнение

Ниже приведен небольшой обзор сильных и слабых сторон популярных библиотек для работы с метриками в Erlang.

synrc/mtx:

В качестве транспорта доставки метрик используется UDP-протокол, что уже не гарантирует надежность доставки; на уровне библиотеки надежность доставки также не гарантируется, т.к. при любых проблемах с отправкой данных, процесс “крэшется” и перезапускается супервизором (используется стратегия let it crash) — данные молча теряются в обоих случаях. При передаче метрики в mtx используется асинхронный вызов gen_server:cast, который приводит к немедленной отправке данных в сетевой драйвер. В библиотеке не предусмотрена возможность аккумулирования данных. Отправка большого количества метрик, например 1K+ в секунду, может привести к тому, что код библиотеки заблокируется (как следствие рост очереди сообщений) на вызове gen_udp:send, который в свою очередь будет ждать ответа от prim_inet:sendto, производящего selective recevie, в ожидании сообщения от сетевого драйвера. Если же очередь сообщений процесса mtx будет достаточно большой, это может сильно сказаться не только на производительности библиотеки, но и всего приложения.

pros:

  • Поддержка всех типов метрик (counters, meters, gauges, histograms, timers);
  • Поддержка statsd-протокола.

cons:

  • В качестве транспортного протокола используется UDP;
  • Нет гарантии корректной отправки данных на уровне библиотеки;
  • Нет поддержки аккумулирования метрик и отправки через заданный интнервал времени;
  • Нет поддержки predefined-метрик на основе произвольных пользовательских функций;
  • Нет поддержки сбора статистики работы виртуальной машины Erlang.

boundary/folsom:

Библиотека имеет полную поддержку всех типов метрик (counters, meters, gauges, histograms, timers) и позволяет аккумулировать их во времени. Однако, какой либо транспорт у библиотеки отсутствует и предполагается, что выборка и передача метрик в целевую систему будет реализована сторонними библиотеками, либо силами разработчика. В следствие выбранной модели работы с метриками, библиотека не обладает возможностью автоматической инвалидации метрик.

pros:

  • Поддержка всех типов метрик (counters, meters, gauges, histograms, timers);
  • Поддержка аккумулирования метрик;
  • Поддержки сбора статистики работы виртуальной машины Erlang.

cons:

  • Нет встроенного graphite/statsd клиента;
  • Нет поддержки автоматической инвалидации метрик;
  • Нет поддержки predefined-метрик на основе произвольных пользовательских функций.

campanja/folsomite:

Представляет собой обертку над библиотекой boundary/folsom. В качестве транспорта доставки метрик используется TCP-протокол, поверх которого реализован текстовый graphite-протокол. Для отправки накопленных метрик используется асинхронный вызов gen_server:cast. В библиотеке также может возникнуть ситуация, когда код библиотеки заблокируется на вызове gen_tcp:send, который в свою очередь будет ждать ответа от prim_inet:sendto, производящего selective recevie в ожидании сообщения от драйвера, т.к. при установке соединения библиотека не предусматривает таймаут отправки данных. При ошибке отправки данных библиотека завершает процесс, в контексте которого установлено соединение с целевой системой. Однако, в связи с архитектурой библиотеки boundary/folsom, данные, которые были переданы в сетевой драйвер не потеряются, и будут отправлены на следующей итерации через заданный интервал времени. В библиотеке не предусмотрена функциональность predefined-метрик на основе произвольных пользовательских функций.

pros:

  • Поддержка всех типов метрик (counters, meters, gauges, histograms, timers);
  • Поддержка аккумулирования метрик и отправка через заданный интервал времени;
  • Поддержка сбора статистики работы виртуальной машины Erlang;
  • В качестве транспортного протокола используется TCP;
  • Поддержка graphite-протокола.

cons:

  • Нет гарантии корректной отправки данных на уровне библиотеки;
  • Нет поддержки автоматической инвалидации метрик;
  • Нет поддержки predefined-метрик на основе произвольных пользовательских функций.

feuerlabs/exometer:

Библиотека имеет большой набор различных функций, поддерживает все типы метрик (counters, meters, gauges, histograms, timers), предоставляет интерфейс работы с метриками из библиотеки boundary/folsom. В качестве транспорта для доставки метрик используется TCP-протокол, поверх которого реализована поддержка текстового graphite- и statsd-протокола. Как и все рассмотренные библиотеки, данная библиотека не предусматривает таймаут отправки данных, в результате чего может возникнуть ситуация, когда код библиотеки заблокируется на вызове gen_tcp:send. При ошибке отправки данных библиотека предпирнимает попытку переустановить соединение, если новое соединение устанавливается — библиотека ведет себя так, как будто данные были успешно отправлены, что весьма странно. В библиотеке не предусмотрена автоматическая инвалидация отправленных метрик.

pros:

  • Поддержка всех типов метрик (counters, meters, gauges, histograms, timers);
  • Поддержка аккумулирования метрик и отправка через заданный интервал времени;
  • Поддержка сбора статистики работы виртуальной машины Erlang;
  • В качестве транспортного протокола используется TCP;
  • Поддержка graphite и statsd-протокола;
  • Поддержки predefined-метрик на основе произвольных пользовательских функций.

cons:

  • Нет гарантии корректной отправки данных на уровне библиотеки;
  • Нет поддержки автоматической инвалидации метрик.

Так как рассмотренные библиотеки не удовлетворяли всем предъявленным требованиям, было принято решение дописать требуемую функциональность в библиотеку и в дальнейшем ее сопровождать.

juise/metronome:

Библиотека имеет поддержку метрик типа: counters, meters, gauges. В качестве транспорта для доставки метрик используется TCP-протокол, поверх которого реализована поддержка текстового graphite-протокола. Соединение с целевой системой устанавливается с использованием рекомендаций приведенных в разделе “Мотивация”. После успешной отправки данных библиотека автоматически инвалидирует отправленные метрики, метрики, которые не удалось отправить, будут отправлены на следующей итерации через заданный интервал времени. Для сбора статистики работы виртуальной машины Erlang используется механизм predefined-метрик на основе произвольных пользовательских фукнций.

pros:

  • Поддержка аккумулирования метрик и отправка через заданный интервал времени;
  • Поддержка сбора статистики работы виртуальной машины Erlang;
  • В качестве транспортного протокола используется TCP;
  • Поддержка graphite-протокола;
  • Поддержки predefined-метрик на основе произвольных пользовательских функций;
  • Гарантии корректной отправки данных на уровне библиотеки;
  • Поддержка автоматической инвалидации метрик.

cons:

  • feedback are welcome!

Архитектура и варианты использования

Ядро библиотеки состоит из трех модулей: metronome, metronome_core и metronome_graphite. Модуль metronome предоставляет интерфейс для работы с метриками: добавление, обновление, просмотр и удаление. Так же модуль предоставляет набор функций для сбора статистики работы виртуальной машины Erlang. При добавлении метрики вместе с именем и значением записывается время, соответствующее началу текущего интервала. Если метрика с именем <<”foo.bar.baz”>> обновляется несколько раз в течении одного интервала, то значение метрики просто обновится, в противном случае добавится новая метрика с временем, соответствующим началу нового интервала (см. пример ниже). После того как метрика была добавлена, она попадает во внутреннюю ets-таблицу, которая работает в контексте модуля metronome_core. Модуль metronome_core занимается подготовкой метрик: раз в заданный интервал времени, модуль автоматически собирает predefined-метрики, производит пользовательские inline подстановки над накопленными метриками и передает в модуль metronome_graphite для отправки в целевую систему. Модуль metronome_graphite представляет собой транспорт для отправки метрик в целевую систему, используя TCP-протокол. В случае успешной отправки данных модуль metronome_core инвалидирует отправленные метрики. Если же в процессе отправки данных возникла ошибка, модуль metronome_graphite переустанавливает соединение с целевой системой, метрики, которые не удалось отправить, будут отправлены в следующую итерацию через заданный интервал времени.

Для создания или обновления метрики достаточно вызвать функцию update/3 модуля metronome, указав в параметрах имя метрики (Name), значение (Value) и тип (Type: counter, meter или gauge), или использовать функции-алиасы: update_counter/2, update_meter/2 или update_gauge/2 с тем же набором параметров за исключением типа:

1> metronome:update(<<”foo.bar.baz”>>, 1, counter).
true
2> metronome.update_counter(<<”foo.bar.baz”>>, 1).
true

через 120 секунд

3> metronome:update_counter(<<”foo.bar.baz”>>, 1).
true

Для просмотра значений метрики <<”foo.bar.baz”>> достаточно вызвать функцию get/2 модуля metronome, указав в параметрах имя метрики (Name) и ее тип (Type: counter, meter или gauge), или использовать функции-алиасы: get_counter/1, get_meter/1 или get_gauge/1 с тем же набором параметров за исключением типа:

1> metronome:get(<<”foo.bar.baz”>>, counter).
[#metric{name = {<<”foo.bar.baz”>>, 1443528270}, value = 2, type = counter},
#metric{name = {<<”foo.bar.baz”>>, 1443528390}, value = 1, type = counter}]
2> metronome:get_counter(<<”foo.bar.baz”>>).
[#metric{name = {<<”foo.bar.baz”>>, 1443528270}, value = 2, type = counter},
#metric{name = {<<”foo.bar.baz”>>, 1443528390}, value = 1, type = counter}]

Для просмотра накопленных метрик определенного типа достаточно вызвать функцию get/1 модуля metronome, указав в параметрах тип метрики (Type: counter, meter или gauge), или использовать функции-алиаcы: get_counter/0, get_meter/0 или get_gauge/0:

1> metronome:get(counter).
[#metric{name = {<<”foo.bar.baz”>>, 1443528270}, value = 2, type = counter},
#metric{name = {<<”foo.bar.baz”>>, 1443528390}, value = 1, type = counter},
#metric{name = {“foo.bar.baz”, 1443528780}, value = 3, type = counter},
#metric{name = {<<”foo.bar”>>, 1443528780}, value = 9, type = counter}]

Для удаления метрики <<”foo.bar.baz”>> достаточно вызвать функцию delete/2 модуля metronome, указав в параметрах имя метрики (Name) и ее тип (Type: counter, meter или gauge), или использовать функции-алиасы: delete_counter/1, delete_meter/1 или delete_gauge/1. При удалении функция возвращает количество удаленных метрик. Для удаления всех метрик заданного типа необходимо вызвать функцию delete/1 модуля metronome, указав в параметрах тип метрики (Type: counter, meter или gauge):

1> metronome:delete(<<”foo.bar.baz”>>, counter).
2
2> metronome:delete(counter).
2
3> metronome:delete(meter).
0

Базовая конфигурация metronome может быть следующая, sys.config:

{metronome, [
{period, 10000},
 {inline, [{“%local%”, “local”},
{“%global%”, “global”}]},
 {graphite_host, “127.0.0.1”},
{graphite_port, 2003},
 {predefined, [
{“%local%.%node%.erlang.memory.ets.gauge”,
{erlang, memory, [], ets}, gauge},

{“%local%.%node%.erlang.processes.gauge”,
{erlang, system_info, [process_count]}, gauge},

{“%global%.%node%.erlang.gc.meter”,
{metronome, system_status, [garbadge_collection], gc_count}, meter},

{“%local%.%ode%.erlang.lhttpc.conn.gauge”,
{myapp, lhttpc_connections_cnt, []}, gauge},

{“%local%.%node%.erlang.cowboy.conn.gauge”,
{fun() -> ranch_server:count_connections(http) end}, gauge}
]}
]}

В данной конфигурации параметр “period” определяет временной интервал (в миллисекундах), через который metronome будет передавать накопленные метрики в целевую систему. Параметры “graphite_host” и “graphite_port” определяют хост и порт целевой системы соответственно. Параметр “inline” позволяет делать произвольные пользовательские подстановки в именах метрик. Для подстановки имени хоста необходимо использовать библиотечную подстановку %node%. Параметр “predefined” позволяет определять метрики, значения которых будут автоматически собираться произвольными пользовательскими функциями с интервалом, определенным через параметр “period”. Формат, описывающий метрику в “predefined” записи, следующий:

{Name, {F}, Type}
{Name, {F, E}, Type}

либо

{Name, {M, F, A}, Type}
{Name, {M, F, A, E}, Type}

где Name — имя метрики, M — модуль, F — функция, A — аргументы, E — ключ proplist’а, значение которого нужно использовать, если F возвращает proplist.

Например, функция lhttpc_connections_cnt в модуле myapp имеет следующий вид:

lhttpc_connections_cnt() ->
Childs = supervisor:which_children(whereis(lhttpc_sup)),
Pids = [Pid || {_, Pid, _, _} <- Childs],
lists:foldl(fun(Pid, Conns) ->
try element(7, sys:get_state(Pid)) of
TableId ->
ets:info(TableId, size) + Conns
catch
_C:_R ->
Conns
end
end, 0, Pids).

Like what you read? Give Juise a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.