Эта статья на текущий момент является черновиком. Всё будет оформлено в виде ansible-репозитория, здесь будут только комментарии.

С чего всё началось

Захотелось мне посмотреть на наш основной продукт, биллинг. Красивый ли он, есть ли аналитика, на которую взглянуть приятно аль нет. И что-то не особо. То есть я не хочу сказать, что он беден на аналитику, отчётов там дохрена, отчёт “Для Директора” даже довольно таки красивый (настолько, даже моя ненависть к ущербному неуместному использованию больших букв останется в рамках этих скобочек!). Но остальные же отчёты - сраная скучная текстовая табличка в браузере, которую можно выгрузить в CSV/Excel/DBF (wow!).

И захотелось мне узнать, можно ли любопытному провайдеру прикрутить туда что-то красивое и прекрасное самостоятельно, не формируя сложное техническое задание нашим суппортам, не создавая 50 фич-реквестов и всё такое, не покупая для этого высокоуровневую подписку (SLA4-аутсорсинг или какие там сейчас у нас модные).

Собственно стэк решил использовать привычный мне:

  • grafana для отображения графиков
  • influxdb как хранилище метрик
  • python как инструмент опроса биллинга и отправки метрик в influxdb
  • crond как пинатель питоновых скриптов
  • collectd - как основной инструмент сбора метрик. Возможно исключит необходимость в crond.
  • ansible - как инструмент для поддержания правильного состояния всех конфигураций на нескольких серверах и облегчения повторного развёртывания при необходимости.

Disclaimer: Никаких алертеров или чего-то сверхумного, детекторов аномалий, больших данных и машинного обучения тут не будет. Просто приемлемая визуализация.

Установка

Всё ставится на отдельную машину. Я втыкал в CentOS 6 внутри OpenVZ контейнера. Разворачивал всё с помощью ansible, есть правда косяк в плейбуке, надо вручную одно подтверждение сделать при установке grafana:

файл files/influxdb.conf

reporting-disabled = false
[meta]
  dir = "/var/lib/influxdb/meta"
  retention-autocreate = true
  logging-enabled = true
  pprof-enabled = false
  lease-duration = "1m0s"
[data]
  enabled = true
  dir = "/var/lib/influxdb/data"
  wal-dir = "/var/lib/influxdb/wal"
  wal-logging-enabled = true
[coordinator]
  write-timeout = "10s"
  max-concurrent-queries = 0
  query-timeout = "0"
  log-queries-after = "0"
  max-select-point = 0
  max-select-series = 0
  max-select-buckets = 0
[retention]
  enabled = true
  check-interval = "30m"
[shard-precreation]
  enabled = true
  check-interval = "10m"
  advance-period = "30m"
[monitor]
  store-enabled = true # Whether to record statistics internally.
  store-database = "_internal" # The destination database for recorded statistics
  store-interval = "10s" # The interval at which to record statistics
[admin]
  enabled = true
  bind-address = ":8083"
  https-enabled = false
  https-certificate = "/etc/ssl/influxdb.pem"
[http]
  enabled = true
  bind-address = ":8086"
  auth-enabled = false
  log-enabled = true
  write-tracing = false
  pprof-enabled = false
  https-enabled = false
  https-certificate = "/etc/ssl/influxdb.pem"
  max-row-limit = 10000
  realm = "InfluxDB"
[subscriber]
  enabled = true
[[graphite]]
  enabled = false
[[collectd]]
  enabled = true
  database = "softrouter"
  typesdb = "/usr/share/collectd/types.db"
[[opentsdb]]
  enabled = false
[[udp]]
  enabled = false
[continuous_queries]
  log-enabled = true
  enabled = true

Файл tasks/influxdb.yml

