Statsd part 2

В прошлой статье мы разобрались, что такое метрики и для чего нужен statsd. А теперь давайте попробуем понять, как его готовить.

На отдельном хосте

Обычный паттерн использования statsd — сделать отдельный хост с демоном statsd. Этот демон получает индивидуальные метрики от хостов по UDP и после агрегации отправляет данные в TSDB.

Такой сетап достаточно прост и удобен. Кроме случаев, когда метрик много, когда нагрузка неравномерная или когда нужен HA(всегда).

Логичным выходом из такой ситуации было бы установить пару серверов с statsd за каким-то балансировщиком. Но так как мы делаем агрегации, подобный способ невозможен. Судите сами: если мы считаем уники или гистограммы, то подсчет будет проходить только по данным той ноды, на которой происходит агрегация.

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

на рисунке выше:
каждая нода отправляет одну метрику в балансер. Балансер отправляет эту метрику на любую из нод с механизмом консистент хеша, которая дальше уже по имени метрики выбирает соответствующий statsd и отправляет данные на него. Statsd принимает данные, агрегирует и складывает в TSDB. Из этой схемы можно убрать балансер, но я бы не рекомендовал лишать себя такого удобного примитива.

Такой механизм больше похож на что-то работающее. Но он не решает проблему неравномерной нагрузки.

Например, если все uniq метрики будут попадать только на один statsd, то он будет перегружен, в то время как остальные — недогружены.

Опять же, любой из сервисов генерируя нагрузку задевает другие сервисы, которые работают с этим же statsd.

И всё это находится где-то далеко, по сети (пакеты могут теряться и т.д.)

Каждому по statsd

Поэтому мне нравится схема, когда statsd установлен на каждой ноде или встроен непосредственно в сервис.

Такой подход даёт нам независимые ноды, это удобно и практично.

При выборе между отдельным демоном и встроенным непосредственно в приложение я бы руководствовался такой логикой:

Если приложение пишет много метрик в statsd (ну например, 500.000 в секунду), то лучше встраивать агрегацию в приложение, ведь каждая запись в сокет — это как минимум несколько переключений контекста. В остальных случаях выигрывает внешний statsd, так как им проще управлять и его можно обслуживать средствами ops команды. Кроме этого, некоторые демоны statsd поддерживают очень интересные алгоритмы, которые будет необходимо самостоятельно имплементировать внутри приложения.

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

Другими словами, если на сервер А пришло 2 пользователя, которым сервер ответил за 3 секунды, а на сервер B пришло 2.000 пользователей, которым сервер ответил за 0.001 секунды, мы всё равно получим 3 секунды.

Это сложно и часто сбивает с толку.

Гибридные решения

Отличным вариантом будет обрабатывать метрики приложения локально и дублировать их для роли целиком.

То есть для приложения локально отправлять метрики в statsd, который параллельно с агрегацией будет вырезать\добавлять часть имени метрики, которая включает в себя имя ноды, и пересылать на глобальные statsd.

Таким образом, в TSDB мы получим такие метрики

metrics.role.<%nodename&>.99_percent # от локальноного statsd
metrics.role.all.99_percent          # от глобального statsd

Если честно, то я не знаю ни одного решения, которое так умеет.

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

Итог.

Как видите, нет ничего сложного в архитектуре statsd приложений. В следующих постах мы уже поговорим об имплементациях.