Погодин Евгений — ИТ Заметки
📈Мониторинг Обновлено 10.11.2026

Из практики админа 03. Бэкап жив? Мониторинг свежести резервных копий

Note

Материал сверен с официальной документацией (Zabbix, Microsoft Learn; ссылки — в конце статьи). Статья основана на реальном случае, описанном обезличенно: названия, адреса и значения изменены. Примеры вывода — иллюстрации. Перед работой на боевых системах проверьте шаги на тестовой машине.

Мониторинг показывал зелёное. Служба резервного копирования запущена, на дисках с копиями есть место, узлы отвечают. Но «служба работает» и «копия создана» — разные факты. Задание могло быть отключено, падать при каждом запуске или просто не стартовать, а служба всё это время честно работала. Узнать об этом можно было бы в тот день, когда копия понадобится.

Мы дополнили Zabbix проверками не процесса, а результата: когда в последний раз появилась свежая успешная копия. Для файловых и скриптовых копий это время изменения файла-отметки, для SQL Server — история резервного копирования в системной базе msdb. Ниже — как это устроено, как выбирать пороги и в какую ловушку легко попасть при переносе готового скрипта.

В примерах Zabbix-сервер — lnx1, файловый сервер с копиями — FS1, Windows-сервер с базами — SRV1 (допустим, что на нём стоит SQL Server).

Что нужно знать заранее

  • Узел, элемент данных (item), триггер, действие и оповещение — как в статье Zabbix с нуля: установка, первые узлы и уведомления. Примеры рассчитаны на Zabbix 7.0.
  • В пассивной проверке сервер Zabbix опрашивает агента, в активной агент сам берёт у сервера список проверок и присылает значения; для активных нужны параметры ServerActive и Hostname, совпадающий с именем узла в Zabbix.
  • UserParameter — строка в конфигурации агента: она связывает ключ элемента данных с командой, которую выполнит агент.
  • msdb — системная база SQL Server. В ней лежат история резервного копирования (таблица backupset — по строке на копию) и задания агента SQL Server (службы, запускающей задания по расписанию): таблицы sysjobs и sysjobhistory.
  • Что такое правило 3-2-1 и зачем проверять восстановление: Резервное копирование: правило 3-2-1 и Windows Server с нуля 11.
  • Для шагов с SQL Server нужны права администратора на тестовом экземпляре: мы создаём там учётную запись.

Симптомы

Строго говоря, симптомов не было: в этом и состоит проблема. Мониторинг следил за службами копирования и за остатком места на дисках с копиями, и оба показателя оставались в норме. При этом возможны такие «тихие» сбои:

  • задание копирования отключено или удалено;
  • задание запускается и падает на первом шаге;
  • скрипт копирования завершается ошибкой, а служба-планировщик исправна;
  • на целевом диске достаточно места, но новые копии на него не пишутся.

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

Как искали причину: что именно измерять

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

Что проверяем Что это доказывает Чего не доказывает
Служба копирования запущена процесс жив что задание вообще выполняется
Остаток места на целевом диске место ещё есть что на диск что-то пишут
Время изменения файла-отметки скрипт дошёл до конца (если отметку пишет только успешный прогон) что копия цела и восстановится
msdb.dbo.backupset завершена копия такой-то базы такого-то типа что файл не повреждён
msdb.dbo.sysjobhistory задание запускалось и закончилось успехом или ошибкой ничего, если задание не запускалось

Таблица backupset особенно удобна. Набор резервных копий (backup set) описан как результат одной успешной операции, поэтому строка появляется после копии, закончившейся успешно, а упавшие прогоны в ней, как правило, отсутствуют. Исключения есть в той же таблице: копии, сделанные с продолжением после ошибок, помечает столбец is_damaged, а разовые копии COPY_ONLY — столбец is_copy_only. Если отфильтровать такие строки, от таблицы можно считать «время с последней успешной копии» и не разбирать сообщения об ошибках.

Что не помогло

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

Ожидание сигнала «задание упало». Он не поймает отключённое, удалённое или не стартующее задание: строки об ошибке просто нет, ведь sysjobhistory хранит данные о выполнениях. Метрика упавших прогонов полезна как пояснение к проблеме, но не как основной сигнал.

