АйТи Фреш
Главная / Статьи / DevOps и автоматизация
DevOps и автоматизация

Вложенное обнаружение в Zabbix 7.4: базы и их tablespace из одного JSON, с сохранением связи метрик

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~21 мин чтения
Вложенное обнаружение в Zabbix 7.4: базы и их tablespace из одного JSON, с сохранением связи метрик
Иллюстрация к статье «Вложенное обнаружение в Zabbix 7.4: базы и их tablespace из одного JSON, с сохранением связи метрик».

Ночью прилетает «tablespace ts2 заполнен на 94 %». Инженер лезет в первую попавшуюся базу, а горит третья — потому что в ключе метрики нет имени базы. Это не редкость и не экзотика: любое вложенное API отдаёт дочерние объекты с одинаковыми именами, и родительский контекст в стандартном LLD теряется на раз. В Zabbix 7.4 появились discovery prototypes — вложенные правила обнаружения, которые берут тот же JSON, что и родитель, и умеют строить ключи из идентификаторов обоих уровней. Ниже — как я это собираю руками, какой конфиг реально работает, где оно ломается из раза в раз и что делать в первую очередь.

Метрика уехала не в ту базу — механика провала

Возьмите любой источник данных, который отдаёт дерево: кластеры → базы, гипервизоры → виртуалки, шасси → блейды, storage pool → тома. У дочерних объектов имена почти всегда переиспользуются. В двух кластерах PostgreSQL под 1С у вас две базы postgres, две template1 и, если повезло с фантазией предыдущего админа, две buh. У каждой базы — свои табличные пространства ts1, ts2, ts3. Никакой уникальности на нижнем уровне нет и не будет.