- hosts: [influxdb]
  tasks:
  - name: grafana.repo
    copy: src=../files/grafana.repo dest=/etc/yum.repos.d/grafana.repo
  - name: grafana
    yum: name=grafana state=present
  - name: crond
    yum: name=cronie state=present
  - name: grafana
    yum: name=cronie state=present
  - name: enable crond
    service: name=crond enabled=yes state=restarted
  - name: enable grafana
    service: name=grafana-server enabled=yes state=restarted
  - name: collectd (for libs)
    yum: name=collectd state=present
  - name: influxdb
    yum: name=https://dl.influxdata.com/influxdb/releases/influxdb-1.0.2.x86_64.rpm state=present
  - name: configure influxdb
    copy: src=../files/influxdb.conf dest=/etc/influxdb/influxdb.conf
  - name: enable influxdb
    service: name=influxdb enabled=yes state=restarted

Поскольку раз уж всё равно экспериментирую в своё свободное время, решил использовать python3.4. Вообще я на нём особо не пишу, так что возможно будет много косяков ниже в статье в примерах кода.

Пробуем юзать

Настраиваем InfluxDB

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

CREATE DATABASE "softrouter";
CREATE RETENTION POLICY "limitations" ON "softrouter" DURATION 180d REPLICATION 1 DEFAULT;

Настраиваем Grafana

Открываем :3000 порт в первый и далеко не последний раз, вводим admin/admin, меню -> data sources -> + add data source.

  • name softrouter
  • default: +
  • type influxdb
  • url http://localhost:8086
  • database softrouter

Больше особо менять не надо.

Дальше Dashboards -> Create New -> Зелёная херня сбоку -> Add Panel -> Graph. Теперь временно забываем про графики и начинаем думать что бы нам такого пособирать из данных.

Собираем данные

Вообще данные в нашем случае делятся два вида - технические и бизнес.

Поскольку я обкатываю всё на нашем тестовом софтроутере, который служит нам на работе гейтом в инет, то начну с технических.

Технические

Так как у нас под коробкой - Linux, собирать системные данные проще всего с помощью collectd. Там даже думать особо не надо, всё есть в репах.

Настройка: настраивать будем с помощью ansible, поскольку collectd нужно установить на все машины, которые мы хотим отслеживать.

Создадим ansible-playbook для быстрого разворачивания collectd на других машинах и возможности менять конфигурацию в одном месте.

Файл: tasks/collectd.yml:

- hosts: [collectd]
  tasks:
    - name: install
      yum: name=collectd state=present
    - name: configure
      template: src=../templates/collectd.conf.j2 dest=/etc/collectd.conf
    - name: enabled
      service: name=collectd enabled=yes state=restarted

Файл: templates/collectd.conf:

FQDNLookup false
Interval   60
LoadPlugin syslog
LoadPlugin conntrack
LoadPlugin cpu
LoadPlugin disk
LoadPlugin interface
LoadPlugin iptables
LoadPlugin irq
LoadPlugin load
LoadPlugin memory
LoadPlugin network
LoadPlugin swap
LoadPlugin tcpconns
LoadPlugin uptime
<Plugin disk>
    Disk "/^sda[0-9]$/"
    IgnoreSelected false
</Plugin>
<Plugin network>
Server "10.50.140.131"
</Plugin>
Include "/etc/collectd.d"

По красивому IP адрес сервера с collectd надо вынести в переменные группы в файле inventory, но я отложу это на потом.

Запускаем:

ansible-playbook tasks/collectd.yml -l IP-второй-машины

После чего по адресу :8083 можем наблюдать в выводе

show measurements

всё, за чем мы теперь можем наблюдать, у меня:

  • conntrack_entropy
  • cpu_value
  • disk_read
  • disk_write
  • interface_rx
  • interface_tx
  • irq_value
  • load_longterm
  • load_midterm
  • load_shortterm
  • memory_value
  • swap_value
  • tcpconns_value
  • uptime_value

PPS сетевых карт

В случае с шлюзом и DPI очень важными являются interface_rx и interface_tx. С них и начнём.

Вот пример запроса про входящий pps на eth0 для grafana:

SELECT
    non_negative_derivative(mean("value"), 1s)
FROM
    "interface_rx"
WHERE
    "host" = 'Gate'
    AND "type" = 'if_packets'
    AND "type_instance" = 'eth0'
    AND $timeFilter
GROUP BY
    time($interval) fill(null)

