Уже прошло больше года с тех пор, как я подключил кусок JavaScript к collectd плагину и начал собирать данные мониторинга с нашей CI, чтобы потом хранить их в Graphite. И знаете что? Оно до сих пор работает. Даже JavaScript’овая часть. Но настали времена, когда мне нужно будет собирать метрики с .NET Core сервисов, и, чувствую, связкой JavaScript + collectd я уже не отделаюсь.
По идее, проблем быть не должно. В Graphite ведь можно отправлять данные хоть по TCP в виде текста, так что сервисы вполне могут заняться этим самостоятельно. Но можно сделать даже ещё проще.
На сцену выходит statsd
Если пристально посмотреть на свежайший Docker образ для Graphite, то в нём можно заметить тулзу под названием StatsD. Это такой маленький буфер-демон между графитом и приложением, который принимает метрики последнего по UDP, агрегирует их немного, и раз в сколько-то секунд складывает результаты в Graphite. Так как речь идёт о UDP, то метрики можно сбрасывать ну очень быстро и совсем не дожидаться ответа получателя. Так как StatsD работает ещё и агрегатором, то метрики можно сбрасывать в огромных количествах, не боясь, что Graphite сляжет.
Ну и наконец, StatsD невероятно простой, может работать с разными хранилищами, и таким образом просто напрашивается, чтобы его использовали как универсальный шлюз для метрик. А где они там будут храниться, приложение интересовать не должно.
StatsD в связке с .NET Core
Во взаимоотношениях StatsD и .NET Core нет ничего особенного, но так как в последнее время мне приходится иметь дело с Core, то эта пара меня интересует больше всего.
На NuGet можно найти кучу дотнэтовских пакетов для StatsD, но они то безумно старые, то поддерживают только .NET Framework. Наконец, я нашёл один, который вроде и обновляется иногда, и работает с нужными .NET Core — StatsdClient (это не реклама, но если у автора есть ненужное пиво — приму в дар).
Самый лучший способ посмотреть, как что-то работает — сделать из этого «что-то» — «нечто». Так что приступим.
Большой пример
.NET Core программулина
У меня есть простенькая .NET Core программка, которая запускает два потока. Первый — для бессмысленной работы (генерация массивов строк), а второй — для её мониторинга: как часто мы вызываем сборщик мусора, сколько всего раз это произошло, сколько всего памяти используем и как долго делалась единица работы. Пока Graphite существует только в теории, все собранные метрики мы будем отправлять в консоль.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
static void Main(string[] args) { Parallel.Invoke( () => { // Worker thread while (true) { var sw = new Stopwatch(); sw.Start(); DoPointlessJob(); sw.Stop(); Console.WriteLine($"Done in {sw.ElapsedMilliseconds}ms"); Thread.Sleep(random.Next(500)); } }, () => { // Monitoring thread int lastGcCount = 0; while (true) { Console.WriteLine($"TotalMemory: {GC.GetTotalMemory(false)}"); var gcCount = GC.CollectionCount(0); Console.WriteLine($"GC Count (gen0): {gcCount} (+{gcCount - lastGcCount})"); lastGcCount = gcCount; Thread.Sleep(100); } } ); } |
Эти данные, конечно, стоило бы собирать откуда-нибудь снаружи, хотя бы через тот же Event Tracing. Но что уж теперь поделаешь. Может, в следующий раз.
Итак, запускаем программку, смотрим, что она выводит:
1 2 3 4 5 6 7 8 9 10 11 12 |
# # TotalMemory: 137643528 # GC Count (gen0): 641 (+17) # TotalMemory: 141173416 # GC Count (gen0): 658 (+17) # Done in 1686ms # TotalMemory: 142680136 # GC Count (gen0): 663 (+5) # TotalMemory: 145614400 # GC Count (gen0): 674 (+11) # TotalMemory: 151649280 # |
Программка выводит много красивых и наверняка полезных чисел. Но без графика я их не воспринимаю, так что установим-ка мы Graphite, который сможет их построить.
Устанавливаем Graphite
С тех времён, как я последний раз устанавливал Graphite, многое изменилось. Теперь вместо установок и настроек индивидуальных сервисов можно взять официальный Docker образ, и расправиться с задачей в одну команду:
1 2 3 4 5 6 7 8 9 |
docker run -d\ --name graphite\ --restart=always\ -p 80:80\ -p 2003-2004:2003-2004\ -p 2023-2024:2023-2024\ -p 8125:8125/udp\ -p 8126:8126\ graphiteapp/graphite-statsd |
Для наших целей мне не нужен контейнер с именем и кучей открытых портов. Достаточно лишь 80-го порта (Graphite UI) и 8125-го для UDP (StatsD), так что эту команду можно порядочно упростить:
1 2 3 4 |
docker run -d \ -p 80:80 \ -p 8125:8125/udp \ graphiteapp/graphite-statsd |
Теперь откываем localhost, и вот он, Graphite, во всём своём пустынном великолепии:
Теперь нужно забить его данными для графика.
Собираем метрики через StatsdClient
Добавив StatsdClient пакет в проект я получил в награду класс Metrics
, которым просто позаменял Console.WriteLine
‘ы и StopWatch
. В результате код даже немного похорошел:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
static void Main(string[] args) { Metrics.Configure(new MetricsConfig { StatsdServerName = "127.0.0.1", Prefix = "myApp" }); Parallel.Invoke( () => { // Thread 1 while (true) { Metrics.Time(() => DoPointlessJob(), "pointlessJob"); Thread.Sleep(random.Next(500)); } }, () => { // Thread 2 int lastGcCount = 0; while (true) { Metrics.GaugeAbsoluteValue("totalMemory", GC.GetTotalMemory(false)); var gcCount = GC.CollectionCount(0); Metrics.Counter("gcCount.gen0", gcCount - lastGcCount); lastGcCount = gcCount; Thread.Sleep(100); } } ); } |
Изменения, думаю, вполне понятны. Во-первых, мы показали StatsdClient , где Graphite, собственно, находится. Во-вторых, так как имена для графитовых метрик обычно похожи на: myserver.myapp.mysensor.mySensorComponent
, что потом очень удобно превращать в дерево папок в Graphite UI. то свойством Prefix
можно задать первую часть имени, а при сборе конкретных метрик — вторую.
Ну и наконец, методы Time
, GaugeAbsoluteValue
и Counter
, собственно, и отправляют метрики в StatsD. Вот на них стоит посмотреть поближе.
GaugeAbsoluteValue
В этот метод скармливают метрики, которые, в принципе, уже полезны как есть. Вроде температуры, места на диске, количества памяти, и т. п. Какое значение считали с сенсора, то потом на графике и нарисовали. В строке 21 мы собираем метрику totalMemory
, и вот она как раз под эту категорию и попадает:
Counter (он же счётчик)
Счётчики немного прикольнее, потому что они не показывают значение метрики как есть, а показывают скорость, с которой оно меняется. Как производная в матанализе. Когда речь идёт о количестве открытых соединений, или сборок мусора, или ещё чего-то, меняющегося со временем, мне интереснее не сколько всего раз сборщик мусора сработал за день, а в какие моменты его было больше, а в какие — меньше.
В строке 23 мы как раз и замеряем, сколько раз сборщик мусора прошёлся по нулевому поколению (gen0) за последние 100 миллисекунд. Console.WriteLine выводил числа в пределах 5-17, так что я думаю, что за секунду происходило в среднем где-то 100-120 сборок. Смотрим на график и… ну почти угадал.
Time
Название «Время» для этого типа метрик несколько сбивает с толку, потому что там может быть не только время. Например, количество байт, переданных в пакете, или количество долларов в корзине пользователя тоже бы вполне подошли. Все метрики этого типа StatsD терпеливо собирает в течение всего периода буферизации (10 секунд по умолчанию), а затем отправляет в Graphite статистику:
- сколько всего метрик собрано за эти 10 секунд,
- какое было минимальное значение,
- какое максимальное,
- какое средние,
- и т. п.
В нашем случае мы замеряли продолжительность функции DoPointlessJob, и в Graphite теперь можно посмотреть статистику всего этого безобразия. Ну или хотя бы максимальное, минимальное и средние значения:
Красота.
И ещё один тип
Если ещё один StatsD тип метрик — множество (Set
). StatsD собирает значения этого типа в течение 10 секунд (или какой там период буферизации мы выбрали), и затем отправляет в Graphite количество уникальных значений, пришедших ему на вход. Например, можно отправлять ему айпишки пользователей, открывающих страницы сайта, а потом узнать, сколько же всего уникальных пользователей сейчас на сайте.
Очень короткое заключение
Самое большое достоинство StatsD в том, что он ну просто невероятно простой. И полезный. Всё-таки агрегация данных, абстрагированность от бакэнда, UDP — всё это делает сборку метрик и приложение в целом чуточку быстрей и понятней. Конечно, можно было бы взять TcpClient и какой-нибудь NetworkStream и писать метрики в Graphite напрямую, но.. зачем?