Что в метриках тебе моих?

О чём могут поведать цифры и графики в мониторинге и зачем вообще заморачиваться.

Андрей Новиков
RubyRussia 2018

🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥

Чего мы хотим?

  • Знать, как быстро растут и разгребаются очереди
  • Знать, какие задачи наиболее «проблемные»
  • Понимать, когда это началось и кто виноват
  • Понимать, помогли ли принятые нами меры

Чего мы хотим?

  • Больше метрик
  • Больше графиков
  • Ещё больше графиков
  • … и ещё два монитора

Недостающие метрики

  • Время обработки задач
  • Количество новых задач

Недостающие графики

  • Динамика изменения размера очередей по отдельным очередям
  • Динамика изменения времени выполнения задач по классам

Мониторинг веб-приложения

  • Сервера: загрузка CPU, RAM, IO, LA, заполненность дисков, …
  • Веб: количество запросов, время их обработки, queueing time, …
  • Отложенные задачи: количество задач, время их обработки, количество ошибок, …
  • Базы данных: количество запросов, время обработки, объём данных, …
  • Внешние API: количество запросов, время обработки, ошибки…
  • Сеть: задержки, DNS lookup time, throughput, потери пакетов, …
  • Свои показатели: свой набор под каждую интеграцию
  • Бизнес-метрики: количество пользователей, заказов, 🤑, …

А зачем?

Чтобы понять, откуда вдруг запахло жареным:

  • Откуда — какой участок приложения горит.

  • Вдруг — когда загорелось. 5 минут назад, после деплоя или медленно, но верно разгоралось по мере роста сервиса?

  • Запах — в чём именно проблема? ЦП, память, внешние сервисы?

Теория

🤓

Мониторинг

Непрерывный процесс наблюдения и регистрации параметров системы в сравнении с заданными критериями

 

«Вы не можете контролировать то, что не можете измерить»

— Том ДеМарко

Метрики

  1. Параметры системы
  2. Численные значения, полученные при измерении этих параметров работающей системы

Временные ряды (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-код показал

✅ Гемы пропиарил

Вопросы?

envek@envek.name

GitHub, Twitter, Telegram: @Envek

Нам нужны твои мозги!

evilmartians.com/chronicles

dev.to/evilmartians

GitHub, Twitter, Telegram: @evilmartians

Скоро опубликуем продолжение!

Что в метриках тебе моих?

By Andrey “Envek” Novikov

Что в метриках тебе моих?

О чём могут поведать цифры и графики в мониторинге и зачем вообще заморачиваться.

  • 2,782