Riemann

В предыдущих статьях мы уже не раз затрагивали проблематику мониторинга, сбора и
хранения метрик (см., например, здесь и здесь). Сегодня мы хотели бы снова вернуться к этой теме и рассказать о необычном, но весьма интересном инструменте — Riemann.


По сравнению с другими системами мониторинга он отличается повышенной сложностью,
и в то же время — гораздо большей гибкостью и отказоуcтойчивостью. На просторах Интернета нам доводилось встречать публикации, где Riemann характеризуют как «самую гибкую систему мониторинга в мире». Riemann хорошо подходит для сбора информации о работе сложных высоконагруженных систем в реальном масштабе времени.

Собственно говоря, системой мониторинга в строгом смысле Riemann не является. Правильнее было бы его называть инструментом обработки событий (event processor).
Он собирает информацию о событиях с хостов и приложений, объединяет события в поток и передаёт их другим приложениям для дальнейшей обработки или хранения. Также Riemann отслеживает состояние событий, что позволяет создавать проверки и рассылать уведомления.

Riemann распространяется бесплатно по лицензии Eclipse. Большая часть кода написана Кайлом Кингсбери, известном также под псевдонимом Aphyr (кстати, рекомендуем почитать его блог: там часто бывают интересные материалы).

Обработка событий в реальном масштабе времени



Рост интереса к проблематике мониторинга, сбора, хранение и анализа метрик, который мы наблюдаем в последнее время, вполне объясним: вычислительные системы становятся всё более сложными и более высоконагруженными. В случае с высоконагруженными системами особую важность приобретает возможность отслеживания событий в реальном масштабе времени. Собственно, Riemann и был создан для того, чтобы решить эту проблему.

Идея обработки событий в режиме, приближенном к реальному времени не нова: первые попытки её осуществления предпринимались ещё в конце 1980-х годов. В качестве примера можно назвать так называемые Active Database Systems (активные системы баз данных), которые выполняли определённый набор инструкций, если поступающие в базу данные соответствовали заданному набору условий.

В 1990-х годах появились системы управления потоками данных (Data Stream Management Systems), которые уже могли обрабатывать поступающие данные в реальном масштабе времени, и системы обработки сложных событий (Complex Event Processing, сокращённо CEP). Такие системы могли как обнаруживать события на основе внешних данных и заложенной внутренней логики, так и осуществлять определённые аналитические операции (например, подсчитывать количество событий за определённые период времени).

Примерами современных инструментов обработки сложных событий могут служить, в частности, Storm (см. также статью о нём на русском языке) и Esper. Они ориентированы на обработку данных без хранения. Riemann — продукт такого же класса. В отличии от того же Storm он гораздо более прост и логичен: вcя логика обработки событий может быть описана всего лишь в одном конфигурационном файле.
Многих системных администраторов-практиков эта особенность может и отпугнуть: конфигурационный файл по сути представляет собой код на языке Clojure, но котором написан и Riemann.

Clojure относится к функциональным (а говоря ещё точнее — лиспообразным) языкам программирования, что само по себе уже настораживает. Однако в этом нет ничего страшного: при всём своём своеобразии Clojure не так сложен, как кажется на первый взгляд. Рассмотрим его особенности более подробно.

Немного о Clojure



Clojure представляет собой функциональный язык, созданный на базе LISP. Программы, написанные на Clojure, работают на платфоре JVM. Первая версия этого языка появилась в 2007 году. Совсем недавно вышла в свет последняя на сегодняшний день версия — 1.8.0.

Clojure используется в проектах таких компаний, как Facebook, Spotify, SoundCloud, Amazon и других (полный список см. на официальном сайте).

В отличие от других реализаций LISP для JVM (например, ABCL или Kawa), Clojure не совместим полностью ни с Common Lisp, ни со Scheme, однако из этих языков в нём очень много позаимствовано. Есть в Clojure и некоторые усовершенствования, которых нет в других современных диалектах LISP: неизменность данных, конкуретное выполнение кода и т.п.