IRQ сетевой карты

Можно следить за ростом прерываний на сетевых картах.

У нас используется не очень крутая карта, крутится с одной очередью на 26 прерывании.

SELECT
    non_negative_derivative(mean("value"), 1s)
FROM
    "irq_value"
WHERE
    "host" = 'Gate'
    AND "type" = 'irq'
    AND "type_instance" = '26'
    AND $timeFilter
GROUP BY
    time($interval) fill(null)

Единственная проблема которая при такой схеме будет - это отслеживание IRQ сетевых карт. Но в принципе можно отнести это к бизнес-логике, а не техническим данным и захардкодить для каждого хоста (прости господи) или вынести на сторону какого-то своего плагина к collectd.

CPU Usage

Memory Usage

Шаблонизирование в grafana

Вообще много графиков которые находятся рядом имеют очень похожие запросы в основе, меняется как правило одна переменная. Чтобы не менять постоянно в куче мест запросы, можно использовать settings -> templating.

Шаблонизируем uptime load

Сперва добавляем новую переменную, назовём её uptime_kind, type = custom. Values:

load_longterm, load_middleterm, load_shortterm

Multivalue: +

Сохраняем, теперь вверху можно выбирать. Выбираем все. В настройках графика с uptime выбираем from default /^uptime_kind$/ (не забываем указывать host, который кстати тоже можно использовать в шаблонизации).

Теперь идём в general -> repeat panel -> uptime_kind. Ставим span=4, minimal span=4, сохраняем и обновляем страницу. Кстати, переменные можно подставлять куда угодно => темплейтить можно всё что угодно, даже функции.

Шаблонизируем несколько хостов

Так как настраивать и использовать целую дэшбордину только ради одного хоста глупо, попробуем воспользоваться ей для других продуктов.

В templating в grafana добавим переменную Host:

type = query, multivalue отключаем, обновлять только при загрузке dashboard, сам query:

SHOW TAG VALUES FROM "cpu_value" WITH KEY = "host"

Собираем бизнес-данные

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

Биллинг

Число radius событий за час

Число авторизованных абонентов с распределением по NAS-серверам

Данные о работе технической поддержки (заявки в хелпдеске)

Carbon Reductor

NET_RX

Модули фильтрации

В collectd можно также создавать свои плагины. Поэтому всякий custom редуктора туда отлично вписывается. Проблема одна - collectd под рутом скрипты запускать не любит. Так что создаём группу и юзера ему:

groupadd collectd
adduser collectd -s '/bin/bash' -c 'statistic gatherer' -d '/' -g 'collectd' -G 'wheel'

В целом плагины в итоге выглядят довольно шаблонно:

xt_reductor

/etc/collectd.d/reductor.conf

LoadPlugin exec
<Plugin exec>
  Exec "collectd" "/usr/lib64/collectd/plugins/xt_reductor.sh"
</Plugin>

/usr/lib64/collectd/plugins/xt_reductor.sh

#!/usr/bin/env bash

PLUGIN_NAME='xt_reductor'
HOSTNAME="${COLLECTD_HOSTNAME:-$(hostname)}"
INTERVAL="${COLLECTD_INTERVAL:-10}"
FILE=/proc/net/${PLUGIN_NAME/xt/ipt}/block_list

replace_regex="s/Registration statement:/gauge-activated /;
	 s/URL count in database:/gauge-entries_load/;
	 s/Matched packets:/gauge-matched/;
	 s/Total packets checked:/gauge-checked/;
	 s/Elements count:/gauge-db_elements/;
"
while sleep $INTERVAL; do
	egrep -v 'Install number|Dont match counter' $FILE | sed -E "$replace_regex" > /tmp/$PLUGIN_NAME
	current_date=$(date +%s)
       	while read var val; do
		echo PUTVAL $HOSTNAME/$PLUGIN_NAME/$var $current_date:$val
	done < /tmp/$PLUGIN_NAME
	rm -f /tmp/$PLUGIN_NAME
done

xt_dnsmatch

/etc/collectd.d/dnsmatch.conf