Время изменения лога. Для существующих скриптов, которые уже писали лог, мы использовали время его изменения. Оно доказывает успех, только если лог обновляется лишь при успешном завершении; если скрипт дописывает лог и при ошибке, нужна отдельная отметка. Поэтому для нового бэкапа скрипт после успеха пишет файл-отметку (практика автора).

Готовый скрипт «как есть» на другом сервере. Это главная ловушка. Механизм копирования бывает разным: задания с «понятными» именами, план обслуживания (Maintenance Plan), в котором задание называется иначе, ежедневные полные копии вообще без копий журнала транзакций. Если скопировать готовый скрипт на другой сервер, не проверив, как копии делаются там, метрика «упавшие задания» всегда будет нулевой: скрипт ищет задания по имени, которого на этом сервере нет, а нулевое значение выглядит как «всё в порядке». Перед использованием проверьте реальный механизм:

-- Какие задания есть и включены ли они
SELECT j.name, j.enabled FROM msdb.dbo.sysjobs AS j ORDER BY j.name;

-- Куда реально пишутся последние копии
SELECT TOP (10) bs.database_name, bs.type, bs.backup_finish_date,
       bmf.physical_device_name
FROM msdb.dbo.backupset AS bs
JOIN msdb.dbo.backupmediafamily AS bmf ON bmf.media_set_id = bs.media_set_id
ORDER BY bs.backup_finish_date DESC;

Путь к файлу копии хранится в backupmediafamily.physical_device_name, связь с backupset идёт по media_set_id. Если пути ведут не туда, куда вы ожидаете, или копии делает сторонняя программа, метрики придётся строить иначе.

Что сработало

Шаг 1. Файловые и скриптовые копии: время изменения отметки

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

# В конце скрипта копирования (пример)
if ($LASTEXITCODE -eq 0) {
    Set-Content -Path C:\BackupMarks\fs1-daily.ok -Value (Get-Date -Format s)
}

Каталог C:\BackupMarks создайте заранее и разрешите запись только SYSTEM и Administrators (командой icacls, как для каталога скриптов в шаге 2): иначе любой пользователь сможет «освежить» отметку вручную. Агент читает файл от имени своей учётной записи; не указывайте сетевые пути: на Windows элемент vfs.file.time проходит по UNC-путям, и документация называет это возможным риском безопасности.

Создайте элемент данных на узле FS1. В примере это тип Zabbix agent (пассивный), тип информации — число без знака, интервал — час. Единицы unixtime покажут дату вместо Unix-метки (числа секунд с 1 января 1970 года).

vfs.file.time[C:\BackupMarks\fs1-daily.ok,modify]

Элемент возвращает время последнего изменения содержимого. Теперь два триггера. Первый — «отметка не обновлялась более 30 часов», серьёзность High:

fuzzytime(/FS1/vfs.file.time[C:\BackupMarks\fs1-daily.ok,modify],30h)=0

Функция fuzzytime возвращает 1, если значение элемента отличается от времени сервера Zabbix не больше чем на заданный срок, и 0 в противном случае. В её описании речь идёт о пассивной проверке агента: для system.localtime прямо требуется пассивный тип (на агенте 2 допускается и активный). Ключ vfs.file.time[...,modify] назван там прямым сценарием применения: проверить, что файл давно не обновлялся. Для активного режима этого ключа документация ничего не описывает, поэтому в примере элемент пассивный. Тридцать часов — сутки плюс запас на задержку ночного прогона.

Второй триггер нужен на случай, когда файла нет или агент не может его прочитать. Тогда значение получить не удаётся, элемент становится неподдерживаемым, а выражение с fuzzytime перестаёт вычисляться: Zabbix требует, чтобы элемент был в поддерживаемом состоянии. Исключение — функция nodata, она считается и для неподдерживаемых элементов:

nodata(/FS1/vfs.file.time[C:\BackupMarks\fs1-daily.ok,modify],3h)=1