Так как Clojure был изначально спроектирован для работы с JVM, в нём можно работать с многочисленными библиотеками, существующими для этой платформы. Взаимодействие с Java реализовано в обе стороны. можно вызывать код, написанный для Java. Возможна также реализация классов, доступных для вызова из Java и других языков программирования, работающих на базе JVM — например, для Scala. Более подробно о Clojure и его возможностях можно прочитать в этой статье, а также на официальном сайте Riemann. Рекомендуем также ознакомиться ещё с одним кратким, но весьма информативным введением в Clojure.

Установка и первый запуск



Чтобы работать с Riemann, нам сначала понадобится установить все необходимые
зависимости: Java и Ruby (на нём написаны некоторые дополнительные компоненты, о которых речь ещё пойдёт ниже):

$ sudo apt-get -y install default-jre ruby-dev build-essential


Далее загрузим и установим последнюю версию Riemann:

$ wget https://aphyr.com/riemann/riemann-0.2.10_all.deb
$ dpkg -i riemann-0.2.10_all.deb


Далее выполним:

$ sudo service riemann start


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

$ gem install riemann-client riemann-tools 


Вот и всё. Для начала работы с Riemann всё готово. Прежде чем перейти к практической части, сделаем небольшое теоретическое отступление и проясним смысл важнейших понятий: события, потоки и индекс.

События, потоки и индекс



Базовым понятием в Riemann является событие. События можно обрабатывать, подсчитывать, собирать и экспортировать в другие программы. Событие может выглядеть, например, так:

{:host riemann, :service riemann streams rate, :state ok, :description nil, :metric 0.0, :tags [riemann], :time 355740372471/250, :ttl 20}


Приведённое событие состоит из следующих полей:

  • :host — имя хоста;
  • :service — имя наблюдаемого сервиса;
  • :state — состояние события (ok, warning, critical);
  • :tags — метки события;
  • :time — время наступления события в формате Unix Timestamp;
  • :description — описание события в произвольной форме;
  • :metric — метрика, ассоциированная с событием;
  • :ttl — время актуальности события (в секундах).


Некоторые события могут также иметь кастомные поля, которые можно добавлять ак во время создания, так и во время обработки событий (например, поля с дополнительными метриками).
Все события объединяются в потоки. Поток — это функция, которой может быть передано событие.

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

:host www, :service apache connections, :state nil, :description nil, :metric 100.0, :tags [www], :time 466741572492, :ttl 20


Это событие, произошедшее на хосте www в сервисе apache connections. В индексе всегда хранится последнее на текущий момент событие. К индексам можно обращаться из потоков и даже из внешних сервисов.

Мы уже видели, что каждое событие содержит поле TTL (time to live). TTL — это промежуток времени, в течение которого событие является актуальным. В только что приведённом примере TTL события составляет 20 секунд. В индекс попадают все события с параметрами :host www и :service apache connections. Если в течение 20 секунд таких событий не происходит, будет создано новое событие со значением expired в поле state. Затем оно будет добавлено в поток.

Конфигурирование



Перейдём от теории к практике и займёмся конфигурированием Riemann. Откроем конфигурационный /etc/riemann/riemann.config. Он представляет собой программу на Clojure и по умолчанию выглядит так:

; -*- mode: clojure; -*-
; vim: filetype=clojure

(logging/init {:file "/var/log/riemann/riemann.log"})

; Listen on the local interface over TCP (5555), UDP (5555), and websockets
; (5556)
(let [host "127.0.0.1"]
  (tcp-server {:host host})
  (udp-server {:host host})
  (ws-server  {:host host}))

; Expire old events from the index every 5 seconds.
(periodically-expire 5)

(let [index (index)]
  ; Inbound events will be passed to these streams:
  (streams
    (default :ttl 60
      ; Index all events immediately.
      index

      ; Log expired events.
      (expired
        (fn [event] (info "expired" event))))))



Этот файл разделён на несколько разделов. Каждый раздел начинается с комментария, обозначаемого, как это принято в Clojure, точкой с запятой (;).

