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

Zabbix 7.4 и Discard unchanged with heartbeat: почему триггер ждёт часы при минутном опросе

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~22 мин чтения
Zabbix 7.4 и Discard unchanged with heartbeat: почему триггер ждёт часы при минутном опросе
Иллюстрация к статье «Zabbix 7.4 и Discard unchanged with heartbeat: почему триггер ждёт часы при минутном опросе».

Вы включили throttling, чтобы история перестала съедать диск. Объём history_uint упал вдвое, все довольны. А через две недели служба записи на видеосервере детского сада падает в 03:12, и письмо приходит в 05:15 — при опросе раз в минуту. Ни агент, ни сеть, ни очередь тут ни при чём. Это ровно то, за что вы заплатили экономией места: отброшенное значение не попадает в базу, а значит, не пересчитывает триггер. Ниже — механика того, что именно ломается (функции по #num, nodata(), trends), разбор реального стенда с таймлайном аварии, правило выбора heartbeat, которым я пользуюсь, и чек-лист по уже настроенным триггерам.

Discard unchanged — это не сжатие истории, это удаление значения

Начну с формулировки, которая объясняет девять из десяти сюрпризов. Discard unchanged и Discard unchanged with heartbeat — не «хранить реже» и не «сжимать». Это удаление значения до того, как сервер вообще узнал, что оно приходило. Шаг отрабатывает в preprocessing manager: если результат совпал с предыдущим, значение исчезает. В базу не пишется, в value cache не кладётся, ни один триггер по нему не пересчитывается.

Мануал 7.4 говорит об этом прямым текстом, без оговорок: если значение отброшено, оно не сохраняется в базе данных и Zabbix server не знает, что это значение было получено; никакие триггерные выражения не будут вычислены, поэтому проблемы по связанным триггерам не будут созданы или закрыты; функции работают только по данным, которые реально сохранены в базе; а так как trends строятся по данным из базы, то если за час не сохранено ни одного значения, за этот час не будет и trends.

Второе предложение важнее первого. Триггер в Zabbix — вещь событийная. Выражение пересчитывается тогда, когда по элементу из этого выражения пришло новое сохранённое значение. Отдельно живут только функции, зависящие от времени, — их дёргает history syncer по таймеру. Причём «зависящие от времени» в терминах документации — это только nodata() и функции даты/времени (now(), time(), dayofweek() и т. п.); min(/host/key,5m) к ним не относится, окно в пять минут не делает триггер таймерным. Всё остальное подчиняется правилу «нет записи — нет пересчёта». Получается, что throttling буквально выключает триггер на всё время, пока метрика не меняется. Что для дискретной метрики вроде «служба запущена / служба остановлена» означает: выключает почти всегда.

Heartbeat-вариант придуман как раз чтобы этого избежать. Параметр задаётся в секундах, поддерживаются положительные целые с минимумом 1 секунда и временные суффиксы вида 30s, 1m, 2h, 1d. Можно подставлять пользовательские макросы и LLD-макросы — это пригодится, я к этому вернусь. Важное ограничение платформы: на один элемент данных допускается только одна опция throttling, скомбинировать «просто Discard unchanged» и heartbeat нельзя.

Проверьте порядок шагов предобработки. Документация не требует ставить throttling последним, но я всегда ставлю его в конец цепочки: если после него идёт JavaScript, Custom multiplier или Regular expression, сравнивается с предыдущим не то значение, которое в итоге ляжет в базу, и поведение становится трудно предсказать.
Памятка: Discard unchanged — это не сжатие истории, это удаление значения — схема
Памятка: Discard unchanged — это не сжатие истории, это удаление значения. Открыть схему в полном размере

Стенд: частный детский сад на 20 рабочих мест

Разберу на живом примере. Условно — частный детский сад «Капитошка»: 20 рабочих мест у администрации, воспитателей и бухгалтерии, 14 IP-камер с записью на видеосервер, пять точек доступа Wi-Fi, два коммутатора, роутер, один сервер под файлы и бухгалтерию и NAS для архива записей. Мониторинг: Zabbix 7.4 на небольшой виртуальной машине с Debian 12 и PostgreSQL 16, без прокси, 46 хостов, около 2 300 элементов данных, средний NVPS в районе 25. Хранение истории выкрутили на 90 дней «чтобы разбираться с инцидентами», и таблицы истории росли примерно на 1,5 ГБ в месяц. Диск виртуалки — 40 ГБ, к августу свободного оставалось 15 %. Классическая ситуация маленькой организации: денег на расширение не выделяют, а данные жалко.

Первое, что сделал приходящий админ до меня: массовым обновлением навесили шаг Discard unchanged на всё, что отдаёт целые числа. Логика понятная — «зачем хранить 1440 одинаковых единиц в сутки». Объём history_uint за две недели упал на 61 %, прирост базы почти остановился. И одновременно случилось два неприятных события подряд.

Первое — в тот же вечер прилетело 46 писем «нет данных от агента». Элемент agent.ping отдаёт единицу всегда, пока агент жив. С Discard unchanged без heartbeat он сохранил ровно одно значение — первое после включения шага — и замолчал навсегда. Триггер nodata(/host/agent.ping,5m)=1 честно сработал через пять минут по всем хостам разом. Откатили шаг на agent.ping, шторм прекратился, все выдохнули и решили, что проблема была локальной.

Второе вылезло через двенадцать дней и было куда неприятнее. На видеосервере в 03:12 упала служба записи. Элемент service.info["VideoRecorderSvc",state] (имя службы условное), интервал опроса 1m, throttling уже был переведён на Discard unchanged with heartbeat со значением 1h — как показалось, «безопасный компромисс». Напомню, что service.info[...,state] возвращает 0 для running и ненулевые коды для остальных состояний (6 — stopped). Триггер был написан ещё при первичной настройке и выглядел так:

min(/sad-srv01/service.info["VideoRecorderSvc",state],#3)>0

Смысл был простой и в общем правильный: «сработать, если три опроса подряд служба не в состоянии running», чтобы не дёргаться на разовый сбой сбора. При минутном опросе это давало реакцию за три минуты. После throttling арифметика поменялась полностью. В 03:12 значение изменилось и легло в базу — одна запись. Две предыдущие сохранённые записи были нулями, записанными по heartbeat в 01:14 и 02:14 (без heartbeat там вообще лежали бы значения недельной давности). Функция min(...,#3) взяла три последние сохранённые записи, среди которых две «нулевые», и вернула 0. Следующий пересчёт триггера возможен только при следующей сохранённой записи — между ними выражение просто не вычисляется. Триггер промолчал. Следующая запись появилась в 04:12 по heartbeat, ещё одна — в 05:12. Только на третьей сохранённой единице окно из трёх значений стало однородным, и проблема наконец открылась. Задержка детекта — два часа вместо трёх минут, и все эти два часа камеры в группах и на входе ничего не записывали.

Если бы включали throttling с нуля на новом стенде — никто бы ничего не заметил, триггеры писались бы уже под новую реальность. Больно именно на существующих инсталляциях, где выражения писались годами и никто не помнит, почему там `#3`.
Zabbix 7.4 и Discard unchanged with heartbeat: почему триггер ждёт часы при минутном опросе — схема
Схема к статье. Открыть схему в полном размере

«Последние три значения» — это больше не три опроса

Вот главное, что нужно унести из статьи. В синтаксисе функций второй параметр может быть либо sec — период вычисления в секундах, либо #num — максимальный диапазон вычисления в последних собранных значениях. Ключевое слово — «собранных». После throttling собранные значения и опросы перестают быть одним и тем же. Пять последних записей у элемента с минутным опросом могут покрывать пять минут, а могут — три месяца. И вы этого не увидите ни в интерфейсе, ни в графике: график просто нарисует прямую линию между двумя точками, как будто данные были.

Разваливается всё семейство функций с #num: min(), max(), avg(), count(), sum(), last(#N), changecount(), nodata тут отдельная песня. Самый коварный случай — счётчики вида «N подряд». Выражение count(/host/key,#5,"eq","1")=5 до throttling означало «пять опросов подряд», после — «пять последних изменений значения», то есть при бинарной метрике фактически «никогда», потому что она чередуется 0 и 1 и пять единиц подряд в сохранённых данных возникнут только по heartbeat.

Функция change() заслуживает отдельного абзаца, потому что на форуме Zabbix регулярно всплывают жалобы на «непонятно почему не восстанавливаются проблемы» именно из связки change() и throttling. По документации change() — это разница между предыдущим и последним значением. Не «между двумя соседними опросами», а между двумя соседними записями в базе. С throttling предыдущая запись может быть недельной давности. Триггер на change() при этом формально работает, но событие «изменилось» и событие «изменилось только что» перестают совпадать, и выражения восстановления, построенные на симметричном change(), начинают вести себя нелогично.

Что я делаю с этим на практике. Правило простое: у элемента с включённым throttling в триггерных выражениях запрещены функции с параметром #num. Совсем. Либо элемент без throttling — тогда #num разрешён, либо throttling — тогда только временные окна и last(). Для того же случая со службой записи видеосервера правильные варианты выглядят так:

Быстрый способ поймать проблему у себя: откройте Latest data по подозрительному элементу и посмотрите реальные метки времени последних значений. Если между соседними записями недели — все ваши `#num`-триггеры по этому элементу давно фиктивные.

nodata(), таймер history syncer и дырки в trends

Про nodata() документация даёт две цифры, которые надо помнить наизусть. Период не должен быть меньше 30 секунд, потому что процесс history syncer вычисляет эту функцию только раз в 30 секунд, а запись nodata(/host/key,0) запрещена вовсе. Это и есть то самое исключение из правила «нет значения — нет пересчёта»: nodata() считается по таймеру, а не по приходу данных. Страница про триггеры формулирует общее правило так: триггер пересчитывается при каждом новом значении элемента из выражения и дополнительно раз в 30 секунд, если в выражении есть nodata() или функции даты и времени. Именно поэтому связка «Discard unchanged без heartbeat» и nodata() даёт гарантированный ложный сигнал — метрика молчит, а таймер тикает.

Отсюда моё жёсткое правило: если по элементу есть триггер с nodata(X), то heartbeat на этом элементе обязан быть не больше половины X. nodata(...,5m) — значит heartbeat 2m или меньше. Не «примерно», а именно с запасом вдвое, потому что момент сохранения по heartbeat привязан не к границам минут, а к моменту последней записи, и на длинных интервалах опроса набегает дрожание. Если запаса нет — лучше вообще снять throttling с этого элемента, он всё равно почти ничего не экономит.

Второй пострадавший — trends. Тут цитата из мануала однозначная: trends строятся по данным в базе, и если за час не сохранено ни одного значения, то и trends за этот час не будет. Последствия видны не сразу, а через месяц-два, когда housekeeper вычистит историю и графики начнут отрисовываться по трендам. В «Капитошке» это выглядело так: график температуры в серверном шкафу за три месяца превратился в пунктир с провалами по 6–8 часов, а виджет со средним значением за 30 дней стал систематически завышать цифру, потому что в среднее попадали только те часы, когда датчик менял показания, то есть часы активности, а спокойные ночные часы просто отсутствовали.

Практический вывод: если по элементу вам нужны trends — а они нужны почти всем метрикам, по которым вы строите отчёты и SLA, — heartbeat должен гарантировать хотя бы одну запись в час. Формально для этого хватает 1h, но 1h — это ровно граница, и при интервалах опроса, не кратных часу, час без данных периодически проскакивает. Я ставлю 30m и не думаю об этом больше.

Не ставьте throttling на `agent.ping` и внутренние элементы `zabbix[...]`. Экономия — сотни килобайт в месяц, а цена ошибки — ложный шторм по всему парку хостов в три часа ночи.
Цифры и версии: nodata(), таймер history syncer и дырки в trends — схема
Цифры и версии: nodata(), таймер history syncer и дырки в trends. Открыть схему в полном размере

Как я выбираю значение heartbeat

У меня это не эвристика «на глаз», а маленький алгоритм, который применяется к каждому элементу перед включением throttling. Берём самое строгое ограничение из тех, что применимы, и его и ставим. Порядок такой: сначала смотрим, есть ли по элементу nodata(); потом — участвует ли элемент в оконных функциях; потом — нужны ли trends; и только если ни одно не сработало, ставим большое значение ради экономии.

Дальше — про то, как это оформить, чтобы не пришлось потом править шаблон в двадцати местах. Значение heartbeat поддерживает пользовательские макросы, и это надо использовать. В шаблоне я задаю макрос уровня шаблона со значением по умолчанию, а на конкретных хостах при необходимости переопределяю. Фрагмент YAML-экспорта шаблона 7.4 выглядит так:

zabbix_export:
  version: '7.4'
  templates:
    - uuid: a1b2c3d4e5f64a7b8c9d0e1f2a3b4c5d
      template: 'Windows services throttling'
      macros:
        - macro: '{$THROTTLE.HB}'
          value: 5m
          description: 'Heartbeat для throttling дискретных метрик'
      items:
        - uuid: 0f1e2d3c4b5a69788796a5b4c3d2e1f0
          name: 'Служба записи видеосервера: состояние'
          key: 'service.info["VideoRecorderSvc",state]'
          delay: 1m
          value_type: UNSIGNED
          preprocessing:
            - type: DISCARD_UNCHANGED_HEARTBEAT
              parameters:
                - '{$THROTTLE.HB}'

Отдельно про интервал опроса. Throttling не отменяет опрос — агент по-прежнему опрашивается раз в минуту, нагрузка на сеть и на pollers остаётся прежней, экономится только запись в базу. Если ваша настоящая цель — снизить нагрузку на сбор, то throttling вам не поможет, надо увеличивать delay или переходить на активные проверки. Я это проговариваю каждому клиенту, потому что путаница между «реже опрашивать» и «реже сохранять» встречается через раз.

Heartbeat 1h выглядит «достаточно частым» на глаз и именно поэтому чаще всего и ставится. При минутном опросе это шестидесятикратное огрубление реакции триггера. Если сомневаетесь — 5m.

Чек-лист для инсталляции, где throttling уже включён

Если вы читаете это уже после включения — порядок действий такой. Сначала инвентаризация: надо понять, на скольких элементах throttling вообще стоит и с какими параметрами. В базе шаги предобработки лежат в таблице item_preproc, коды типов известны из документации API: 19 — Discard unchanged, 20 — Discard unchanged with heartbeat. Запрос на чтение, боевой базе не вредит:

SELECT h.host, i.key_, i.delay,
       CASE p.type WHEN 19 THEN 'discard-unchanged'
                   WHEN 20 THEN 'heartbeat' END AS mode,
       p.params AS hb
FROM item_preproc p
JOIN items i ON i.itemid = p.itemid
JOIN hosts h ON h.hostid = i.hostid
WHERE p.type IN (19, 20)
  AND i.flags <> 2
  AND h.status = 0
ORDER BY mode, h.host, i.key_;

Тем, кто не хочет ходить в базу руками, то же самое достаётся через API. Авторизацию в современных версиях делайте API-токеном в заголовке Authorization: Bearer — передача auth в теле запроса устарела:

curl -s -X POST https://zabbix.example.com/api_jsonrpc.php \
  -H 'Content-Type: application/json-rpc' \
  -H "Authorization: Bearer $ZBX_TOKEN" \
  -d '{"jsonrpc":"2.0","id":1,"method":"item.get","params":{"output":["key_","delay"],"selectHosts":["host"],"selectPreprocessing":"extend","monitored":true}}' \
| jq -r '.result[] | select(any(.preprocessing[]?; .type=="19" or .type=="20")) | [.hosts[0].host, .key_, .delay] | @tsv'

Дальше — сверка с триггерами. Берём полученный список ключей и ищем по триггерным выражениям вхождения этих ключей вместе с #. Каждое совпадение — кандидат на переписывание. Отдельным проходом ищем nodata( и проверяем, что у соответствующих элементов heartbeat вдвое меньше периода. Это ручная работа на пару часов на средней инсталляции, и её нечем заменить: универсального автоматического правила «как переписать выражение» не существует, потому что смысл каждого триггера знает только тот, кто его писал.

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

Что мониторить и когда throttling не нужен вовсе

Раз уж вы полезли в предобработку, заведите заодно наблюдение за ней самой. Внутренний элемент zabbix[preprocessing_queue] показывает количество значений в очереди предобработки — если он устойчиво растёт, у вас не хватает preprocessing workers, и throttling с JavaScript-шагами тут виноваты в первую очередь. Второй полезный элемент — zabbix[wcache,values] с шагом предобработки Change per second: это ваш реальный NVPS, и именно по нему видно, сколько throttling сэкономил на самом деле. Документация прямо рекомендует эту связку.

А теперь честная часть, которую обычно не пишут. Throttling — инструмент для высокочастотного мониторинга. Он придуман для случаев, когда вы снимаете метрики раз в секунду с тысяч устройств, и разница между «писать всё» и «писать изменения» измеряется терабайтами. Если у вас полсотни хостов, как в детском саду с камерами и Wi-Fi, NVPS в пару десятков и база растёт на полтора гигабайта в месяц — throttling вам, скорее всего, вообще не нужен. Дешевле и безопаснее сократить срок хранения истории до 7–14 дней, оставив trends на год, или подключить TimescaleDB со сжатием. Лишние 100 ГБ на виртуалке стоят меньше, чем два часа без записи с камер.

И наоборот, там где риск часто преувеличивают: throttling сам по себе не «портит данные» и не «теряет метрики». График по throttled-элементу рисуется корректно — Zabbix соединяет сохранённые точки, и для ступенчатой метрики это как раз честное отображение. Проблема не в данных, а исключительно в семантике триггерных выражений и в trends. Разберитесь с этими двумя вещами — и throttling будет работать ровно так, как вы от него ждёте.

Мой итоговый рецепт для типичного офиса до 50 рабочих мест: throttling с heartbeat 5m на статусах служб, состоянии дисковых массивов, link status и подобной дискретике; heartbeat 1d на справочных элементах вроде версий и серийников; никакого throttling на agent.ping, внутренних элементах и на всём, где есть nodata(); все триггеры по throttled-элементам — только на last() и временных окнах. Это даёт большую часть экономии и не стоит вам ни одной минуты задержки в детекте реальных аварий.

Если после включения throttling у вас за месяц не сработал ни один триггер, который раньше срабатывал регулярно, — это не признак того, что инфраструктура стала здоровее. Проверьте выражения.

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

Какое значение heartbeat поставить, если разбираться некогда?

5m. Оно безопасно почти для любых триггеров, гарантирует наличие trends и всё равно даёт основную часть экономии на дискретных метриках. Менять на большее имеет смысл только для справочных элементов вроде версий ПО и серийных номеров, где нет ни триггеров, ни отчётов.

Почему после включения throttling посыпались триггеры «нет данных»?

Потому что nodata() вычисляется по таймеру history syncer раз в 30 секунд, независимо от того, приходят значения или нет. Метрика вроде agent.ping всегда равна единице, при Discard unchanged без heartbeat она сохраняется один раз и больше никогда — таймер видит отсутствие данных и открывает проблему. Лечится либо heartbeat не больше половины периода nodata(), либо снятием throttling с таких элементов.

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

Нет. Опрос выполняется с прежней частотой, pollers и сеть нагружены так же. Экономится только запись в базу и пересчёт триггеров. Если цель — снизить нагрузку на сбор, надо увеличивать интервал опроса или переводить проверки в активный режим.

Можно ли комбинировать Discard unchanged и Discard unchanged with heartbeat на одном элементе?

Нет, документация прямо ограничивает: на один элемент данных допускается только одна опция throttling. Отдельно следите за порядком: мануал не требует ставить throttling последним, но если после него идут другие шаги, он сравнивает промежуточное значение, а не то, что реально попадёт в базу, — я всегда ставлю его в конец цепочки.

Как быстро понять, какие триггеры у меня уже сломаны?

Выгрузите список элементов с шагами предобработки типа 19 и 20 (через SQL по item_preproc или через item.get с selectPreprocessing), а затем найдите триггеры, где ключи этих элементов используются вместе с параметром #num. Каждое такое выражение почти наверняка означает не то, что задумывал автор.

Стоит ли вообще включать throttling в небольшой компании?

Чаще всего нет. Throttling создавался для высокочастотного мониторинга с тысячами устройств. В сети детского сада или небольшого офиса с NVPS в пару десятков дешевле сократить срок хранения истории до 7–14 дней, оставив тренды на год, или включить сжатие в TimescaleDB. Диск стоит дешевле, чем задержка обнаружения аварии.

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

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

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

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

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

Источники

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