Проверяйте не на боевой отметке, иначе сработает настоящая авария и уйдёт оповещение. Создайте отдельный файл fs1-test.ok, временные элемент и триггер с теми же выражениями (без действия оповещения), сдвиньте время файла назад, дождитесь опроса и верните:

$f = Get-Item C:\BackupMarks\fs1-test.ok
$f.LastWriteTime = (Get-Date).AddHours(-31)
# После срабатывания тестового триггера и опроса — откат:
$f.LastWriteTime = Get-Date

Шаг 2. SQL Server: три метрики из msdb

Агент отдаёт числа, которые считает скрипт PowerShell. Нам нужны три метрики (имена ключа и метрик условные), и они дополняют друг друга:

  • hours_since_last — сколько часов прошло с последней успешной полной копии любой пользовательской базы; если копий нет вообще, скрипт возвращает 9999. Это ответ на вопрос «работает ли механизм хоть где-то»;
  • unprotected — сколько баз в состоянии ONLINE не имеют полной копии за последние 26 часов. Это ответ на вопрос «кого пропустили»: одна исправно копируемая база скрывает остальные в первой метрике;
  • failed_jobs24 — сколько прогонов заданий копирования закончились ошибкой за сутки. Это пояснение к проблеме, а не основной сигнал.

Warning

Ниже создаётся учётная запись SQL Server и файл с её паролем: это изменение защиты сервера. Делайте его в согласованное окно работ. Режим проверки подлинности SQL Server должен допускать логины SQL: режим сервера обязан соответствовать типу логина (так сказано в описании CREATE LOGIN). Если сервер принимает только проверку подлинности Windows, переключать режим ради мониторинга можно лишь по решению владельца сервера. Откат — в разделе «Откат» ниже.

У нас служба агента работала от LocalSystem (встроенная учётная запись службы Windows с широкими локальными правами), и для неё в SQL Server не было логина. Поэтому в примере SQL-аутентификация (логин и пароль внутри SQL Server, в отличие от проверки подлинности Windows) к localhost. Права минимальные: чтение трёх таблиц msdb. Пароль — заглушка, свой задайте генератором паролей без символов ;, ' и ": они ломают строку подключения.

-- Под администратором SQL Server
CREATE LOGIN zbx_backup_mon
  WITH PASSWORD = N'<ПАРОЛЬ>', CHECK_POLICY = ON, DEFAULT_DATABASE = msdb;
USE msdb;
CREATE USER zbx_backup_mon FOR LOGIN zbx_backup_mon;
GRANT SELECT ON dbo.backupset TO zbx_backup_mon;
GRANT SELECT ON dbo.sysjobs TO zbx_backup_mon;
GRANT SELECT ON dbo.sysjobhistory TO zbx_backup_mon;

По умолчанию у нового логина включена политика паролей Windows (CHECK_POLICY = ON), а срок действия пароля не проверяется (CHECK_EXPIRATION = OFF). Видимость строк в sys.databases зависит от прав, поэтому ниже результат под этим логином сверяется с результатом под администратором. Достаточность прав проверьте на тестовом экземпляре.

Строку подключения храним в файле рядом со скриптом и закрываем права. Идентификаторы S-1-5-18 и S-1-5-32-544 — это SYSTEM и группа Administrators; они не зависят от языка системы, поэтому в командах стоят они. Параметр /inheritance:r убирает унаследованные права, а оба идентификатора перечислены явно. Выполняйте шаг в консоли «От имени администратора»: в обычной после /inheritance:r вы потеряете доступ к каталогу.