В первом разделе указан файл, в который будут записываться логи. Далее идёт раздел с указанием интерфейсов. Обычно Riemann слушает на TCP-, UDP- и вебсокет-интерфейсе. По умолчанию все они привязаны к локальному хосту (127.0.0.1).

Следующий раздел содержит настройки для событий и индекса:

(periodically-expire 5)

(let [index (index)]
  ; Inbound events will be passed to these streams:
  (streams
    (default :ttl 60
      ; Index all events immediately.
      index


Первая функция (periodically-expire) убирает из индекса все события, у которых истёк период актуальности, и присваивает им статус expired. Очистка событий запускается каждые 5 секунд.

По умолчанию Riemann копирует в события с истёкшим сроком актуальности поля :service и :host. Можно с копировать и другие поля; для этого нужно с функцией periodically-expired использовать опцию :key-keys. Вот так, например, мы можем дать указание сохранять не только имя хоста и имя сервиса, но ещё и тэги:

(periodically-expire 5 {:keep-keys [:host :service :tags]})


Далее следует конструкция, в которой мы определяем символ с именем index. Значение этого символа — index, т.е. это функция, которая отправляет события в индекс. Она используется, чтобы указать Riemann, когда индексировать то или иное событие.

С помощью функции streams мы описываем потоки. Каждый поток представляет собой функцию, принимающую в качестве аргумента событие. Функция streams указывает Riemann: «вот список функций, которые нужно вызывать при добавлении новых событий». Внутри этой функции мы устанавливаем TTL для событий — 60 секунд. Для этого мы воспользовались функцией default, которая берёт поле из события и позволяет установить для него значение по умолчанию. События, у которых нет TTL, будет получать статус expired.

Затем дефолтная конфигурация вызывает символ index. Это означает, что все поступающие события будут добавляться в индекс автоматически.

Заключительный раздел содержит указание логгировать события со статусом expired:

; Log expired events.
      (expired
        (fn [event] (info "expired" event))))))


Внесём в конфигурационный файл некоторые изменения. В разделе, посвящённом сетевым интерфейсам, заменим 127.0.0.1 на 0.0.0.0, чтобы Riemann мог принимать события с любого хоста.

В самый конец файла добавим:

