Мониторинг по SNMP никогда не был тривиальной задачей и дело совсем не в непонятных оидах и громоздких миб-базах. Очень часто конечные устройства выдают противоречивую информацию. Такой случай мне встретился как раз в тот момент, когда я активно тестировал snmp_exporter.
Содержание
Ошибка snmp_exporter
В статье я хочу привести не только варианты решения проблемы, но и рассказать о том пути, который я проделал, диагностируя проблему.
Вводные данные
Если вы только осваиваете snmp_exporter 1, то скажу пару моментов. Во-первых, основной конфиг для него готовите не вы, а отдельное приложение – generator 2. А вот подготовить конфиг для генератора уже ваша задача. Во-вторых, вам обязательно потребуются официальные MIB-базы, без них генератор работать не будет.
Исходный generator.yml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
modules: apc: version: 1 max_repetitions: 25 retries: 3 timeout: 10s auth: username: public walk: - sysUpTime - interfaces - 1.3.6.1.4.1.318.1.1.1.2 # upsBattery - 1.3.6.1.4.1.318.1.1.1.3 # upsInput - 1.3.6.1.4.1.318.1.1.1.4 # upsOutput - 1.3.6.1.4.1.318.1.1.1.7.2 # upsAdvTest - 1.3.6.1.4.1.318.1.1.1.8.1 # upsCommStatus - 1.3.6.1.4.1.318.1.1.1.9.3 # upsPhaseOutput - 1.3.6.1.4.1.318.1.1.1.12 # upsOutletGroups - 1.3.6.1.4.1.318.1.1.10.2.3.2 # iemStatusProbesTable - 1.3.6.1.4.1.318.1.1.26.8.3 # rPDU2BankStatusTable lookups: - source_indexes: [upsOutletGroupStatusIndex] lookup: upsOutletGroupStatusName drop_source_indexes: true - source_indexes: [iemStatusProbeIndex] lookup: iemStatusProbeName drop_source_indexes: true overrides: ifType: type: EnumAsInfo rPDU2BankStatusLoadState: type: EnumAsStateSet upsAdvBatteryCondition: type: EnumAsStateSet upsAdvBatteryChargingCurrentRestricted: type: EnumAsStateSet upsAdvBatteryChargerStatus: type: EnumAsStateSet |
Секция конфигурации Prometheus для сбора данных с экспортера:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
- job_name: snmp scrape_interval: 2m static_configs: - targets: - ups-01.domain.local metrics_path: /snmp params: module: [apc] relabel_configs: - source_labels: [__address__] target_label: __param_target - source_labels: [__param_target] target_label: instance - target_label: __address__ replacement: 127.0.0.1:9116 |
Ничего не предвещало проблем, но если зайти в веб-интерфейс Prometheus – Targets, то видна ошибка:
1 |
Get http://127.0.0.1:9116/snmp?module=apc&target=ups-01.domain.local: context deadline exceeded |
Ни о чем интересном она нам не говорит. Есть некоторые намеки на истечение таймаута 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, но и этого может быть достаточно.
Пингом проверяем доступность хоста и сразу корректность разрешения имени:
1 2 3 4 5 6 7 8 9 |
# ping ups-01.domain.local PING ups-01.domain.local (192.168.24.71): 56 data bytes 64 bytes from 192.168.24.71: seq=0 ttl=251 time=1.465 ms 64 bytes from 192.168.24.71: seq=1 ttl=251 time=1.461 ms 64 bytes from 192.168.24.71: seq=2 ttl=251 time=1.472 ms ^C --- ups-01.domain.local ping statistics --- 3 packets transmitted, 3 packets received, 0% packet loss round-trip min/avg/max = 1.461/1.466/1.472 ms |
Проверим порт телнетом, скажете вы, но нет. Телнет сработает для TCP, а с SNMP у нас UDP и потому ответа не получим.
На помощь придет netcat (он есть в бизибоксе по умолчанию):
1 2 |
# nc -vz -u ups-01.domain.local 161 ups-01.domain.local (192.168.24.71:161) open |
Если порт будет закрыт, то трассировкой проверяем маршрут и сразу забираемся на промежуточные устройства и смотрим разрешен ли UDP 161.
Даже с таким скромным набором удалось что-то выяснить, но проблему так и не устранили, а только убедились, что порты открыты и затык не в сети. Тем более странным в этом случае выглядит возврат таймаута на стороне Prometheus.
Net-snmp
Из контейнера snmp_exporter нам ничего не светит, но можно попробовать поднять рядом другой и доставить нужные утилиты (либо сделайте это сразу на хосте):
1 |
yum -y install net-snmp net-snmp-utils net-snmp-libs |
Запускаем команду:
1 |
snmpwalk -v2c -c public ups-01.domain.local |
# snmpwalk -v3 -l authPriv -u monitor -a SHA -A <authentication_passphrase> -x DES -X <privacy_passphrase> ups-01.domain.local
В ответ вы должны получить огромный список оидов с их значениями. После этих манипуляция я убедился, что snmpwalk данные возвращает.
Curl
Ну и последний вариант, с которого на самом деле нужно было начинать диагностику, это curl. Да, мы можем стукнуться экспортеру напрямую и посмотреть что он нам возвращает, благо ошибка в Prometheus уже говорит нам какой урл запрашивается:
1 |
curl -sw '' 'http://127.0.0.1:9116/snmp?module=apc&target=ups-01.domain.local' |
Если увидите вот такую ошибку, то проблема в кредах:
1 2 |
An error has occurred while serving metrics: error collecting metric Desc{fqName: "snmp_error", help: "Error scraping target", constLabels: {}, variableLabels: []}: error getting target ups-01.domain.local: Request timeout (after 3 retries) |
Но я увидел вот это (были десятки подобных строк):
1 2 3 4 |
An error has occurred while serving metrics: ... * collected metric "upsPhaseOutputPhaseIndex" { label:<name:"upsPhaseOutputPhaseIndex" value:"1" > label:<name:"upsPhaseOutputPhaseTableIndex" value:"1" > gauge:<value:2 > } was collected before with the same name and label values * collected metric "upsPhaseOutputPhaseIndex" { label:<name:"upsPhaseOutputPhaseIndex" value:"1" > label:<name:"upsPhaseOutputPhaseTableIndex" value:"1" > gauge:<value:3 > } was collected before with the same name and label values |
Странно, что везде встречается upsPhaseOutputPhaseIndex. Этот оид хранит номер фазы 5 (The output phase identifier), если фаз больше 1. Посмотрим какие значения он возвращает (помните, выше мы ставили net-snmp? Сейчас нам это пригодится):
1 2 3 4 |
# snmpwalk -v2c -c public ups-01.domain.local 1.3.6.1.4.1.318.1.1.1.9.3.3.1.2 SNMPv2-SMI::enterprises.318.1.1.1.9.3.3.1.2.1.1.1 = INTEGER: 1 SNMPv2-SMI::enterprises.318.1.1.1.9.3.3.1.2.1.1.2 = INTEGER: 2 SNMPv2-SMI::enterprises.318.1.1.1.9.3.3.1.2.1.1.3 = INTEGER: 3 |
Вернул номера всех трех фаз. Теперь, чтобы получить значение любой из трех фаз, нужно просто добавить её порядковый номер к оиду. Например оид первой фазы по идее должен быть 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. Давайте попробуем опросить оиды, добавляя единицу за единицей:
1 2 3 4 |
# snmpwalk -v2c -c public ups-01.domain.local 1.3.6.1.4.1.318.1.1.1.9.3.3.1.2.1 SNMPv2-SMI::enterprises.318.1.1.1.9.3.3.1.2.1.1.1 = INTEGER: 1 SNMPv2-SMI::enterprises.318.1.1.1.9.3.3.1.2.1.1.2 = INTEGER: 2 SNMPv2-SMI::enterprises.318.1.1.1.9.3.3.1.2.1.1.3 = INTEGER: 3 |
И ещё:
1 2 3 4 |
# snmpwalk -v2c -c public ups-01.domain.local 1.3.6.1.4.1.318.1.1.1.9.3.3.1.2.1.1 SNMPv2-SMI::enterprises.318.1.1.1.9.3.3.1.2.1.1.1 = INTEGER: 1 SNMPv2-SMI::enterprises.318.1.1.1.9.3.3.1.2.1.1.2 = INTEGER: 2 SNMPv2-SMI::enterprises.318.1.1.1.9.3.3.1.2.1.1.3 = INTEGER: 3 |
Устройство почему-то упорно возвращает полный список номеров фаз, хотя делать это не должно и только добавив ещё одну единицу в конце, получаем искомое значение индекса первой фазы:
1 2 |
# snmpwalk -v2c -c public ups-01.domain.local 1.3.6.1.4.1.318.1.1.1.9.3.3.1.2.1.1.1 SNMPv2-SMI::enterprises.318.1.1.1.9.3.3.1.2.1.1.1 = INTEGER: 1 |
То есть нужное значение пряталось на два уровня глубже, чем это описано в MIB-базе. Snmp_exporter к такому поведению не готов, ему нужно строгое соответствие базе и потому он выкидывает ошибку. В корне проблемы разобрались, но что делать для её исправления?
Итог
Пора сделать вывод о том, что же все-таки произошло. Дело в том, что экспортер ожидает получить уникальные наборы лейблов и их значений, но в нашем случае ему это не удавалось, потому что на запрос конкретного OID экспортер получал от устройства несколько записей.
Самое время заняться устранением проблемы.
Устранение проблемы
Для начала обратим внимание на родительский оид у 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 и возможно указать свои комментарии, чтобы потом знать где вы вносили изменения):
1 2 3 4 5 6 7 8 9 10 11 |
<...> - name: upsPhaseOutputPhaseIndex oid: 1.3.6.1.4.1.318.1.1.1.9.3.3.1.2.1.1 type: gauge help: The output phase identifier. - 1.3.6.1.4.1.318.1.1.1.9.3.3.1.2(added .1.1) indexes: - labelname: upsPhaseOutputPhaseTableIndex type: gauge - labelname: upsPhaseOutputPhaseIndex type: gauge <...> |
Разумеется лучше автоматизировать этот ручной труд. Например sed умеет изменять содержимое файла на месте.
Изменение MIB
Второй вариант – изменить сразу базу MIB, которую вы скачали с официального сайта производителя ваших устройств.
Есть некоторые особенности работы с базой – она не содержит полностью числовых значений оидов, а также всякий последующий оид образуется путем соединения имени предыдущего с соответствующем индексом дочернего. Например имеем upsPhaseOutputPhaseIndex с родительским оидом upsPhaseOutputPhaseEntry, вот что получаем:
1 2 3 4 5 6 7 8 9 |
<...> upsPhaseOutputPhaseIndex OBJECT-TYPE SYNTAX INTEGER ACCESS read-only STATUS mandatory DESCRIPTION "The output phase identifier." ::= { upsPhaseOutputPhaseEntry 2 } <...> |
Чтобы пофиксить нужный оид, просто добавляем наши две единицы в самый конец и получаем:
1 2 3 |
<...> ::= { upsPhaseOutputPhaseEntry 2 1 1 } <...> |
После этого вы можете смело использовать generator, подсунув ему исправленную базу и на выходе получите корректный snmp.yml.
Notes:
- snmp_exporter ↩
- generator ↩
- context deadline exceeded #1438 ↩
- Package manager? #45 ↩
- upsPhaseOutputPhaseIndex ↩
- upsPhaseOutputPhaseEntry ↩
- OID rewriting in generator.yml #486 ↩
- APC Powernet MIB ↩