О чём могут поведать цифры и графики в мониторинге и зачем вообще заморачиваться.
Андрей Новиков
RubyRussia 2018
🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
Чтобы понять, откуда вдруг запахло жареным:
Откуда — какой участок приложения горит.
Вдруг — когда загорелось. 5 минут назад, после деплоя или медленно, но верно разгоралось по мере роста сервиса?
Запах — в чём именно проблема? ЦП, память, внешние сервисы?
🤓
Непрерывный процесс наблюдения и регистрации параметров системы в сравнении с заданными критериями
«Вы не можете контролировать то, что не можете измерить»
— Том ДеМарко
Значения какой либо величины в порядке времени измерения
Для хранения разработаны специальные базы данных:
InfluxDB, Graphite, …
Позволяют сегментировать значения и агрегировать их вновь
Для каждого уникального набора пишется свой временной ряд
Есть опасность комбинаторного взрыва (не стоит записывать в них id пользователей)
CRDT: G-Counter — монотонно возрастающий счётчик
Базовый тип для более сложных метрик: histogram, …
Позволяет уверенно детектировать рестарты и суммировать значения хоть с тысячи воркеров.
Произвольное значение, без ограничений.
Используется для мониторинга состояния «внешнего мира»: Загрузка CPU, потребление памяти, количество процессов.
Сложные типы для сбора статистики по времени выполнения или потребления ресурсов: перцентили, среднее, медиана, …
Histogram: набор счётчиков по «корзинам».
Summary: Предпросчёт требуемой статистики клиентом на ходу.
| Prometheus | Librato | NewRelic | |
|---|---|---|---|
| Типы метрик: (🔧 — настраивается) | histogram 🔧 summary 🔧 | summary 🔧(перцентили) | summary (min, max, сумма квадратов, …) | 
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."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
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
endSidekiq.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
endcurl 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
Пусть роботы анализируют метрики
🔔
# 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.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.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
endYabeda::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 | 
Попробовать в деле:
github.com/yabeda-rb/example-prometheus-sidekiq
✅ Метрики в сайдкике разобрал
✅ Теорию про мониторинг рассказал
✅ Ruby-код показал
✅ Гемы пропиарил
GitHub, Twitter, Telegram: @Envek
Скоро опубликуем продолжение!