С чего началась реконструкция
В одном из наших внедрений я разбирал ситуацию, когда на панели оставалось благополучное значение, а Linux-служба уже не выполняла свою работу. Имя заказчика и детали контура здесь не нужны: важна сама диагностическая ошибка. Последнее число в истории Zabbix выглядело нормальным, поэтому первым соблазном было считать unit исправным. Я начал не с перезапуска, а с разделения двух вопросов: что сейчас думает systemd о процессе и когда агент действительно получил это знание. На хосте я последовательно выполнил systemctl is-active для быстрого ответа, systemctl is-failed для проверки зафиксированного отказа и systemctl show для полноценного снимка свойств. Такое разделение оказалось принципиальным. Команда может вернуть состояние, но старое значение в базе мониторинга лишь сообщает, что когда-то сбор прошёл успешно. Поэтому рядом с состоянием unit я всегда рассматриваю время последней проверки, возраст значения и ошибку выполнения элемента. Если свежего результата нет, зелёный предыдущий результат нельзя называть чистым или здоровым состоянием. Он становится неизвестностью, которую должен отражать отдельный триггер свежести или ошибки сбора. Именно это различие позволило восстановить последовательность события без опасного воздействия на службу.
Как я читаю состояние systemd
Для меня ActiveState и SubState образуют основную пару, но не исчерпывают диагноз. ActiveState показывает крупное состояние менеджера, например active, inactive, activating или failed, а SubState уточняет текущую фазу конкретного типа unit. Я не превращаю любое отличие от running в аварию: у oneshot-службы нормальным может быть exited, а ожидаемая остановка может дать clean inactive. Затем я смотрю Result, потому что он сохраняет смысл последнего завершения, и сопоставляю ExecMainCode с ExecMainStatus: первый объясняет природу завершения основного процесса, второй содержит его статус или сигнал. MainPID равный нулю означает отсутствие известного основного процесса, но трактуется только вместе с типом службы и остальными полями. NRestarts помогает увидеть автоматические перезапуски: текущий active ещё не доказывает, что перед ним не было падения. Эти восемь фактов — ActiveState, SubState, Result, ExecMainCode, ExecMainStatus, MainPID, NRestarts и время получения снимка — я храню как конкретные данные, а не как расплывчатый флаг «всё хорошо». Дополнительно записываю boot ID. После загрузки системы одинаковые показания относятся уже к другой временной линии, и без идентификатора загрузки легко ошибочно связать старую запись журнала с новым процессом.
Ограниченная временная ось журнала
После снимка свойств я обращаюсь к journald, но никогда не начинаю с бесконечного вывода всего журнала. Команда journalctl --unit=example-test.service --since '-15 minutes' задаёт конкретный unit и узкое окно, привязанное к наблюдаемому событию; реальную границу я выбираю по времени последней успешной проверки и моменту тревоги, а не считаю пятнадцать минут универсальным правилом. В выводе ищу сообщения самого процесса, записи systemd о запуске и завершении, явные timeout и error. stdout и stderr полезны как свидетельства приложения, однако сами по себе не равны состоянию unit: программа может написать слово error и продолжить работу либо завершиться без понятной строки. Поэтому журнал подтверждает временную последовательность, а свойства systemd дают машинное состояние. Я сохраняю timestamp каждой существенной записи и сверяю boot ID, часовой пояс хоста и формат вывода. Это защищает от склейки соседних загрузок и от сдвига времени при удалённом разборе. Для safety журнал читается с --no-pager, без follow, с ограниченным интервалом и ожидаемым лимитом выполнения команды. Таймаут сборщика трактуется как ошибка получения доказательств, а не как доказанный отказ или доказанное здоровье службы.
Безопасный UserParameter
В Zabbix Agent я применяю отдельный UserParameter для заранее выбранной службы и фиксированной команды чтения. Ключ не принимает имя unit из элемента данных и не содержит wildcard. Это сознательное ограничение: wildcard вместе с shell и недоверенной макроподстановкой способен превратить мониторинг в запуск произвольных аргументов. Строку команды я проверяю с абсолютными путями там, где это требует локальная политика, запрещаю перенаправления в изменяемые файлы и не добавляю управляющие systemctl start, stop или restart. AllowKey и DenyKey рассматриваю как часть одной модели доступа. Недостаточно добавить разрешение и забыть об остальных совпадениях: я проверяю, какой шаблон фактически разрешает ключ, и исключаю широкие правила, открывающие семейство опасных команд. Предпочтителен непривилегированный read-only вызов systemctl show. Если дистрибутив или политика доступа мешают чтению, запасной вариант — limited sudoers: только конкретный пользователь агента, один абсолютный исполняемый файл и точные неизменяемые аргументы. Разрешение sudo на весь systemctl, journalctl, оболочку или произвольное имя unit я отвергаю, поскольку оно расширяет наблюдение до управления системой или чтения чужих журналов. Ошибка, пустой вывод и таймаут возвращаются как отдельный неуспех сбора, а не подменяются нулём.
Как значения становятся сигналами
Полученный снимок я разбираю на проверяемые условия. ActiveState=failed является прямым фактом; связка active с ожидаемым SubState подтверждает лишь состояние менеджера. Ненулевой ExecMainStatus, необычный ExecMainCode и Result, отличный от success, объясняют последнее завершение, но могут относиться к уже восстановившемуся процессу. MainPID позволяет заметить исчезновение или замену основного процесса, а рост NRestarts — цикл автоматического восстановления. Отдельными сигналами служат новый boot ID, поздний timestamp журнала, сообщение о timeout, запись из stderr, код возврата команды агента и возраст последнего значения. Так получается больше восьми предметных признаков, которые можно сопоставить, а не один универсальный текстовый статус. Я обязательно развожу три исхода: подтверждённый отказ unit, подтверждённое свежее нормальное состояние и отсутствие свежих данных. При ошибке элемента или превышении допустимой для этого контура давности срабатывает наблюдение за сбором. Оно не должно утверждать, что unit failed. И наоборот, свежий ActiveState=active не отменяет расследование, если NRestarts изменился или Result хранит след недавнего завершения. Порог давности и частоту опроса выбираю по критичности службы, длительности допустимого разрыва и нагрузке, без фиксированного универсального интервала.
Когда уместно обнаружение служб
Я не запускаю низкоуровневое обнаружение по всему списку systemd ради удобства. Blanket discovery всех services приносит динамические, transient, пользовательские и вспомогательные unit, создаёт шум и неожиданно расширяет поверхность команд. Для одиночной важной службы проще и прозрачнее завести фиксированные элементы. LLD оправдано, когда в контуре действительно есть несколько однородных именованных unit с общей семантикой: например, заранее определённые экземпляры одного шаблонного приложения. Тогда источник обнаружения возвращает только разрешённый перечень имён, а прототипы используют строгую проверку формата и не передают недоверенную макроподстановку в shell. Я заранее определяю, какие ActiveState и SubState для этой группы считаются нормальными, потому что одинаковое правило для разных Type будет неверным. Из LLD также исключаю unit, чья краткая жизнь является штатной. Такой подход сохраняет смысл тревоги: оператор видит отказ известного компонента, а не изменение случайного объекта операционной системы. Если перечень мал и меняется редко, явная конфигурация предпочтительнее автоматизации. Это немного увеличивает редакторскую работу в шаблоне, зато делает права UserParameter, ожидаемые состояния, зависимости и процедуру проверки обозримыми.
Контролируемая проверка цепочки
Перед включением тревог в рабочем контуре я проверяю всю цепочку только на изолированном тестовом service. Сначала фиксирую systemctl show, текущий MainPID, NRestarts, timestamp и boot ID, затем убеждаюсь, что агент возвращает свежий разбор без ошибки. Контролируемый stop допустим лишь для этого тестового unit. После него ожидаю согласованный набор доказательств: is-active перестаёт сообщать active, is-failed соответствует реальной семантике остановки, MainPID исчезает, а journalctl --unit с ограниченным --since показывает остановку в нужной временной оси. Важно не требовать failed от штатного stop, если unit корректно переходит в inactive с успешным Result. Затем выполняется recovery через start тестовой службы. Ожидаю active и правильный SubState, новый MainPID, релевантные записи запуска, свежий успешный результат UserParameter и закрытие тревоги по данным новой проверки. Если настроена политика Restart, отдельный безопасный тест сбоя согласуется заранее, поскольку он отличается от штатной остановки и должен проявить Result, коды завершения и NRestarts. Боевой unit я не останавливаю для доказательства мониторинга. Там использую уже имеющиеся события, чтение свойств и журнал, сохраняя границу между наблюдением и управлением.
Рабочий разбор, откат и границы
Мой read-only runbook начинается с точного имени unit и команд systemctl is-active, systemctl is-failed и systemctl show с перечисленными свойствами. Потом я читаю journalctl --unit для ограниченного --since, сверяю список загрузок и /proc/sys/kernel/random/boot_id, после чего локально тестирую именно настроенный ключ через zabbix_agent2 -t либо zabbix_agentd -t. Фиктивный ключ и agent.ping здесь бесполезны: они не проверяют команду получения состояния. Если проблема появилась после настройки, rollback ограничен удалением конкретного UserParameter, связанного узкого AllowKey или точечной записи sudoers и возвратом элементов шаблона к предыдущему варианту. Наблюдаемую службу этот откат не трогает. Я учитываю и пределы метода. systemd сообщает состояние unit, но не гарантирует корректный прикладной ответ, доступность зависимости или целостность данных. Особенности forking, oneshot, notify и socket activation меняют смысл PID и под-состояния. Journald может быть ротирован, непостоянен либо недоступен пользователю агента. Короткий сбой может попасть между опросами, а автоматический restart — скрыть его текущим active. Поэтому для критичных служб этот слой дополняется проверкой прикладного ответа, зависимостей и отдельным контролем свежести, но не заменяется широкими обещаниями о полной доступности.
Сигналы, которые сопоставляю
- ActiveState
- SubState
- Result
- ExecMainCode
- ExecMainStatus
- MainPID
- NRestarts
- timestamp проверки
- boot ID
- код возврата команды сбора
- возраст последнего значения
- записи journald в ограниченном окне
Отвергнутые альтернативы
- unit действительно failed
- unit clean и healthy, но значение устарело
- агент не смог выполнить разрешённую команду
- служба перезапускается и краткий отказ скрыт текущим active
- проверяется неверный unit или данные относятся к предыдущей загрузке
Конфигурация и сбор
Фиксированный пользовательский ключ примера определяется точной строкой: `UserParameter=itfresh.systemd.example_test,/usr/bin/systemctl show example-test.service --property=ActiveState --property=SubState --property=Result --property=ExecMainCode --property=ExecMainStatus --property=MainPID --property=NRestarts`. Этот пример развёртывается только после того, как на проверке конфигурации имя `example-test.service` заменено на одобренный фиксированный unit; пользовательские аргументы и макросы не допускаются. Точечная allowlist-запись `AllowKey=itfresh.systemd.example_test` применяется вместе с ограничительной политикой для остальных UserParameter, причём итоговую доступность ключей проверяют штатной проверкой конфигурации агента без утверждений о небезопасном порядке правил. Широкие wildcard, shell, недоверенные макросы и широкий sudo для systemctl или journalctl отвергаются.
Контролируемая проверка и свидетельство
Тест выполняется только на изолированном тестовом service: фиксируются исходные systemctl show и boot ID, затем выполняется контролируемый stop, ожидаются inactive/failed согласно поведению unit, изменение MainPID и релевантная запись journald; после start ожидаются active, рабочий SubState, новый MainPID и свежая успешная проверка Zabbix. Боевые службы тестом не останавливаются.
Фрагмент рабочего регламента
Только чтение: `systemctl is-active example-test.service`; `systemctl is-failed example-test.service`; `systemctl show example-test.service --property=ActiveState,SubState,Result,ExecMainCode,ExecMainStatus,MainPID,NRestarts`; `journalctl --unit=example-test.service --since '-15 minutes' --no-pager --output=short-iso`; `journalctl --list-boots --no-pager`; `cat /proc/sys/kernel/random/boot_id`; `zabbix_agent2 -t 'itfresh.systemd.example_test'` (или `zabbix_agentd -t 'itfresh.systemd.example_test'` для установленного варианта агента). Это точный ключ, определённый выше, а не встроенный ключ Zabbix; имя фиксированного unit предварительно сверяется с одобренной конфигурацией, команды stop/start в этот read-only runbook не входят.
Откат
Удалить добавленную строку UserParameter и только связанную с ней точечную запись AllowKey либо sudoers, проверить конфигурацию агента штатной проверкой, затем перезапустить только агент по принятой процедуре. Элементы и триггеры Zabbix вернуть к предыдущей версии шаблона; состояние наблюдаемой службы rollback не изменяет.
Ограничения вывода
Снимок свойств systemd не доказывает прикладную доступность порта или корректность ответа сервиса. oneshot, notify, forking, socket-activated и пользовательские unit имеют разную семантику. Журнал может быть непостоянным, ротированным или ограниченным правами. Result и коды процесса требуют сопоставления с Type, Restart и историей текущей загрузки.
Источники
- https://www.freedesktop.org/software/systemd/man/latest/systemctl.html
- https://www.zabbix.com/documentation/current/en/manual/config/items/userparameters