;print events to the log
(streams
  prn

  #(info %))


Это функция prn, которая будет записывать события в логи и в стандартный вывод. После этого сохраним внесённые изменения и перезапустим Riemann.

В ситуации, когда приходится отслеживать работу множества сервера, можно создавать не общий конфигурационный файл, а целую директорию с отдельными файлами для каждого сервера или группы серверов (см. рекомендации в этой статье).

С подробной инструкцией по написанию конфигурационного файла можно познакомиться здесь.

Отправка данных в Riemann



Попробуем теперь отправить данные в Riemann. Воспользуемся для этого клиентом riemann-health, который входит в уже установленный нами ранее пакет riemann-tools. Откроем ещё одну вкладку терминала и выполним:

$ riemann-health


Эта команда передаёт Riemann данные о состоянии хоста (загрузка CPU, объём занятого дискового пространства, объём используемой памяти).
Riemann начнёт принимать события. Информация об этих событиях будет записываться в файл /var/log/riemann/riemann.log. Она представлена в следующем виде:

#riemann.codec.Event{:host "cs25706", :service "disk /", :state "ok", :description "8% used", :metric 0.08, :tags nil, :time 1456470139, :ttl 10.0}
INFO [2016-02-26 10:02:19,571] defaultEventExecutorGroup-2-1 - riemann.config - #riemann.codec.Event{:host cs25706, :service disk /, :state ok, :description 8% used, :metric 0.08, :tags nil, :time 1456470139, :ttl 10.0}
#riemann.codec.Event{:host "cs25706", :service "load", :state "ok", :description "1-minute load average/core is 0.02", :metric 0.02, :tags nil, :time 1456470139, :ttl 10.0}


Riemann-health — это лишь одна из утилит в пакете riemann-tools. В него входит довольно большое количество утилит для сбора метрик: riemann-net (для мониторинга сетевых интерфейсов), riemann-diskstats (для мониторинга подсистемы ввода-вывода), riemann-proc (для мониторинга процессов в Linux) и другие. С полным списком утилит можно ознакомиться здесь.

Создаём первую проверку



Итак, Riemann установлен и запущен. Попробуем теперь создать первую проверку. Откроем конфигурационный файл и добавим в него такие строки:

(let [index (index)] 
  (streams 
    (default :ttl 60 
      index 
   ;#(info %) 
   (where (and (service "disk /") (> metric 0.10)) 
    #(info "Disk space on / is over 10%!" %))  


Перед функцией (#info) стоит знак комментария — точка с запятой (;). Это сделано, чтобы Riemann не записывал каждое событие в лог. Далее мы описываем поток where. В него попадают события, которые соответствуют заданному критерию. В нашем примере таких критериев два:

  • поле :service должно иметь значение disk /;
  • значение поля :metric должно быть больше 0.10 или 10%.


Затем они передаются в дочерний поток для дальнейшей обработки. В нашем случае информация о таких событиях будет записываться в файл /var/log/riemann/riemann.log.

Фильтрация: краткая справка



Без фильтрации событий полноценная работа c Riemann невозможна, поэтому о ней стоит сказать несколько слов отдельно.

Начнём с фильтрации событий с помощью регулярных выражений. Рассмотрим следующий пример описания потока where:

where (service #”^nginx”)) 


В Clojure регулярные выражения обозначаются знаком # и заключаются в двойные кавычки. В нашем примере в поток where будут попадать выражения, у которые содержат имя nginx в поле :service.

События в потоке where можно объединять с помощью логических операторов:

(where (and (tagged "www") (state "ok"))) 


В этом примере в поток where будут попадать события с тэгом www и значением ok в поле state. Они объединяются с событиями из потока tagged.
Tagged — это сокращённое имя функции tagged-all, которая объединяет все события с заданными тэгами. Еcть ещё функция tagged-any — она объединяет в поток события, отмеченные одним или несколькими из указанных тэгов:

(tagged-any ["www" "app1"] #(info %)) 


В нашем примере в поток tagged попадут события, отмеченные тэгами www и app1.

По отношению к событиям можно выполнять математические операции, например:

(where (and (tagged "www") (>= (* metric 10) 5)))


В этом примере будут события будут попадать события с тэгом www, у которых значение поля :metric, умноженное на 10, будет больше 5.
Аналогичный синтаксис можно использовать, чтобы выбирать события, у которых значения в поле :metric попадают в указанный диапазон:

(where (and (tagged "www") (< 5 metric 10)))



В приведённом примере в поток where будут попадать события с тэгом www, у которых значение поля :metric находится в диапазоне 5 —10.

Настройка уведомлений



Riemann может рассылать уведомления в случае соответствия заданным условиям проверок. Начнём с настройки уведомлений по электронной почте. В Riemann для этого используется функция email:

[
(def email (mailer {:from "riemann@example.com"}))

(let [index (index)]
; Inbound events will be passed to these streams:
(streams
  (default :ttl 60
    ; Index all events immediately.
    index

    (changed-state {:init "ok"}
      (email "andrei@example.com")))))


Рассылки уведомлений в Riemann осуществляется на базе специальной библиотеки на Clojure — Postal. По умолчанию для рассылки используется локальный почтовый сервер.
Все сообщения будут отправляться с адреса вида riemann@example.com.

Если локальный почтовый сервер не установлен, Riemann будет выдавать сообщения об ошибке вида:

riemann.email$mailer$make_stream threw java.lang.NullPointerException


В приведённом выше примере кода мы использовали ярлык changed-state и тем самым указали, что Riemann должен отслеживать события, состояние которых изменилось. Значение переменной init сообщает Riemann, каким было начальное состояние события. Все события, состояние которых изменилось с ok на какое-либо другое, будут передаваться функции email. Информация о таких событиях будет отправлена на указанный адрес электронной почты.
С более подробными примерами настройки уведомлений можно ознакомиться в статье Джеймса Тернбулла, одного из разработчиков Riemann.

Визуализация метрик: riemann-dash



В Riemann имеется собственный инструмент для визуализации метрик и построения простых дашбордов — riemann-dash. Установить его можно так:

$ git clone git://github.com/aphyr/riemann-dash.git
$ cd riemann-dash
$ bundle


Запускается riemann-dash с помощью команды:

$ riemann-dash


Домашняя страница riemann-dash доступна в браузере по адресу [ip-aдрес сервера]:4567:

riemann-dash

Подведём к чёрной надписи Riemann в самом центре, нажмём клавишу Ctrl (на Mac — cmd) и кликнем по ней. Надпись будет выделена серым цветом. После этого нажмём на клавишу E, чтобы приступить к редактированию:

riemann-dash

В выпадающем меню title выберем пункт Grid, а в поле query напишем true:

riemann-dash

Установив необходимые настройки, нажмём на кнопку Apply:

riemann-dash

Дашборд получается не особо эстетичный и удобный, но вполне наглядный. Неудобство, однако, компенсируется тем, что с Riemann можно использовать сторонние инструменты визуализации, d в частости Graphite и Grafana — заитересованный читатель без труда сможет найти соответствующие публикации в Интернете. А процедуру настройки связки Riemann+InfluxDB+Grafana мы опишем в следующем разделе.

Отправка данных в InfluxDB



Несомненным преимуществом Riemann являются широкие возможности интеграции. Собранные с его помощью метрики можно отправлять в сторонние хранилища. Ниже мы покажем, как интегрировать Riemann c InfluxDB и настроить визуализацию данных с помощью Grafana.

Установим InfluxDB:

$ wget https://s3.amazonaws.com/influxdb/influxdb_0.9.6.1_amd64.deb
$ sudo dpkg -i influxdb_0.9.6.1_amd64.deb


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

По завершении установки выполним команду:

$ sudo /etc/init.d/influxdb start


Затем создадим базу для хранения данных из Riemann:

$ sudo influx

>CREATE DATABASE riemann


Создадим для этой базы пользователя и установим для него пароль:

>CREATE USER riemann WITH PASSWORD ‘пароль пользователя riemann’
>GRANT ALL ON riemann TO riemann


Вот и всё, установка и базовая настройка InfluxDB завершены. Теперь нужно прописать необходимые настройки в конфигурационном файле Riemann (код взят отсюда и незначительно модифицирован):

; -*- mode: clojure; -*-
; vim: filetype=clojure

;подключаем capacitor, клиент для работы с InfluxDB
(require 'capacitor.core)
(require 'capacitor.async)
(require 'clojure.core.async)

(defn make-async-influxdb-client [opts]
    (let [client (capacitor.core/make-client opts)
          events-in (capacitor.async/make-chan)
          resp-out (capacitor.async/make-chan)]
        (capacitor.async/run! events-in resp-out client 100 10000)
        (fn [series payload]
            (let [p (merge payload {
                    :series series
                    :time   (* 1000 (:time payload)) ;; s > ms
                })]
                (clojure.core.async/put! events-in p)))))

(def influx (make-async-influxdb-client {
        :host     "localhost"
        :port     8086
        :username "riemann"
        :password "пароль пользователя riemann"
        :db       "riemann"
    }))

(logging/init {:file "/var/log/riemann/riemann.log"})


(let [host "0.0.0.0"]
  (tcp-server {:host host})
  (udp-server {:host host})
  (ws-server  {:host host}))

(periodically-expire 60)

(let [index (index)]
  (streams
        index

        (fn [event]
            (let [series (format "%s.%s" (:host event) (:service event))]
                (influx series {
                    :time  (:time event)
                    :value (:metric event)
                })))))


Сохраним внесённые изменения и перезапустим Riemann.

После этого установим Grafana:

$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_2.6.0_amd64.deb
$ sudo dpkg -i grafana_2.6.0_amd64.deb


Подробных инструкций по настройке Grafana мы приводить не будем, да в этом и нет особой необходимости: соответствующие публикации можно без труда найти в Интернете.

Домашняя страница Grafana будет доступна в браузере по адресу http://[IP-адрес сервера]:3000. Далее потребуется лишь добавить новый источник данных (InfluxDB) и создать дашборд.

Заключение



В этой статье мы представили краткий обзор возможностей Riemann. Мы затронули следующие темы:

  • особенности языка Clojure;
  • установка и первичная настройка Riemann;
  • структура конфигурационного файла и особенности его синтаксиса;
  • создание проверок;
  • настройка уведомлений;
  • визуализация метрик с помощью riemann-dash
  • интеграция Riemann c InfluxDB и визуализация метрик с помощью Grafana


Если вам кажется, что мы упустили какие-то важные детали — напишите нам, и мы дополним обзор. А если вы используете Riemann на практике, приглашаем поделиться опытом в комментариях.

Если вы по тем или иным причинам не можете оставлять комментарии здесь — добро пожаловать в наш корпоративный блог.

Комментарии (7)


  1. Arrest
    15.04.2016 12:18

    Эй, а ничего, что в riemann.influxdb уже лежит клиент для InfluxDB? Зачем его второй раз писать?


  1. Scratch
    15.04.2016 12:47
    +1

    Используем риман около года в связке с ELK. Отличная вещь, но добавлять в него правила — сущий гемор. Сейчас думаем над фасадом в виде внешнего легкочитаемого файлика, который позволял бы гибко настраивать алерты. Так же, не хватает отказоустойчивого кластера. Упал риман\рестартанулся и всё, все метрики/ttl слетели.


  1. acmnu
    18.04.2016 09:18

    Кто-нибудь может подсказать что можно ли подобный софт использовать для мониторинга предопределённых событий? Ну например, каждую ночь идет бэкап определённого хоста. При этом, сутуация, когда он закончился фейлом ловится любой системой мониторинга хорошо, а вот ситуация, когда он вообще в тихую не начался, требует сопоставления с эталонным расписанием. И вот этот случай я как-то не знаю чем можно покрыть. Читаешь о всех этих модных средствах и аж глаза разбегаются.


    1. Scratch
      18.04.2016 09:29
      +1

      В римане есть таймаут у событий. Т.е. если что-то не произошло в заданный промежуток времени, то он может сгенерировать алерт.
      Я так сделал довольно оригинальную фичу — оповещение о падении и поднятии одного глючного клиентского FTP сервера. Если сервер падает, то мы отсылаем клиенту сообщение «Подними сервак» и ждем, пока логи о падении сервака перестанут приходить. Как перестают — отсылаем сообщение, мол сервак поднялся.

      http://riemann.io/howto.html#detect-down-services


      1. acmnu
        18.04.2016 11:47

        Немного не то. TTL это вещь понятная: наступило событие, если через n секунд не наступило ещё раз, значит дело труба. Во многих системах мониторинга это называется heart beat — отслеживаение периодических событий. В случае бэкапа дело сложнее. Во-первых, расписание не имеет четкого периода. Логика расписания намного сложнее и завязана на бизнес нагрузку (днем легкие инкременталы, ночью полный бэкап, но не в каждую ночь). Во-вторых, нужно анализировать не только время запуска, но и длительность, которая тоже разная в разные периоды месяца, и нужно уметь отследить аномалии в длительности или объеме бэкапа.


        1. Scratch
          18.04.2016 13:08

          Есть метрики, их тоже можно использовать для алерта. Можно ttl передавать прям в сообщении, у нас так делается. Это если вы заранее знаете что должно произойти, а что нет. Например — запланировали бекап на 11:00, отправили сообщение риману, которое протухает в 11:10. Если бекап сделаться не успел, то алерт по протуханию. Сам он вычислять аномалии, конечно, не будет. А сложную логику на clojure писать я никому не пожелаю


    1. NikolaySivko
      18.04.2016 12:43

      Проще всего брать размер целевого файла бэкапа, если файла нет — размер 0. Алерт, если размер бэкапа за сегодня меньше N байт. Проверка корректности — отдельная задача.