Ошибка snmp_exporter: metric was collected before with the same name and label values

Ошибка snmp_exporter: metric was collected before with the same name and label valuesМониторинг по SNMP никогда не был тривиальной задачей и дело совсем не в непонятных оидах и громоздких миб-базах. Очень часто конечные устройства выдают противоречивую информацию. Такой случай мне встретился как раз в тот момент, когда я активно тестировал snmp_exporter.

Ошибка snmp_exporter

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

Вводные данные

Если вы только осваиваете snmp_exporter 1, то скажу пару моментов. Во-первых, основной конфиг для него готовите не вы, а отдельное приложение – generator 2. А вот подготовить конфиг для генератора уже ваша задача. Во-вторых, вам обязательно потребуются официальные MIB-базы, без них генератор работать не будет.

Примечание: в статье не будет пошагового гайда по настройке snmp_exporter. Я сконцентрируюсь на кейсе, решение которого потребовало от меня некоторого времени.

Исходный generator.yml:

Секция конфигурации Prometheus для сбора данных с экспортера:

Ничего не предвещало проблем, но если зайти в веб-интерфейс PrometheusTargets, то видна ошибка:

Ни о чем интересном она нам не говорит. Есть некоторые намеки на истечение таймаута 3 и это может нас натолкнуть на мысли о проблемах с доступом. Это действительно могло быть причиной, но не в моем случае.

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

Диагностика

Busybox container image

Проверить как стягиваются данные по SNMP мы можем утилитами из пакета net-snmp, например с помощью snmpwalk. Идеально это сделать изнутри контейнера. К сожалению, обычно контейнеры содержат базовый минимум утилит и среди них уж точно не окажется net-snmp, а значит её надо поставить вручную. Но тут есть один момент – образ snmp_exporter собирается из образа busybox, в котором отсутствует менеджер пакетов, что практически исключает возможность нормальной установки нужных пакетов для диагностики. А вот что говорят мейнтейнеры на этот счет 4:

we won’t add a package manager unless BusyBox upstream does, and they don’t seem particularly likely to any time soon

Нужные утилиты можно собрать из исходников, но опять же надо доставить как минимум gcc и ещё кучу всего (и это надо будет делать каждый раз после рестарта контейнера, либо собирайте свой образ). Итого имеем – тупиковый busybox, traceroute, nc, telnet и ping, но и этого может быть достаточно.

Пингом проверяем доступность хоста и сразу корректность разрешения имени:

Проверим порт телнетом, скажете вы, но нет. Телнет сработает для TCP, а с SNMP у нас UDP и потому ответа не получим.

На помощь придет netcat (он есть в бизибоксе по умолчанию):

Если порт будет закрыт, то трассировкой проверяем маршрут и сразу забираемся на промежуточные устройства и смотрим разрешен ли UDP 161.

Даже с таким скромным набором удалось что-то выяснить, но проблему так и не устранили, а только убедились, что порты открыты и затык не в сети. Тем более странным в этом случае выглядит возврат таймаута на стороне Prometheus.

Net-snmp

Из контейнера snmp_exporter нам ничего не светит, но можно попробовать поднять рядом другой и доставить нужные утилиты (либо сделайте это сразу на хосте):

Запускаем команду:

Примечание: если вдруг у вас используется SNMPv3, то команда будет сложнее, например:
# snmpwalk -v3 -l authPriv -u monitor -a SHA -A <authentication_passphrase> -x DES -X <privacy_passphrase> ups-01.domain.local

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

Curl

Ну и последний вариант, с которого на самом деле нужно было начинать диагностику, это curl. Да, мы можем стукнуться экспортеру напрямую и посмотреть что он нам возвращает, благо ошибка в Prometheus уже говорит нам какой урл запрашивается:

Если увидите вот такую ошибку, то проблема в кредах:

Но я увидел вот это (были десятки подобных строк):

Странно, что везде встречается upsPhaseOutputPhaseIndex. Этот оид хранит номер фазы 5 (The output phase identifier), если фаз больше 1. Посмотрим какие значения он возвращает (помните, выше мы ставили net-snmp? Сейчас нам это пригодится):

