В разрезе: новостной агрегатор на Android с бэкендом. Система мониторинга и визуализации данных (InfluxDB, Grafana)

    Вводная часть (со ссылками на все статьи)

    Сложные системы (распределённые/крупные/со сложной логикой/сложной системой данных) – как живой организм: подвижный, изменчивый и самостоятельный. Всё это требует постоянного контроля со стороны разработчиков/администраторов/DevOps-инженеров.

    К этому выводу я пришёл, когда система несколько раз «загибалась» в ходе её разработки, настройки сервера и эксплуатации. Это натолкнуло меня на мысль, что мониторинг должен осуществляться не только на этапе производственной эксплуатации, но и на этапе разработки.

    Обо всём по порядку…

    Когда я пришёл к выводу о необходимости мониторинга за проектом (как минимум серверной части) я решил, что идеальным вариантом для этого будет схема: «коллектор данных → TSDB → веб-клиент для отображения данных».

    image

    Выбор TSDB


    На текущий момент очень много статей посвящённых настройке Graphite в качестве TSDB, я же выбрал более современное и legacy-free решение на базе InfluxDB. Об InfluxDB уже писалось на Хабре в блоге компании Selectel. Не хочу копировать чужой текст, единственное могу сказать, что часть информации уже не соответствует действительности, но основа всё так же верна – система производительна, гибка, доступна для работы для разных языков и поддерживает разные протоколы других TSDB и агентов. Graphite же меня отпугнул наличием нескольких связанных друг с другом демонов, написанных на Python (излишняя сложность и дополнительные компоненты).

    Puppet-скрипт для установки и настройки InfluxDB
    class storyline_infra::influxdb () {
            include stdlib
    
            $params = lookup({"name" => "storyline_infra.influxdb",
                "merge" => {"strategy" => "deep"}})
            $port_http = $params['port_http']
            $port_rpc = $params['port_rpc']
            $pid_file = $params['pid_file']
            $init_script = $params['init_script']
            $dir_data = $params['dir_data']
            $dir_logs = $params['dir_logs']
            $enabled_auth = $params['enabled_auth']
            $enabled_startup = $params['enabled_startup']
            $enabled_running = $params['enabled_running']
            $version = $params['version']
    
            $dist_name = $facts['os']['name']
    
            user { 'influxdb':
                    ensure => "present",
                    managehome => true,
            }
            exec { "influxdb-mkdir":
                    command => "/bin/mkdir -p /data/db && /bin/mkdir -p /data/logs",
                    cwd => "/",
                    unless => '/usr/bin/test -d /data/db -a -d /data/logs',
            } ->
            # working dir
            file { $dir_logs:
                    ensure => "directory",
                    recurse => "true",
                    owner => "influxdb",
                    group=> "influxdb",
                    require => Exec['influxdb-mkdir'],
            }
            file { $dir_data:
                    ensure => "directory",
                    recurse => "true",
                    owner => "influxdb",
                    group=> "influxdb",
                    require => Exec['influxdb-mkdir'],
            }
            # see by "gpg --verify keyfile"
            apt::key { 'influxdb-key':
                    id => '05CE15085FC09D18E99EFB22684A14CF2582E0C5',
                    source  => 'https://repos.influxdata.com/influxdb.key',
            } ->
            # echo "deb https://repos.influxdata.com/${DISTRIB_ID,,} ${DISTRIB_CODENAME} stable" | sudo tee /etc/apt/sources.list.d/influxdb.list
            apt::source { 'influxdb-repo':
                    comment  => 'influxdb repo',
                    location => "https://repos.influxdata.com/${downcase($dist_name)}",
                    release => "${facts['os']['distro']['codename']}",
                    repos    => 'stable',
                    include  => {
                            'deb' => true,
                    },
            } ->
            package {  'influxdb':
                    ensure => $version,
            } ->
            file { "/etc/influxdb/influxdb.conf":
                    replace => true,
                    content => epp('storyline_infra/influxdb.epp'),
                    owner => "influxdb",
                    group=> "influxdb",
                    notify => Service['influxdb'],
            }->
            file { $init_script:
                    replace => true,
                    content => epp('storyline_infra/influxdb_startup.epp'),
                    mode=>"ug=rwx,o=r",
                    notify => Service['influxdb'],
            }->
            service { 'influxdb':
                    ensure => $enabled_running,
                    enable    => $enabled_startup,
                    start           => "${init_script} start",
                    stop            => "${init_script} stop",
                    status          => "${init_script} status",
                    restart         => "${init_script} restart",
                    hasrestart => true,
                    hasstatus => true,
            }
            if $enabled_startup != true {
                    exec { "disable_influxdb":
                            command => "/bin/systemctl disable influxdb",
                            cwd => "/",
                    }
            }
            logrotate::rule { 'influxdb':
                    path                    => "${dir_logs}/*.log",
                    rotate          => 10,
                    missingok               => true,
                    copytruncate    => true,
                    dateext                 => true,
                    size            => '10M',
                    rotate_every    => 'day',
            }
    }
    


    Grafana


    Выбор веб-клиента для отображения данных был сделан давно (видел его в действии давненько и всегда хотел использовать в своём проекте). Вот несколько скриншотов с моего проекта:

    image

    image

    image

    Особенностями grafana являются:

    • Приятный внешний вид
    • Динамичное обновление всех данных
    • Визуальный конструктор
    • Подключение большого количества типов источников данных (Graphite, InfluxDB, Prometheus, Elasticsearch…)
    • Множество способов аутентификации
    • Возможность отправки Alert`ов ( Slack, PagerDuty, VictorOps, OpsGenie...)
    • Большое количество plugin`ов для расширения функционала

    И ссылка на готовые dashboard'ы — есть чудесная возможность посмотреть как другие формируют графики и подчерпнуть что-то для себя (полезное и/или красивое). Я подчерпнул :)

    Клиент, идущий в комплекте с самим InfluxDB – Chronograf пока не настолько хорош в части функционала.

    Puppet-скрипт для установки и настройки Grafana
    class storyline_infra::grafana () {
            include stdlib
    
            $params = lookup({"name" => "storyline_infra.grafana",
                "merge" => {"strategy" => "deep"}})
            $port = $params['port']
            $pid_file = $params['pid_file']
            $init_script = $params['init_script']
            $dir_data = $params['dir_data']
            $dir_logs = $params['dir_logs']
            $enabled_startup = $params['enabled_startup']
            $enabled_running = $params['enabled_running']
            $version = $params['version']
    
            user { 'grafana':
                    ensure => "present",
                    managehome => true,
            }
            exec { "grafana-mkdir":
                    command => "/bin/mkdir -p /data/db && /bin/mkdir -p /data/logs",
                    cwd => "/",
                    unless => '/usr/bin/test -d /data/db -a -d /data/logs',
            } ->
            # working dir
            file { $dir_logs:
                    ensure => "directory",
                    recurse => "true",
                    owner => "grafana",
                    group=> "grafana",
                    require => Exec['grafana-mkdir'],
            }
            file { $dir_data:
                    ensure => "directory",
                    recurse => "true",
                    owner => "grafana",
                    group=> "grafana",
                    require => Exec['grafana-mkdir'],
            }
            # see by "gpg --verify keyfile"
            apt::key { 'grafana-key':
                    id => '418A7F2FB0E1E6E7EABF6FE8C2E73424D59097AB',
                    source  => 'https://packagecloud.io/gpg.key',
            } ->
            # deb https://packagecloud.io/grafana/stable/debian/ jessie main
            apt::source { 'grafana-repo':
                    comment  => 'grafana repo',
                    location => "https://packagecloud.io/grafana/stable/debian/",
                    release => "jessie",
                    repos    => 'main',
                    include  => {
                            'deb' => true,
                    },
            } ->
            package {  'grafana':
                    ensure => 'present',
            }
            file { '/etc/init.d/grafana-server':
                    ensure => 'absent',
            } ->
            file { '/etc/grafana':
                    ensure => "directory",
            } ->
            file { "/etc/grafana/grafana.ini":
                    replace => true,
                    content => epp('storyline_infra/grafana.epp'),
                    owner => "grafana",
                    group=> "grafana",
                    notify => Service['grafana'],
            } ->
            file { $init_script:
                    replace => true,
                    content => epp('storyline_infra/grafana_startup.epp'),
                    mode=>"ug=rwx,o=r",
                    notify => Service['grafana'],
            }->
            service { 'grafana':
                    ensure => $enabled_running,
                    enable    => $enabled_startup,
                    start           => "${init_script} start",
                    stop            => "${init_script} stop",
                    status          => "${init_script} status",
                    restart         => "${init_script} restart",
                    hasrestart => true,
                    hasstatus => true,
            }
            if $enabled_startup != true {
                    exec { "disable_grafana":
                            command => "/bin/systemctl disable grafana",
                            cwd => "/",
                    }
            }
    }
    


    О сборе данных


    Основным источником отображения данных являются time-series данные из InfluxDB, а в неё они попадают из 2-х источников: демона collectd и java-библиотеки «com.github.davidb:metrics-influxdb».

    Collectd


    Collectd — демон, написанный на C, способный передавать данные своему сетевому аналогу, протокол которого InfluxDB и может эмулировать. «Из коробки» он может собирать достаточно большое количество метрик по серверному окружению и по части сервисов, возможность расширения достигается посредством модулей, написанных на python или Java.

    Функционал collectd по сбору информации о функционировании сервера (физического и виртуального) меня вполне устраивает, однако дополнительные настройки требуемые для сбора данных со сторонних сервисов (в моём случае Elsticsearch, MongoDB и Apache Storm) достаточно нетривиальны и не всегда функциональны в полном объёме (например для Elsticsearch не корректно собирается информация по скорости выполнения запроса при наличии нескольких shard на разных репликах). Скорее всего следует посмотреть в сторону родного клиент InfluxDB — Telegraf.

    Puppet-скрипт для установки и настройки Collectd
    class storyline_infra::collectd () {
            include stdlib
    
            $params = lookup({"name" => "storyline_infra.collectd",
                "merge" => {"strategy" => "deep"}})
            $server_port = $params['server_port']
            $server_address = $params['server_address']
            $pid_file = $params['pid_file']
            $init_script = $params['init_script']
            $dir_data = $params['dir_data']
            $dir_logs = $params['dir_logs']
            $enabled_startup = $params['enabled_startup']
            $enabled_running = $params['enabled_running']
            $version = $params['version']
    # mongo db
            $enabled_mongodb = $params['enabled_mongodb']
            $mongodb_user = $params['mongodb_user']
            $mongodb_password = $params['mongodb_password']
    # storm db
            $enabled_storm = $params['enabled_storm']
            $storm_ui_url = $params['storm_ui_url']
            # elasticsearch
            $enabled_elasticsearch = $params['enabled_elasticsearch']
            $elasticsearch_host = $params['elasticsearch_host']
            $elasticsearch_port = $params['elasticsearch_port']
            $elasticsearch_cluster = $params['elasticsearch_cluster']
    
            exec { "collectd-mkdir":
                    command => "/bin/mkdir -p /data/db && /bin/mkdir -p /data/logs",
                    cwd => "/",
                    unless => '/usr/bin/test -d /data/db -a -d /data/logs',
            } ->
            # working dir
            file { $dir_logs:
                    ensure => "directory",
                    recurse => "true",
                    require => Exec['collectd-mkdir'],
            }
            file { $dir_data:
                    ensure => "directory",
                    recurse => "true",
                    require => Exec['collectd-mkdir'],
            }
            package {  'collectd':
                    #       ensure => $version,
                    ensure => "present",
            } ->
            file { "/etc/collectd/collectd.conf":
                    replace => true,
                    content => epp('storyline_infra/collectd.epp'),
                    notify => Service['collectd'],
            }->
            file { $init_script:
                    replace => true,
                    content => epp('storyline_infra/collectd_startup.epp'),
                    mode=>"ug=rwx,o=r",
                    notify => Service['collectd'],
            }->
            service { 'collectd':
                    ensure => $enabled_running,
                    enable    => $enabled_startup,
                    start           => "${init_script} start",
                    stop            => "${init_script} stop",
                    status          => "${init_script} status",
                    restart         => "${init_script} restart",
                    hasrestart => true,
                    hasstatus => true,
            }
            if $enabled_startup != true {
                    exec { "disable_collectd":
                            command => "/bin/systemctl disable collectd & /bin/systemctl disable collectd.service",
                            cwd => "/",
                    }
            }
            if $enabled_mongodb {
                    package {  'python-pip':
                            ensure => "present",
                    } ->
                    exec { "install-pymongo":
                            command => "/usr/bin/python -m pip install pymongo",
                            cwd => "/",
                            unless => '/usr/bin/python -m pip show pymongo',
                    } ->
                    file { "/usr/share/collectd/mongodb":
                            ensure => "directory",
                    }->
                    file { "/usr/share/collectd/mongodb.py":
                            replace => true,
                            content => epp('storyline_infra/collectd_mongodb_py.epp'),
                    }->
                    file { "/usr/share/collectd/mongodb/types.db":
                            replace => true,
                            content => epp('storyline_infra/collectd_mongodb_types_db.epp'),
                    }->
                    file { "/etc/collectd/collectd.conf.d/mongodb.conf":
                            replace => true,
                            content => epp('storyline_infra/collectd_mongodb_conf.epp'),
                            notify => Service['collectd'],
                    }
            } # if $enabled_mongodb {
            # https://github.com/srotya/storm-collectd
            if $enabled_storm {
                    file { "/usr/share/collectd/java/storm-collectd.jar":
                            replace => true,
                            ensure => file,
                            source => "puppet:///modules/storyline_infra/storm-collectd.jar",
                    }->
                    file { "/etc/collectd/collectd.conf.d/storm.conf":
                            replace => true,
                            content => epp('storyline_infra/collectd_storm_conf.epp'),
                            notify => Service['collectd'],
                    }
            } # if $enabled_mongodb {
            # https://github.com/signalfx/integrations/tree/master/collectd-elasticsearch
            # https://github.com/signalfx/collectd-elasticsearch
            if $enabled_elasticsearch {
                    file { "/usr/share/collectd/elasticsearch.py":
                            replace => true,
                            content => epp('storyline_infra/collectd_elasticsearch_py.epp'),
                    }->
                    file { "/etc/collectd/collectd.conf.d/elasticsearch.conf":
                            replace => true,
                            content => epp('storyline_infra/collectd_elasticsearch_conf.epp'),
                            notify => Service['collectd'],
                    }
            } # if $enabled_mongodb {
    }
    


    com.github.davidb:metrics-influxdb


    Указанная библиотека фактически является адаптером для широко известной java-библиотеки метрик Metrics. Поддерживает протокол версии 0.9 для InfluxDB и позволяет передавать необходимую информацию в полном объёме.

    Инициализация осуществляется примерно так:

    if (metricsConfiguration.enabled) {
    String hostName = InetAddress.getLocalHost().getCanonicalHostName();
    final ScheduledReporter reporterInfluxDB = InfluxdbReporter.forRegistry(metricRegistry)
        .protocol(new HttpInfluxdbProtocol("http", metricsConfiguration.influxdbHost,
            metricsConfiguration.influxdbPort, metricsConfiguration.influxdbUser,
            metricsConfiguration.influxdbPassword, metricsConfiguration.influxdbDB))
        // rate + dim conversions
        .convertRatesTo(TimeUnit.SECONDS).convertDurationsTo(TimeUnit.MILLISECONDS)
        // filter
        .filter(MetricFilter.ALL)
        // don't skip
        .skipIdleMetrics(false)
        // hostname tag
        .tag("host", hostName)
        // !!! converter
        // al metrics must be of form: "processed_links.site_ru .crawling" -> "crawling
        // source=site_ru, param=processed_links value=0.1"
        .transformer(new CategoriesMetricMeasurementTransformer("param", "source"))
        .build();
    reporterInfluxDB.start(metricsConfiguration.reportingPeriod, TimeUnit.SECONDS);
    }
    

    В дальнейшем используется обычный Metrics API, что позволяет повысить прозрачность того что и как делается в моём ПО в любой момент времени.

    Указанная система позволила не только следить за работой системы в рабочем режиме, но и отслеживать изменения, вызванные внесенными изменениями – как увеличилось потребление памяти, скорость обработки, объем данных и т.д. Сейчас, в принципе, страничка с общим дашбоардом является закладкой на кухонном ноутбуке и утренний завтрак всегда сопровождается просмотром событий за последние 9 часов.

    Спасибо за внимание!
    Поделиться публикацией

    Комментарии 6

      0
      InfluxDB для метрик отличное решение. Но если вам нужно метрик больше чем за пару последних недель — вам обязательно нужно использовать Continuous Queries для уплотнения данных. Тогда графики даже за несколько лет будут моментально отображаться.
        0
        Полностью согласен. Continuous Queries вместе с Retention Policy даёт возможность хранить данные за почти произвольный период. Однако нужно решить как долго хранить и что. Для незнакомых с CQ и RP: Разные метрики нужно downsampl’ить по разному – где-то подойдёт среднее, где-то максимум. Я сейчас просто удаляю данные старше 2-х недель без downsamplin’га.
        0

        Почему collectd, а не нативный для стека influx telegraf?

          0
          Ранее был опыт с работы с ним. Однако его поддержка новых продуктов не на высоте, хотя стабильность и низкие требования к ресурсам нареканий не вызывает. По тексту я уже писал, что к Telegraf придётся вернуться.
            +1

            Сорян, не заметил :)


            они достаточно бодро пишут плагины. Правда на паре проектов я держу свои ветки, где делаем свои датчики сбрасывающий информации. Это куда удобнее чем писать на С для collectd. Или запускать питоновские скрипты, через обертку collectd.

              0
              Понял. Как говорится: «хозяйке на заметку».

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое