Pull to refresh

Собираем свои счетчики через collectd протокол

Reading time 4 min
Views 4.2K
Приветствую!

Думаете как собирать счетчики со своих собственных сервисов?
Запарились парсить логи?
Постоянно забываете настроить сбор счетчиков для нового или переехавшего в другое место сервиса?



Тогда welcome!

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

Конечно же разработчики не скупятся на логи и раньше мне приходилось долго и мучительно парсить гигабайты этого добра, чтобы построить какой-нибудь график.

С этим можно жить, но жизнь можно сделать лучше, особенно когда вы имеете доступ к коду сервиса. Тогда можно научить сервис самостоятельно отсылать данные о себе на сервис сбора данных — collectd

На хабре уже был обзор архитектуры этого сервиса, поэтому подробно останавливаться на этом не будем.

Суть collectd:
  • Это демон, который слушает один порт
  • Принимает udp-пакеты (см. протокол)
  • Извлеченные из пакетов данные записывает в rrd базу

Осталось научить свои сервисы формировать и отправлять такие udp-пакеты и графики почти готовы.

Конфигурация collectd

Сначала нужно научить collectd понимать ваши собственные счетчики.

Правим /etc/collectd.conf и добавляем описание своих типов:

TypesDB "/usr/lib/collectd/types.db" "/etc/collectd/customtypes.db"

Пример №1. Данные будут сохранены в отдельных rrd-файлах

http_server_avg_item_process_time value:GAUGE:0:U
http_server_items_processed value:GAUGE:0:U


Пример №2. Данные будут сохранены в одном rrd-файле

process_memory working_set:GAUGE:0:U, peak_working_set:GAUGE:0:U

Разница в том, что для первого примера придется отправлять 2 udp-пакета, а для второго можно послать один пакет с двумя значениями.

Перезапускаем collectd и он готов к приему ваших данных.

Binary protocol на питоне

На самом деле реализовать binary protocol достаточно просто на любом языке, на раз уж блог про питон, то будет на питоне :)

NB: Я использую стандартную версию collectd демона из пакета debian 5 — 4.4.2. Она уже довольно старая, но вроде binary protocol там не менялся, поэтому думается, что версия особой роли не играет.

Дефолтная реализация в принципе работает, но она лишена возможности отправки множества значений в одном пакете.

Если выкинуть из дефолтной реализации почти все лишнее, и допилить отправку множественных значений, то можно получить, например, такой код:
import struct
import time
import socket

SEND_INTERVAL = 10      # seconds
MAX_PACKET_SIZE = 1024  # bytes

TYPE_NAME = "gauge"

TYPE_HOST            = 0x0000
TYPE_TIME            = 0x0001
TYPE_PLUGIN          = 0x0002
TYPE_PLUGIN_INSTANCE = 0x0003
TYPE_TYPE            = 0x0004
TYPE_TYPE_INSTANCE   = 0x0005
TYPE_VALUES          = 0x0006
TYPE_INTERVAL        = 0x0007
LONG_INT_CODES = [TYPE_TIME, TYPE_INTERVAL]
STRING_CODES = [TYPE_HOST, TYPE_PLUGIN, TYPE_PLUGIN_INSTANCE, TYPE_TYPE, TYPE_TYPE_INSTANCE]

VALUE_COUNTER  = 0
VALUE_GAUGE    = 1
VALUE_DERIVE   = 2
VALUE_ABSOLUTE = 3
VALUE_CODES = {
    VALUE_COUNTER:  "!Q",
    VALUE_GAUGE:    "<d",
    VALUE_DERIVE:   "!q",
    VALUE_ABSOLUTE: "!Q"
}

def pack_numeric(type_code, number):
    return struct.pack("!HHq", type_code, 12, number)

def pack_string(type_code, string):
    return struct.pack("!HH", type_code, 5 + len(string)) + string + "\0"

def pack(typeId, value):
    if typeId in LONG_INT_CODES:
        return pack_numeric(typeId, value)
    elif typeId in STRING_CODES:
        return pack_string(typeId, value)
    else:
        raise AssertionError("invalid type code " + str(id))

def pack_counters(counters):
        length = 6 + len(counters)*9
        result = []
        result.append(struct.pack("!HHH", TYPE_VALUES, length, len(counters)))
        for value in counters:
            result.append(struct.pack("<B", VALUE_GAUGE)) # this code allow to send only gauge value
        for value in counters:
            result.append(struct.pack("<d", value))
        return result

def message_start(when=None, host=socket.gethostname(), plugin_inst="", plugin_name="any", value_type=TYPE_NAME):
    return "".join([
        pack(TYPE_HOST, host),
        pack(TYPE_TIME, when or time.time()),
        pack(TYPE_PLUGIN, plugin_name),
        pack(TYPE_PLUGIN_INSTANCE, plugin_inst),
        pack(TYPE_TYPE, value_type),
        pack(TYPE_TYPE, value_type),
        pack(TYPE_INTERVAL, SEND_INTERVAL)
    ])

def create_message(counters, when=None, host=socket.gethostname(), plugin_inst="", plugin_name="any", type_name=TYPE_NAME):
    message = [message_start(when, host, plugin_inst, plugin_name, type_name)]
    parts = pack_counters(counters)
    message.extend(parts)
    return "".join(message)

Пример формирования udp-пакета для отправки с двумя счетчиками

create_message([working_set_value, peak_working_set_value], plugin_name='service_name', type_name='process_memory')

При получении такого пакета collectd создаст или добавит в файл ваши данные примерно по такому вот пути:
/var/lib/collectd/rrd/hostname/service_name/process_memory.rrd

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

Строим графики


Collectd — это сборщик данных и ничего более. Для того, чтобы построить график ему нужна веб-морда.
Идеальный партнер для collectd, которого мне удалось найти — drraw.
Это веб-морда для rrdtool и ничего более.

Главная фишка которая понравилась лично мне и которой нет у остальных веб-морд — это гибкая настройка графиков по регулярным выражениям. Drraw автоматически найдет все хосты/сервисы/etc и объединит их на одном графике.

Скриншот настройки графика (частично)



Пример графика



UPD Небольшой багфикс в коде. Порядок отсылаемых значений должен совпадать с тем как они определены в customtypes.db
Tags:
Hubs:
+13
Comments 27
Comments Comments 27

Articles