LoadPlugin exec
<Plugin exec>
  Exec "collectd" "/usr/lib64/collectd/plugins/xt_dnsmatch.sh"
</Plugin>

/usr/lib64/collectd/plugins/xt_dnsmatch.sh

#!/usr/bin/env bash

PLUGIN_NAME='xt_dnsmatch'
HOSTNAME="${COLLECTD_HOSTNAME:-$(hostname)}"
INTERVAL="${COLLECTD_INTERVAL:-10}"
FILE=/proc/net/${PLUGIN_NAME/xt/ipt}/block_list

replace_regex="s/Registration statement:/gauge-activated /;
	 s/URL count in database:/gauge-entries_load/;
	 s/Matched packets:/gauge-matched/;
	 s/Total packets checked:/gauge-checked/;
	 s/Elements count:/gauge-db_elements/;
"
while sleep $INTERVAL; do
	egrep -v 'Install number|Dont match counter' $FILE | sed -E "$replace_regex" > /tmp/$PLUGIN_NAME
	current_date=$(date +%s)
       	while read var val; do
		echo PUTVAL $HOSTNAME/$PLUGIN_NAME/$var $current_date:$val
	done < /tmp/$PLUGIN_NAME
	rm -f /tmp/$PLUGIN_NAME
done

xt_snimatch

/etc/collectd.d/dnsmatch.conf

LoadPlugin exec
<Plugin exec>
  Exec "collectd" "/usr/lib64/collectd/plugins/xt_snimatch.sh"
</Plugin>

/usr/lib64/collectd/plugins/xt_snimatch.sh

#!/usr/bin/env bash

PLUGIN_NAME='xt_snimatch'
HOSTNAME="${COLLECTD_HOSTNAME:-localhost}"
INTERVAL="${COLLECTD_INTERVAL:-10}"
FILE=/proc/net/${PLUGIN_NAME/xt/ipt}/block_list

replace_regex="s/Registration statement:/gauge-activated /;
	 s/URL count in database:/gauge-entries_load/;
	 s/Matched packets:/gauge-matched/;
	 s/Total packets checked:/gauge-checked/;
	 s/Elements count:/gauge-db_elements/;
"
while sleep $INTERVAL; do
	egrep -v 'Install number|Dont match counter' $FILE | sed -E "$replace_regex" > /tmp/$PLUGIN_NAME
	current_date=$(date +%s)
       	while read var val; do
		echo PUTVAL $HOSTNAME/$PLUGIN_NAME/$var $current_date:$val
	done < /tmp/$PLUGIN_NAME
	rm -f /tmp/$PLUGIN_NAME
done

Настройки графиков в Grafana

Выглядеть это будет как-то так:

influxdb, grafana & reductor

Создадим новую дэшборду, в ней новый ряд, в него в первый график с названием Packet Processing добавим два запроса. Нас интересует их прирост за минуту, так что используем non_negative_derivative(x, 1m).

Проверенные пакеты:

SELECT
    non_negative_derivative(last("value"), 1m)
FROM
    /^$module$/
WHERE
    "host" =~ /^$host$/
    AND "type_instance" = 'checked'
    AND $timeFilter
GROUP BY
    time($interval)
    fill(null)

Число срабатываний:

SELECT
    non_negative_derivative(last("value"), 1m)
FROM
    /^$module$/
WHERE
    "host" =~ /^$host$/
    AND "type_instance" = 'matched'
    AND $timeFilter
GROUP BY
    time($interval)
    fill(null)

Создадим второй график, DB Stats, в него тоже два запроса:

Число загруженных URL/доменов

SELECT
    last("value")
FROM
    /^$module$/
WHERE
    "host" =~ /^$host$/
    AND "type_instance" = 'entries_load'
    AND $timeFilter
GROUP BY
    time($interval)
    fill(null)

и число элементов в базе данных (реально влияющая на производительность величина)

SELECT
    last("value")
FROM
    /^$module$/
WHERE
    "host" =~ /^$host$/
    AND "type_instance" = 'db_elements'
    AND $timeFilter
GROUP BY
    time($interval)
    fill(null)