New-Item -ItemType Directory -Force -Path C:\ZabbixScripts | Out-Null
$cs = 'Server=localhost;Database=msdb;'
$cs += 'User ID=zbx_backup_mon;Password=<ПАРОЛЬ>'
Set-Content -Path C:\ZabbixScripts\mssql.conn -Encoding ascii -Value $cs
icacls C:\ZabbixScripts /inheritance:r `
  /grant '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F'

Пароль всё равно лежит на том же сервере открытым текстом: это осознанный компромисс простого примера. Смягчает его то, что логин только читает три таблицы, файл закрыт для обычных пользователей, а пароль не повторяется на других серверах. После создания файла закройте консоль или очистите историю сеанса и не сохраняйте скрипты SSMS с паролем. Альтернатива — проверка подлинности Windows от отдельной учётной записи службы агента: пароля в файле не будет, но нужны смена учётной записи службы и логин в SQL Server; здесь мы её не разбираем.

Скрипт C:\ZabbixScripts\mssqlbk.ps1. Обратите внимание на две детали. Набор допустимых значений параметра жёстко ограничен: через ключ элемента в команду не попадёт ничего лишнего. А ошибки не перехватываются: при сбое скрипт выведет текст ошибки, числовой элемент станет неподдерживаемым, и мы получим тревогу вместо ложного нуля.

param(
  [Parameter(Mandatory, Position = 0)]
  [ValidateSet('hours_since_last', 'unprotected', 'failed_jobs24')]
  [string]$Metric
)
$ErrorActionPreference = 'Stop'
$conn = (Get-Content -Raw -Path "$PSScriptRoot\mssql.conn").Trim()

$sql = @{
  hours_since_last = @'
SELECT ISNULL(DATEDIFF(MINUTE, MAX(backup_finish_date), GETDATE()) / 60,
              9999)
FROM msdb.dbo.backupset
WHERE type = 'D' AND is_copy_only = 0 AND is_damaged = 0
  AND database_name NOT IN ('master', 'model', 'msdb', 'tempdb')
'@
  unprotected = @'
SELECT COUNT(*) FROM sys.databases AS d
WHERE d.state_desc = 'ONLINE' AND d.source_database_id IS NULL
  AND d.name NOT IN ('master', 'model', 'msdb', 'tempdb')
  AND NOT EXISTS (SELECT 1 FROM msdb.dbo.backupset AS b
    WHERE b.database_name = d.name AND b.type = 'D'
      AND b.is_copy_only = 0 AND b.is_damaged = 0
      AND b.backup_finish_date > DATEADD(HOUR, -26, GETDATE()))
'@
  failed_jobs24 = @'
SELECT COUNT(*)
FROM msdb.dbo.sysjobhistory AS h
JOIN msdb.dbo.sysjobs AS j ON j.job_id = h.job_id
CROSS APPLY (SELECT DATETIMEFROMPARTS(
    h.run_date / 10000, h.run_date % 10000 / 100, h.run_date % 100,
    h.run_time / 10000, h.run_time % 10000 / 100, h.run_time % 100, 0)
  AS started) AS t
WHERE h.step_id = 0 AND h.run_status = 0
  AND j.name LIKE 'Backup%'
  AND t.started >= DATEADD(HOUR, -24, GETDATE())
'@
}

$cn = New-Object System.Data.SqlClient.SqlConnection $conn
try {
  $cn.Open()
  $cmd = $cn.CreateCommand()
  $cmd.CommandText = $sql[$Metric]
  $cmd.CommandTimeout = 20
  [string]$cmd.ExecuteScalar()
}
finally { $cn.Dispose() }

Пояснения к запросам. Состояние ONLINE — значение столбца state_desc в sys.databases; непустой source_database_id означает снимок базы, его мы исключаем. Тип D в backupset — полная копия базы; в нашем примере копии с типами I (разностные) и L (журнал) в метрики не входят. Разовые копии COPY_ONLY и копии с признаком повреждения мы не считаем: иначе ручная копия скроет сбой штатного задания. В sysjobhistory значение run_status = 0 означает ошибку, а строка с step_id = 0 — итог всего задания, а не отдельного шага (так описано в справке sp_help_jobhistory); без этого условия одно упавшее задание считалось бы несколько раз. Фильтр j.name LIKE 'Backup%' условный: подставьте то, что показал запрос из предыдущего раздела.

Теперь подключите скрипт к агенту. В zabbix_agent2.conf на SRV1:

UserParameterDir=C:\ZabbixScripts
UserParameter=mssql.backup[*],powershell -NoProfile -File .\mssqlbk.ps1 $1

UserParameterDir позволяет писать в команде относительные пути: агент сменит рабочий каталог перед запуском. Строку UserParameter нельзя переносить на следующую строку, поэтому параметр скрипта сделан позиционным. Аргументы гибких параметров ([*]) подставляются в команду, а это создаёт риск инъекции команд. Здесь обе защиты работают вместе: по умолчанию (UnsafeUserParameters=0) агент не принимает в аргументах ряд спецсимволов, а скрипт принимает только три известных значения. Не включайте UnsafeUserParameters ради удобства. Если политика выполнения PowerShell запрещает запуск локальных скриптов, не отключайте проверку «для удобства»: подпишите скрипт или настройте политику решением администратора (практика автора).

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

zabbix_agent2.exe -c "C:\Program Files\Zabbix Agent 2\zabbix_agent2.conf" ^
  -t "mssql.backup[unprotected]"

Результат такого теста не гарантирует работу под учётной записью службы: права и окружение отличаются, на это указывает справка по zabbix_agent2. Поэтому окончательная проверка — «Последние данные» в Zabbix. Сравните числа с запросами, выполненными вручную под администратором SQL Server и под zbx_backup_mon: если unprotected под этим логином меньше, чем под администратором, логин видит не все базы.

Создайте на узле SRV1 три элемента типа Zabbix agent (active) с ключами mssql.backup[hours_since_last], mssql.backup[unprotected], mssql.backup[failed_jobs24]: тип информации — число, интервал — например десять минут. Холодный запуск powershell.exe и подключение к SQL Server занимают секунды, поэтому сразу задайте тайм-аут элемента (в Zabbix 7.0 это режим Override, от 1 до 600 секунд; для примера хватит 20–30 секунд).

Шаг 3. Триггеры, пороги и зависимости

Порог — это интервал копирования плюс запас. Для ежедневных копий предупреждение мы выставили через 26 часов, а аварию — через 50: это пропуск минимум двух прогонов (практика автора). Остальные пороги — стартовые, подбирайте по факту. Скрипт считает целые часы с округлением вниз, поэтому пороги записаны как «не менее»: >26 сработало бы только через 27 часов.

Триггер Выражение Серьёзность
Давно не было копии last(/SRV1/mssql.backup[hours_since_last])>=26 Warning
Копий нет двое суток last(/SRV1/mssql.backup[hours_since_last])>=50 High
Базы без свежей копии last(/SRV1/mssql.backup[unprotected])>5 Warning
Падают задания копирования last(/SRV1/mssql.backup[failed_jobs24])>3 Warning
Метрики перестали приходить nodata(/SRV1/mssql.backup[hours_since_last],30m)=1 High

Строка про nodata закрывает ту же лазейку, что и в шаге 1: если скрипт не подключился к SQL Server (сменили пароль, остановили службу), значения не придут, last() перейдёт в состояние Unknown и не создаст проблему, а nodata сообщит, что слепы мы сами. Порог nodata — тридцать минут при десятиминутном опросе.

Предупреждение «26 часов» сделайте зависимым от аварии «50 часов» (вкладка Dependencies в форме триггера). Пока родительский триггер в состоянии Problem, зависимый не меняет состояние, и по нему не выполняются действия. Это убирает дублирующие уведомления, когда оба условия становятся истинными одновременно, например при первом опросе после долгого простоя. Уже открытую проблему такая зависимость не закрывает.

Чтобы сработки доходили до людей, нужны действие и тип оповещения; без них проблемы видны только в интерфейсе. Настройка — в статье про Zabbix с нуля.

Откат

Если мониторинг нужно убрать: отключите элементы и триггеры в Zabbix; удалите строку UserParameter и перезапустите агента; удалите файл mssql.conn; на SQL Server выполните

USE msdb;
DROP USER zbx_backup_mon;
USE master;
DROP LOGIN zbx_backup_mon;

Права на каталог C:\ZabbixScripts возвращает icacls C:\ZabbixScripts /reset, если каталог оставляете.

Что показали первые срабатывания

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

Выводы и чек-лист

Следите за результатом (свежая успешная копия каждой базы), а не за процессом (служба и место на диске). Чек-лист:

  • у каждой копии есть «свидетель успеха»: файл-отметка, запись в backupset или оба;
  • порог равен интервалу плюс запас, а авария — минимум два пропущенных прогона;
  • отдельная метрика считает базы без свежей копии, а не только возраст самой свежей;
  • есть сигнал «метрика перестала приходить» (nodata), иначе поломка самого мониторинга выглядит как тишина;
  • скрипт не возвращает ноль при ошибке, а делает элемент неподдерживаемым;
  • учётная запись мониторинга читает только нужное, а файл с её паролем доступен только SYSTEM и Administrators;
  • перед копированием скрипта на другой сервер проверены реальные имена заданий и пути копий;
  • раз в квартал копию восстанавливают в тестовой среде.

Мониторинг свежести не равен проверке восстановимости. RESTORE VERIFYONLY проверяет, что набор полон и читается (в SQL Server с дополнительными проверками), но, как сказано в описании команды, не пытается проверить структуру данных в нём и не заменяет настоящего восстановления.

Состояние на ноябрь 2026 года (проверьте актуальность). Примеры сверены с документацией Zabbix 7.0: гибкие тайм-ауты элементов и параметр UserParameterDir описаны там; в селекторе версий документации уже есть и ветка 8.0, поэтому при другой версии сверьте синтаксис выражений и поля формы. У агента 2 есть встроенный плагин MSSQL (ключи mssql.last.backup.get и mssql.job.status.get); пример выше самописный и нужен, чтобы показать принцип «результат, а не процесс», а свой ключ при желании назовите иначе, чтобы не путать с плагином. Страницы backupset, sysjobhistory и sys.databases относятся к SQL Server; если у вас управляемый экземпляр или другая редакция, проверьте, какие столбцы и функции доступны.

Типичные ошибки и недоразумения

  • Мониторят только службу и место. Служба может работать, а копии нет; места может хватать именно потому, что туда давно ничего не пишут.
  • Скрипт скопирован без проверки механизма копирования. Метрика «упавшие задания» всегда нулевая, потому что ищет несуществующие имена. Сначала посмотрите sysjobs и backupmediafamily.physical_device_name.
  • Ручная копия COPY_ONLY засчитана как штатная. Она «освежает» метрику и скрывает падение задания.
  • Пароль логина мониторинга с правами выше нужных или в общедоступном месте. Логину достаточно чтения нескольких объектов msdb, а файлу — доступа только для SYSTEM и Administrators.
  • Скрипт глотает ошибки и возвращает ноль. Тогда сбой подключения выглядит как «всё хорошо». Пусть числовой элемент лучше станет неподдерживаемым.
  • Один порог для всех копий. Еженедельная копия при пороге в 26 часов будет шуметь, а ежедневная при пороге в неделю — молчать. Порог выводят из интервала.
  • Мониторинг без оповещений. Сработки видны только тому, кто смотрит интерфейс.

Проверьте себя

  1. Почему проверки службы копирования и остатка места не заменяют мониторинг результата? Они показывают, что процесс жив и место есть, но не то, что задание выполнилось. Задание может быть отключено или падать, а оба показателя останутся в норме.
  2. Почему метрика «время с последней успешной копии» строится по backupset, а не по ошибкам заданий? В backupset попадает результат успешной операции, поэтому отсутствие свежей строки и есть признак проблемы. Ошибки в sysjobhistory не появляются, если задание не запускалось.
  3. Что произойдёт с триггером last(...)>=26, если скрипт не смог подключиться к SQL Server, и чем это закрыть? Элемент станет неподдерживаемым, и выражение перестанет вычисляться, тревоги не будет. Это закрывает триггер nodata, который работает и для неподдерживаемых элементов.
  4. Как выбрать пороги для еженедельной копии? Интервал плюс запас: предупреждение чуть позже недели, авария — после пропуска двух прогонов. Числа 26 и 50 часов подходят только для ежедневных копий.
  5. Почему нельзя просто перенести скрипт на другой сервер? Механизмы копирования разные: имена заданий и пути отличаются. Метрика «упавшие задания» окажется нулевой из-за несовпадения имён. Сначала проверьте sysjobs и backupmediafamily.

Что дальше

Источники

Ещё по теме «Мониторинг»