Что в метриках тебе моих?
О чём могут поведать цифры и графики в мониторинге и зачем вообще заморачиваться.
Андрей Новиков
RubyRussia 2018
🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
Чего мы хотим?
- Знать, как быстро растут и разгребаются очереди
- Знать, какие задачи наиболее «проблемные»
- Понимать, когда это началось
и кто виноват - Понимать, помогли ли принятые нами меры
Чего мы хотим?
- Больше метрик
- Больше графиков
- Ещё больше графиков
- … и ещё два монитора
Недостающие метрики
- Время обработки задач
- Количество новых задач
Недостающие графики
- Динамика изменения размера очередей по отдельным очередям
- Динамика изменения времени выполнения задач по классам
Мониторинг веб-приложения
- Сервера: загрузка CPU, RAM, IO, LA, заполненность дисков, …
- Веб: количество запросов, время их обработки, queueing time, …
- Отложенные задачи: количество задач, время их обработки, количество ошибок, …
- Базы данных: количество запросов, время обработки, объём данных, …
- Внешние API: количество запросов, время обработки, ошибки…
- Сеть: задержки, DNS lookup time, throughput, потери пакетов, …
- Свои показатели: свой набор под каждую интеграцию
- Бизнес-метрики: количество пользователей, заказов, 🤑, …
А зачем?
Чтобы понять, откуда вдруг запахло жареным:
-
Откуда — какой участок приложения горит.
-
Вдруг — когда загорелось. 5 минут назад, после деплоя или медленно, но верно разгоралось по мере роста сервиса?
-
Запах — в чём именно проблема? ЦП, память, внешние сервисы?
Теория
🤓
Мониторинг
Непрерывный процесс наблюдения и регистрации параметров системы в сравнении с заданными критериями
«Вы не можете контролировать то, что не можете измерить»
— Том ДеМарко
Метрики
- Параметры системы
- Численные значения, полученные при измерении этих параметров работающей системы
Временные ряды (time series)
Значения какой либо величины в порядке времени измерения
Для хранения разработаны специальные базы данных:
InfluxDB, Graphite, …
Ярлыки (labels)
Позволяют сегментировать значения и агрегировать их вновь
Для каждого уникального набора пишется свой временной ряд
Есть опасность комбинаторного взрыва (не стоит записывать в них id пользователей)
Счётчик
CRDT: G-Counter — монотонно возрастающий счётчик
Базовый тип для более сложных метрик: histogram, …
Позволяет уверенно детектировать рестарты и суммировать значения хоть с тысячи воркеров.
Gauge
Произвольное значение, без ограничений.
Используется для мониторинга состояния «внешнего мира»: Загрузка CPU, потребление памяти, количество процессов.
Histogram и Summary
Сложные типы для сбора статистики по времени выполнения или потребления ресурсов: перцентили, среднее, медиана, …
Histogram: набор счётчиков по «корзинам».
Summary: Предпросчёт требуемой статистики клиентом на ходу.
Prometheus | Librato | NewRelic | |
---|---|---|---|
Типы метрик: (🔧 — настраивается) |
histogram 🔧 summary 🔧 |
summary 🔧(перцентили) | summary (min, max, сумма квадратов, …) |
DYI: Prometheus
Регистрация метрик
prometheus = Prometheus::Client.registry
prometheus.counter :sidekiq_jobs_executed_total, "A counter of the total number of jobs sidekiq executed."
prometheus.counter :sidekiq_jobs_success_total, "A counter of the total number of jobs successfully processed by sidekiq."
prometheus.counter :sidekiq_jobs_failed_total, "A counter of the total number of jobs failed in sidekiq."
prometheus.histogram :sidekiq_job_runtime_seconds, "A histogram of the job execution time."
prometheus.gauge :sidekiq_jobs_waiting_count, "The number of jobs waiting to process in sidekiq."
prometheus.gauge :sidekiq_active_workers_count, "The number of currently running machines with sidekiq workers."
prometheus.gauge :sidekiq_jobs_scheduled_count, "The number of jobs scheduled for later execution."
prometheus.gauge :sidekiq_jobs_retry_count, "The number of failed jobs waiting to be retried"
prometheus.gauge :sidekiq_jobs_dead_count, "The number of jobs exceeded their retry count."
prometheus.gauge :sidekiq_active_processes, "The number of active Sidekiq worker processes."
DYI: Prometheus
Обновление метрик
class SidekiqWorkerPrometheusMiddleware
def call(worker, job, queue)
prometheus = Prometheus::Client.registry
labels = { queue: queue, worker: worker.class }
start = Time.now
begin
yield
prometheus.get(:sidekiq_jobs_success_total).increment(labels)
rescue Exception # rubocop: disable Lint/RescueException
prometheus.get(:sidekiq_jobs_failed_total).increment(labels)
raise
ensure
prometheus.get(:sidekiq_job_runtime_seconds).observe(labels, (Time.now - start).round(3))
prometheus.get(:sidekiq_jobs_executed_total).increment(labels)
end
end
end
DYI: Prometheus
Обновление метрик
class SidekiqExporter < ::Prometheus::Middleware::Exporter
def call(env)
refresh_sidekiq_stats if env["REQUEST_PATH"].start_with?(path)
super
end
def refresh_sidekiq_stats
r = Prometheus.registry
stats = ::Sidekiq::Stats.new
stats.queues.each do |k, v|
r.get(:sidekiq_jobs_waiting_count).set({ queue: k }, v)
end
r.get(:sidekiq_active_workers_count).set({}, stats.workers_size)
r.get(:sidekiq_jobs_scheduled_count).set({}, stats.scheduled_size)
r.get(:sidekiq_jobs_dead_count).set({}, stats.dead_size)
r.get(:sidekiq_active_processes).set({}, stats.processes_size)
end
end
DYI: Prometheus
Вывод метрик
Sidekiq.configure_server do |config|
config.redis = redis_credentials
config.server_middleware do |chain|
chain.add SidekiqWorkerPrometheusMiddleware
end
# Start server to expose metrics for Prometheus collector
Thread.new do
Rack::Handler::WEBrick.run(
Rack::Builder.new do
use Rack::CommonLogger, ::Sidekiq.logger
use SidekiqExporter, registry: Prometheus.registry
run ->(_env) { [404, {"Content-Type" => "text/plain"}, ["Not Found\n"]] }
end,
Host: ENV["PROMETHEUS_EXPORTER_BIND"] || "0.0.0.0",
Port: ENV.fetch("PROMETHEUS_EXPORTER_PORT", 9310),
AccessLog: []
)
end
end
DYI: Prometheus
Успех!
curl http://localhost:9091/metrics
# TYPE sidekiq_jobs_executed_total counter
# HELP sidekiq_jobs_executed_total A counter of the total number of jobs sidekiq executed.
sidekiq_jobs_executed_total{queue="utils",worker="EmptyJob"} 16
sidekiq_jobs_executed_total{queue="default",worker="WaitingJob"} 160
# TYPE sidekiq_jobs_waiting_count gauge
# HELP sidekiq_jobs_waiting_count The number of jobs waiting to process in sidekiq.
sidekiq_jobs_waiting_count{queue="utils"} 2190
sidekiq_jobs_waiting_count{queue="default"} 1490
# TYPE sidekiq_job_runtime_seconds histogram
# HELP sidekiq_job_runtime_seconds A histogram of the job execution time.
sidekiq_job_runtime_seconds_bucket{queue="utils",worker="EmptyJob",le="0.5"} 9.0
sidekiq_job_runtime_seconds_bucket{queue="utils",worker="EmptyJob",le="1"} 16.0
sidekiq_job_runtime_seconds_bucket{queue="utils",worker="EmptyJob",le="5"} 16.0
sidekiq_job_runtime_seconds_bucket{queue="utils",worker="EmptyJob",le="10"} 16.0
sidekiq_job_runtime_seconds_bucket{queue="utils",worker="EmptyJob",le="60"} 16.0
sidekiq_job_runtime_seconds_bucket{queue="utils",worker="EmptyJob",le="300"} 16.0
sidekiq_job_runtime_seconds_bucket{queue="utils",worker="EmptyJob",le="+Inf"} 16.0
sidekiq_job_runtime_seconds_sum{queue="utils",worker="EmptyJob"} 7.963
sidekiq_job_runtime_seconds_count{queue="utils",worker="EmptyJob"} 16.0
А теперь рисуем 🎨
Алерты!
Пусть роботы анализируют метрики
🔔
yabeda
Набор библиотек для удобной работы с метриками
# Gemfile
gem "yabeda-rails"
# Yay! Standard RoR metrics already tracked!
gem "yabeda-sidekiq"
# Yay! Sidekiq metrics are registered and tracked!
gem "yabeda-prometheus"
# Now you can export metrics to Prometheus
# But some configuration is still needed
gem "yabeda-newrelic"
# And to NewRelic at the same time (not yet released though)
Пока WIP :-)
yabeda
DSL для описания метрик
Yabeda.configure do
group :sidekiq
counter(:jobs_total_count, comment: "Total number of jobs sidekiq executed.")
counter(:jobs_success_count, comment: "Total number of jobs successfully processed by sidekiq.")
counter(:jobs_failed_count, comment: "Total number of jobs failed in sidekiq.")
histogram(:job_runtime, unit: :seconds, comment: "Job execution time.")
gauge(:jobs_waiting_count, comment: "The number of jobs waiting to process in sidekiq.")
end
yabeda
DSL для сбора метрик
# Явно в коде
Yabeda.things_duration.measure({type: :foo}, 10)
Yabeda.things_happened_total.increment({type: :foo})
Yabeda.configure do
# Подписываемся на события в системе
ActiveSupport::Notifications.subscribe 'occurence.custom_things' do |*args|
things_duration.measure({type: :foo}, 10)
things_happened_total.increment({type: :foo})
end
# Получаем текущие значения когда приходит время отдать накопленные метрики
collect do
things_alive_total.set({}, Thing.alive.count)
end
end
yabeda
Хуки для сбора собственных метрик
Yabeda::Rails.after_controller_action do |event|
tags = {}
external_service_requests_total.increment(tags)
external_service_latency_seconds.measure(tags, event.duration)
end
Различия систем мониторинга
Prometheus | Librato | NewRelic | |
---|---|---|---|
Метод сбора: | Опрос сервером клиентов | 1. Отправка с клиента на сервер 2. Периодический вывод в лог |
Отправка с клиента на сервер |
Типы метрик: | counter gauge histogram summary |
counter gauge summary |
counter gauge histogram |
yabeda
Набор библиотек для удобной работы с метриками
Попробовать в деле:
github.com/yabeda-rb/example-prometheus-sidekiq
И это всё?
- Наличие подробных метрик приложения и его окружения драматично упрощает отладку (если озаботиться заранее).
- Продакшен по прежнему падает, но время восстановления меньше
- Изменения и оптимизации больше не делаются в слепую.
- С одного взгляда можно ответить на вопрос «Ну чо, как?»
- Можно бесконечно смотреть на три вещи: огонь, воду и графики.
Кажись, всё
✅ Метрики в сайдкике разобрал
✅ Теорию про мониторинг рассказал
✅ Ruby-код показал
✅ Гемы пропиарил
Вопросы?
GitHub, Twitter, Telegram: @Envek
Нам нужны твои мозги!
Скоро опубликуем продолжение!
Что в метриках тебе моих?
By Andrey “Envek” Novikov
Что в метриках тебе моих?
О чём могут поведать цифры и графики в мониторинге и зачем вообще заморачиваться.
- 2,782