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 приложений. В следующих постах мы уже поговорим об имплементациях.