Вернул номера всех трех фаз. Теперь, чтобы получить значение любой из трех фаз, нужно просто добавить её порядковый номер к оиду. Например оид первой фазы по идее должен быть 1.3.6.1.4.1.318.1.1.1.9.3.3.1.2.1, но что мы видим в выводе snmpwalk – к оидам добавляются ещё две промежуточные единицы – SNMPv2-SMI::enterprises.318.1.1.1.9.3.3.1.2.1.1.1. Давайте попробуем опросить оиды, добавляя единицу за единицей:

И ещё:

Устройство почему-то упорно возвращает полный список номеров фаз, хотя делать это не должно и только добавив ещё одну единицу в конце, получаем искомое значение индекса первой фазы:

То есть нужное значение пряталось на два уровня глубже, чем это описано в MIB-базе. Snmp_exporter к такому поведению не готов, ему нужно строгое соответствие базе и потому он выкидывает ошибку. В корне проблемы разобрались, но что делать для её исправления?

Итог

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

Примечание: правильнее сказать, что экспортеру нужно уникальное сочетание имени метрики и набора лейблов, но имя метрики по сути и так является лейблом. Обратиться к нему можно по имени __name__. Почитать больше об этом лейбле можно в статье What’s in a __name__?.

Самое время заняться устранением проблемы.

Устранение проблемы

Для начала обратим внимание на родительский оид у upsPhaseOutputPhaseIndex – это upsPhaseOutputPhaseEntry 6, у которого 21 дочерний оид. Ситуация с добавлением .1.1 присутствует у всех 21 и озвученные ниже варианты решения проблемы необходимо применить для каждого из них.

Начнем с более правильных вариантов решения проблемы.

Обращение к вендору

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

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

Обращение к разработчикам snmp_exporter

Ещё один вариант – официально обратиться к разработчикам snmp_exporter, а конкретнее generator, что я и сделал 7. На мой взгляд было бы полезно иметь функцию ручного переписывания оидов внутри каких-либо модулей generator.yml, но разработчики думают иначе:

If you want a different oid tree, provide a different MIB.

С одной стороны это звучит логично, но тем, кто работал с SNMP, известно, что на свете миллионы устройств, поведение которых отличается от эталонного. В пользу этого свидетельствует и тот факт, что в функционал generator добавлена возможность маппинга значений определенных оидов.

Поскольку с обращением к вендору нас скорее всего отправят подальше, остается только пара вариантов и оба из них не такие красивые с точки зрения “ручного” вмешательства.

Изменение snmp.yml

Первый вариант – подкорректировать оиды прямо в snmp.yml, который нам выдает generator. Ищем upsPhaseOutputPhaseIndex и меняем значение в поле oid (можно это сделать ещё и в поле help и возможно указать свои комментарии, чтобы потом знать где вы вносили изменения):

Примечание: обратите внимание, что generator принимает файл, который может (и обычно будет) содержать конфигурацию для нескольких устройств и подменять оиды вероятно нужно не для всех из них (у каких-то устройств может наблюдаться баг с оидами, а у каких-то нет)

Разумеется лучше автоматизировать этот ручной труд. Например sed умеет изменять содержимое файла на месте.

Изменение MIB

Второй вариант – изменить сразу базу MIB, которую вы скачали с официального сайта производителя ваших устройств.

Есть некоторые особенности работы с базой – она не содержит полностью числовых значений оидов, а также всякий последующий оид образуется путем соединения имени предыдущего с соответствующем индексом дочернего. Например имеем upsPhaseOutputPhaseIndex с родительским оидом upsPhaseOutputPhaseEntry, вот что получаем:

Чтобы пофиксить нужный оид, просто добавляем наши две единицы в самый конец и получаем:

Примечание: скачать mib-файл нужно с официального сайта, а открыть сможете простым текстовым редактором 8.

После этого вы можете смело использовать generator, подсунув ему исправленную базу и на выходе получите корректный snmp.yml.

Notes:

  1. snmp_exporter
  2. generator
  3. context deadline exceeded #1438
  4. Package manager? #45
  5. upsPhaseOutputPhaseIndex
  6. upsPhaseOutputPhaseEntry
  7. OID rewriting in generator.yml #486
  8. APC Powernet MIB
Яндекс.Метрика