Дальше начинается арифметика Zabbix. Ключ элемента данных должен быть уникален в пределах хоста — это жёсткое ограничение, не рекомендация. Если прототип элемента написан как db.ts.size[{#TSNAME}], то первое обнаруженное табличное пространство ts1 займёт ключ, а остальные ts1 из соседних баз просто не создадутся. Правило обнаружения загорится красным, в колонке Info появится классика жанра: «Cannot create item: item with the same key ... already exists». Это ещё хороший сценарий — вы хотя бы видите ошибку.

Плохой сценарий — когда уникальность формально есть, но она держится на порядке элементов в массиве или на индексе. Данные приехали в другом порядке, СУБД переименовали, добавили базу в середину списка — и метрика, которая вчера показывала db1, сегодня показывает db3. Триггер сработал, дежурный пошёл разбирать не тот объект, потерял двадцать минут. Ошибочная привязка метрик не просто мусорит в мониторинге — она уводит инженера от реальной проблемы.

До 7.4 это лечили обходом. Прототипов правил обнаружения не существовало вовсе, поэтому второй уровень приходилось делать через прототип хоста (каждая база — отдельный хост) либо через отдельное правило, которое повторно ходило в API за данными по каждой базе. Шесть баз — шесть обращений к источнику каждые пять минут. На боевом сервере СУБД это заметно.

Если у вас сейчас в шаблоне есть прототип элемента, в ключе которого только один LLD-макрос, а сущность в источнике — вложенная, считайте, что бомба уже заложена. Проверьте колонку Info у правил обнаружения на всех хостах, а не только на том, где «всё работает».

Discovery prototypes: что именно появилось в 7.4

Zabbix 7.4 (стандартный релиз, по странице Life Cycle — 1 июля 2025 года) принёс discovery prototypes — прототипы правил обнаружения внутри родительского правила. В What's new 7.4 это описано прямо: вложенные правила обнаружения внутри «родительского» правила, у каждого уровня — собственные прототипы элементов данных, триггеров, графиков, хостов и — да — собственные прототипы правил обнаружения. Документация отдельно оговаривает: «The levels of nesting for discovery prototypes are unlimited». То есть цепочка «инстанс → база → табличное пространство → сегмент» строится штатно, без прототипов хостов и без костылей.

Ключевая вещь для нашей боли — тип Nested (в объекте API discoveryruleprototype это type: 23, интервал delay для него не задаётся). Правило такого типа не ходит никуда за данными: по документации оно формируется из JSON-объекта того же значения, которое получило родительское правило, применяет к нему свою предобработку и вытаскивает нужный «срез». Активируется оно одновременно с родительским правилом. Никакого второго обращения к API, никакого второго подключения к СУБД. Это ровно то, чего не хватало в предыдущих версиях.

Второй ключевой момент: макросы родительского LLD доступны дочернему правилу. {#DB}, объявленный на родителе, виден и в ключе вложенного правила, и в прототипах элементов внутри него. Именно это позволяет собрать ключ из идентификаторов обоих уровней и закрыть проблему коллизий раз и навсегда.

Пример из документации (раздел Discovery prototypes, случай 1) выглядит так — родительское правило получает массив баз, у каждой внутри массив табличных пространств:

[
  {
    "database": "db1",
    "created_at": "2024-02-01T12:30:00Z",
    "encoding": "UTF8",
    "tablespaces": [
      { "name": "ts1", "max_size": "10GB" },
      { "name": "ts2", "max_size": "20GB" },
      { "name": "ts3", "max_size": "15GB" }
    ]
  },
  {
    "database": "db2",
    "created_at": "2023-11-15T08:45:00Z",
    "encoding": "UTF16",
    "tablespaces": [
      { "name": "ts1", "max_size": "5GB" },
      { "name": "ts2", "max_size": "25GB" },
      { "name": "ts3", "max_size": "30GB" }
    ]
  }
]

Конфигурация раскладывается на четыре шага. На родительском правиле во вкладке LLD macros объявляем {#DB} = $.database (в документации там же создаётся прототип элемента db.connections[{#DB}]). Внутри правила создаём прототип правила обнаружения: имя Discover tablespaces for {#DB}, тип Nested, ключ с макросом родителя, например db.tablespace.discovery[{#DB}] (API требует хотя бы один LLD-макрос в ключе прототипа), шаг предобработки JSONPath = $.tablespaces, и уже на его вкладке LLD macros — {#TSNAME} = $.name. Внутри вложенного правила прототип элемента получает ключ db.ts.size[{#DB},{#TSNAME}]. На выходе — db.ts.size[db1,ts1], db.ts.size[db1,ts2], db.ts.size[db2,ts1] и так далее, каждая метрика намертво привязана к своей паре.

Обратите внимание: `{#DB}` стоит не только в ключе элемента, но и в ключе самого вложенного правила — `db.tablespace.discovery[{#DB}]`. Правила обнаружения тоже элементы данных, их ключи тоже уникальны в пределах хоста. Забудете макрос здесь — второе правило не создастся и вы получите tablespace только от первой базы.
Вложенное обнаружение в Zabbix 7.4: базы и их tablespace из одного JSON, с сохранением связи метрик — схема
Схема к статье. Открыть схему в полном размере

Разбор со стенда: два кластера PostgreSQL под 1С в рекламном агентстве

Собирали это в рекламном агентстве «Медиа Сигнал» на 16 рабочих мест. Бухгалтерия, зарплата и учёт проектов клиентов живут в 1С на PostgreSQL: один сервер СУБД, два кластера — боевой на порту 5432 и тестовый на 5433, куда подрядчик по 1С регулярно разворачивает копии перед обновлениями. Всего 11 баз, и имена между кластерами совпадают: unf, buh, zup есть и в бою, и в тесте. Старый шаблон был написан по принципу «одно правило на кластер»: два LLD-правила, два отдельных вызова скрипта, каждые пять минут два подключения к СУБД, а при появлении третьего кластера под архив — ручное клонирование правила. Плюс регулярные вопросы «алерт про базу unf, а какая именно unf — боевая или тестовая?».

Переделал так. Один мастер-элемент типа Script на хосте pg-01.example.local, интервал 5 минут, отдаёт единый JSON: массив кластеров, внутри каждого — массив баз с размером, числом соединений и возрастом самой старой транзакции. Родительское LLD-правило — зависимый элемент от мастера, макрос {#INST} = $.instance. Внутри него прототип правила типа Nested с ключом pg.db.discovery[{#INST}], предобработка $.databases, макрос {#DBNAME} = $.name. Прототипы элементов внутри — тоже зависимые от того же мастера, с ключами вида pg.db.size[{#INST},{#DBNAME}].

[
  {
    "instance": "prod-5432",
    "version": "16.4",
    "databases": [
      { "name": "unf",  "size": 412536729600, "conn": 63, "xact_age_s": 12 },
      { "name": "buh",  "size": 88214732800,  "conn": 11, "xact_age_s": 3  }
    ]
  },
  {
    "instance": "test-5433",
    "version": "16.4",
    "databases": [
      { "name": "unf",  "size": 39822131200,  "conn": 2,  "xact_age_s": 0  }
    ]
  }
]

Итог по цифрам. Было: 2 правила обнаружения, 2 запуска скрипта, 2 подключения к СУБД каждые 5 минут, и каждый новый кластер означал копирование набора прототипов. Стало: 1 мастер-элемент, 1 родительское правило, 1 прототип вложенного правила, 4 прототипа элементов — на выходе 44 элемента данных по 11 базам, все зависимые, все с уникальными и читаемыми ключами. Подключений от мониторинга к серверу СУБД стало вдвое меньше просто потому, что опрос теперь один. Когда подрядчик поднял третий кластер под архив, шаблон трогать не пришлось: скрипт увидел новый порт — Zabbix создал ветку.

Побочный эффект, ради которого всё и затевалось: имя триггера стало PostgreSQL {#INST}: база {#DBNAME} выросла на 20% за сутки. Дежурный видит test-5433 / unf и не бежит на боевой. Среднее время до понимания «где горит» на этом классе инцидентов упало с пятнадцати минут до пары минут — и, что важнее для агентства без штатного админа, бухгалтер перестал получать тревожные письма про тестовую копию.

Не делайте вложенное правило самостоятельным (с типом Script, HTTP agent и т.п.), если данные уже есть в родительском JSON. Тип Nested существует именно для того, чтобы не ходить в источник повторно — это его главная ценность, а не удобство интерфейса.
Цифры и версии: Разбор со стенда: два кластера PostgreSQL под 1С в рекламном агентстве — схема
Цифры и версии: Разбор со стенда: два кластера PostgreSQL под 1С в рекламном агентстве. Открыть схему в полном размере

Правило двух идентификаторов и правильные JSONPath

Формулирую правило, которое стоит повесить на стену: в ключ сущности любого уровня входят идентификаторы всех уровней выше. Не «для красоты», а потому что иначе Zabbix физически не сможет их различить. Это касается ключа вложенного правила обнаружения, ключа прототипа элемента, а в человекочитаемом виде — имени элемента, имени триггера и имени графика. Экономия символов здесь ничего не даёт, а стоит потом дорого.

Второе место, где всё разъезжается, — предобработка зависимых элементов внутри вложенного правила. Элемент зависит от общего мастера, значит на входе он получает весь исходный JSON целиком и должен сам найти в нём свою ветку. Фильтр обязан отработать по обоим макросам:

JSONPath: $[?(@.instance == "{#INST}")].databases[?(@.name == "{#DBNAME}")].size.first()

Здесь легко ошибиться с кавычками, и ошибка молчаливая — элемент просто становится неподдерживаемым с ошибкой разбора JSONPath. Документация Zabbix по экранированию LLD-макросов в JSONPath говорит вещь конкретную: при подстановке значения макроса экранируются только обратный слэш и двойная кавычка. Поэтому макрос в выражении фильтра всегда берётся в двойные кавычки — в нотации документации $.[@.value == "{#MACRO}"], а не $.[@.value == {#MACRO}]. А если макрос используется как элемент пути, его надо оборачивать в квадратные скобки и двойные кавычки: $.["{#MACRO}"].value, а не $.{#MACRO}.value.

Отдельно про предобработку самого вложенного правила. $.tablespaces в примере документации работает потому, что вложенное правило формируется из JSON-объекта родительского уровня — той самой базы, — а не из всего исходного массива. Это интуитивно не очевидно: люди пишут туда $[*].tablespaces или $..tablespaces по привычке и получают либо пустоту, либо все табличные пространства всех баз в каждой ветке. Если сомневаетесь — начните с шага JSONPath $ и посмотрите в тесте, что именно приезжает.

Макрос в JSONPath-выражении — только в двойных кавычках. Экранируются лишь `\` и `"`; всё остальное (пробелы, точки, скобки в именах баз) поедет как есть и разломает выражение, если кавычек нет.

Пять мест, где вложенный LLD ломается из раза в раз

Первое и самое частое: макрос родительского уровня не объявлен. Люди читают «макросы родителя доступны дочернему правилу» и думают, что доступны любые поля родительского JSON. Нет. Доступен тот макрос, который родительское правило реально создало — либо потому что источник отдал его в формате LLD, либо потому что вы прописали его на вкладке LLD macros родителя. Если {#DB} нигде не объявлен, он не резолвится и уезжает в ключ буквально, строкой {#DB}. Выглядит это как «правило отработало, а элементы странные».

Второе: фильтры. На практике фильтр на родительском правиле отсекает не только сам объект, но и всё его поддерево: объект не обнаружен — значит, и вложенное правило для него не создано. Это логичное поведение, но о нём забывают и потом ищут, «почему у одной базы нет tablespace». Фильтры на вложенном правиле работают только внутри своей ветки. Проверять надо оба уровня.

Третье: жизненный цикл потерянных объектов. Настройки «Delete lost resources» и «Disable lost resources» (они есть с 7.0) задаются в каждом правиле отдельно, в том числе во вложенном. Пропала база из выдачи — вместе с ней по цепочке уходят в «потерянные» её табличные пространства, их элементы, триггеры и история. Я на боевых контурах никогда не ставлю немедленное удаление: недельный «Delete lost resources: after 7d» плюс «Disable lost resources: immediately» даёт время заметить, что вы просто сломали скрипт-мастер, а не что базу удалили. Автоматически отключённые объекты, кстати, включатся сами при повторном обнаружении, а отключённые руками — нет.

Четвёртое: связка с прототипами хостов. Обнаруженные хосты и раньше умели через шаблоны обнаруживать другие хосты, а в 7.4 документация discovery prototypes отдельно оговаривает: если на обнаруженном хосте есть правило типа Nested, то JSON-объект, из которого этот хост был создан, отправляется всем nested-правилам на этом хосте, а макросы правила, создавшего хост, доступны вложенным правилам. Это мощно (гипервизор → ВМ → контейнеры), но легко словить дубли, если один и тот же шаблон навешан и на родителя, и на потомка. Пятое — отладка: вложенное правило само за данными не ходит, поэтому проверять его в отрыве от родителя бессмысленно. Я тестирую предобработку родительского правила, копирую один объект из результата и уже на нём прогоняю JSONPath вложенного уровня.

Диагностика начинается не с логов, а с колонки Info в Data collection → Hosts → Discovery. Красный треугольник там содержит точный текст ошибки LLD. Логи нужны, только когда правило зелёное, а объектов нет.
Памятка: Пять мест, где вложенный LLD ломается из раза в раз — схема
Памятка: Пять мест, где вложенный LLD ломается из раза в раз. Открыть схему в полном размере

Отладка, автоматизация и версии: на чём это строить в 2026

Когда Info пустой, а объекты не появляются, я поднимаю уровень логирования точечно, по типу процесса, а не глобально в конфиге. Синтаксис runtime control из документации:

# поднять детализацию только у LLD-воркеров
zabbix_server -R log_level_increase="lld worker"
tail -f /var/log/zabbix/zabbix_server.log | grep -iE 'lld|discovery'
# вернуть обратно, не забудьте — на большом сервере лог растёт быстро
zabbix_server -R log_level_decrease="lld worker"

Конфигурацию я держу в git в виде YAML-экспорта шаблона, а массовые правки делаю через API. Прототипы правил обнаружения получили собственные методы, и это заметно упрощает жизнь тем, кто раскатывает один шаблон на десятки хостов. Тип 23 в примере ниже — это как раз Nested, поэтому delay не передаётся; обязательны ruleid родительского правила, hostid, name, key_ с LLD-макросом и type. Идентификаторы в примере условные:

{
  "jsonrpc": "2.0",
  "method": "discoveryruleprototype.create",
  "params": {
    "name": "Discover tablespaces for {#DB}",
    "key_": "db.tablespace.discovery[{#DB}]",
    "hostid": "10084",
    "ruleid": "47251",
    "type": 23
  },
  "id": 1
}

Теперь неприятная часть, о которой надо говорить честно. Zabbix 7.4 — не LTS, а стандартный релиз. По официальной политике жизненного цикла полная поддержка 7.4 длится до выхода 8.0 LTS, а ограниченная (только критические исправления) заканчивается в IV квартале 2026 года. На 7.0 LTS вложенного обнаружения нет вообще, никакими настройками его туда не принести. Версия 8.0 LTS по той же странице заявлена на III квартал 2026 года с полной поддержкой до III квартала 2029-го и ограниченной — до III квартала 2031-го; на момент написания в документации уже есть ветка 8.0. Отсюда моя позиция: новые стенды с вложенным LLD имеет смысл планировать сразу под 8.0 LTS, а тем, кто уже сидит на 7.4, — заложить апгрейд до конца года, иначе в 2027 году вы окажетесь на версии без обновлений безопасности.

Спорный момент, и я его не буду сглаживать: свежий LTS в первые месяцы — всё ещё свежий LTS. Боевые контуры клиентов я обычно перевожу не раньше третьего-четвёртого минорного релиза, то есть реалистично — зима. Пока 7.4 в ограниченной поддержке, сидеть на ней нормально, но это уже отсчёт последних месяцев. Единственное, чего делать не стоит, — строить новую большую схему мониторинга на 7.4 в расчёте «поживёт года три».

Не ставьте DebugLevel=4 глобально на боевом сервере Zabbix с сотнями хостов ради одного правила обнаружения. Рантайм-управление уровнем логирования по конкретному типу процессов делает ровно то, что нужно, и не топит диск.

Что делать первым, а на что можно забить

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

Приоритет номер два — один мастер-элемент на дерево. Если ваш скрипт или HTTP-агент дёргает источник по разу на каждый объект верхнего уровня, вы платите за это подключениями к боевой СУБД или запросами к API гипервизора. Схлопнуть всё в один JSON и раздать зависимыми элементами — самая большая по эффекту переделка, и вложенный LLD её просто делает возможной. Приоритет номер три — фильтры и настройки потерянных объектов: сначала «disable immediately», удаление — с задержкой в неделю.

На что можно забить с чистой совестью. Не гонитесь за третьим и четвёртым уровнем вложенности только потому, что документация говорит «уровни не ограничены» — реальная польза почти всегда исчерпывается двумя, максимум тремя. Не переписывайте работающие шаблоны из коробки Zabbix: если типовой шаблон PostgreSQL или MySQL вас устраивает и не создаёт нагрузки, вложенный LLD там не нужен. И не начинайте с графиков и дашбордов по вложенным сущностям — сначала пусть метрики стабильно приезжают на правильные объекты, красота потом.

Резюмируя по существу вопроса: базы и их tablespace из одного JSON в Zabbix 7.4 обнаруживаются штатно — родительское правило плюс прототип правила типа Nested с предобработкой $.tablespaces. Связь с нужной базой держится ровно на одном: родительский макрос присутствует в ключе вложенного правила и в ключах всех его прототипов. Всё остальное — детали реализации.

Если после переделки правило обнаружения зелёное, а элементов нет — в 9 случаях из 10 виноват JSONPath в предобработке вложенного правила: он написан под весь массив, а на вход приходит один объект.
Порядок действий: Что делать первым, а на что можно забить — схема
Порядок действий: Что делать первым, а на что можно забить. Открыть схему в полном размере

Частые вопросы

Обязательно ли вставлять родительский макрос в ключ самого вложенного правила обнаружения?

Да. Правило обнаружения — это тоже элемент данных, и его ключ должен быть уникален в пределах хоста. Если ключ вложенного правила написан как db.tablespace.discovery без параметров, оно создастся ровно один раз — для первой обнаруженной базы, а для остальных Zabbix сообщит о дубликате ключа. Правильный вариант — db.tablespace.discovery[{#DB}].

Делает ли вложенное правило повторный запрос к API или к базе?

Нет, если у него тип Nested. Такое правило работает с JSON-объектом, который уже получило родительское правило, применяет к нему свою предобработку и активируется одновременно с родителем; интервал опроса у него не задаётся. Именно поэтому переход на вложенный LLD обычно снижает нагрузку на источник данных: вместо N обращений остаётся одно.

Почему во вложенном правиле не резолвится макрос {#DB} и он попадает в ключ как есть?

Потому что родительское правило его не создало. Доступны только те макросы, которые родитель реально сформировал: либо источник отдал данные в LLD-формате, либо макрос прописан на вкладке LLD macros родительского правила с JSONPath вида $.database. Проверьте вкладку LLD macros на родителе — в 90 % случаев дело в ней.

Можно ли получить вложенное обнаружение на Zabbix 7.0 LTS?

Нет. Discovery prototypes появились в 7.4 и никакими настройками в 7.0 не включаются. Обходной путь на 7.0 — прототипы хостов (каждый объект верхнего уровня становится отдельным хостом со своим правилом обнаружения) либо второе независимое правило с повторным запросом к источнику. Оба варианта работают, но стоят дороже в поддержке.

Сколько уровней вложенности имеет смысл делать?

Формально ограничения нет, практически — два, максимум три. Каждый уровень добавляет макросы в ключи, усложняет отладку и увеличивает число объектов на хосте. Если вам кажется, что нужен четвёртый уровень, чаще всего проблема в структуре данных источника, а не в возможностях Zabbix.

Что выбрать в 2026 году — 7.4 или ждать 8.0 LTS?

Если стенд новый и рассчитан надолго — планируйте сразу под 8.0 LTS: по политике жизненного цикла полная поддержка 7.4 заканчивается с выходом 8.0 LTS, а ограниченная — в IV квартале 2026 года. Если вы уже на 7.4 и всё работает, закладывайте апгрейд в план до конца года. Переезжать на свежий LTS в первые месяцы после релиза я боевым контурам обычно не советую — жду нескольких минорных релизов.

Столкнулись с похожей задачей? Обращайтесь — решим

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

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 💬 MAX: +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи