
After-Image: отказоустойчивая синхронизация архивов
Что такое After-Image File Management?
AI File Management (After-Image File Management) – это встроенный автоматический механизм СУБД Progress OpenEdge (OpenEdge RDBMS), предназначенный для управления файлами транзакционного лога (After-Image файлов).
В базах данных enterprise-уровня механизм After-Imaging используется для обеспечения непрерывной защиты данных и катастрофоустойчивости. Он записывает все изменения, происходящие в БД, в специальные After-Image файлы. В случае сбоя эти файлы позволяют восстановить базу данных до состояния на момент последней транзакции, либо непрерывно накатывать их на резервную базу данных (Standby/Replication Server).
Когда AI-файл заполняется, СУБД должна переключить запись на следующий пустой файл, а заполненный – заархивировать и очистить. Механизм AI File Management полностью автоматизирует этот процесс на стороне самой СУБД, исключая риск остановки базы из-за переполнения логов транзакций.
Настройка параметров старта базы данных
Для активации и корректной работы встроенного AI File Management на этапе запуска базы данных задаются следующие ключевые параметры:
-aiarcdir <каталог>– указывает целевой каталог (или список каталогов через запятую), куда СУБД будет перемещать архивированные файлы After-Image.- aiarcinterval <время>– определяет интервал (в секундах) архивации областей хранения After-Image.
Механизм AI File Management ведёт свой лог-файл вида dbname.archival.log, который хранится в каталоге базы данных, а именно в том каталоге, где находятся файлы dbname.db и dbname.lg, именно этот лог-файл будет использоваться для контроля появления новых архивов After-Image.
Проблема и цель создания скрипта
Несмотря на свое удобство, стандартный механизм AI File Management имеет существенное ограничение он умеет архивировать файлы After-Image только локально – строго в каталоги, указанные в параметре -aiarcdir.
На практике этого недостаточно для обеспечения комплексной безопасности бизнеса. Задачи следующего уровня «из коробки» решать не умеет:
- Транспортировка данных: копирование и надежная доставка критически важных AI-архивов на удаленный архивный сервер, геораспределенный резервный сервер (Standby) или на ленточную библиотеку.
- Управление дисковым пространством: контроль продолжительности хранения файлов в исходных локальных каталогах и своевременное освобождение места.
Таким образом, для построения полноценной инфраструктуры резервного копирования и обеспечения высокой доступности (High Availability) требуется внешняя автоматизация.
Цель создания данного скрипта – ликвидировать это ограничение. Скрипт в режиме реального времени перехватывает события успешного архивирования на стороне СУБД, берет на себя задачу гарантированной и отказоустойчивой доставки AI-файлов на удаленный сервер через rsync, контролирует историю на случай сбоев и организует умное управление процессами для неограниченного числа баз данных в системе.
Скрипт следит за логом AI File Management .archival.log, который хранится в каталоге базы данных. При этом на уровне инициализации проверяется синхронизация AI-архивов между двумя последними бэкапами, а затем скрипт встает на постоянное дежурство, т.е. при появлении новых AI-архивов сразу копирует их на резервный сервер. Контроль файла dbname.archival.log гарантирует что AI-архив полностью сформирован, так как запись в логе появляется только после того, как это произошло.
Описание логики работы скрипта
- Этап 1: Обработка истории (инициализация): при запуске скрипт сканирует лог-файл и находит файлы с кодом
0001, которые были записаны строго между предпоследним и последним бэкапами (код0032). Обрабатывает их. А затем приступает к обработке всех файлов с кодом (код0001), созданных после последнего бэкапа (код0032). Это гарантирует, что данные, накопленные за время простоя скрипта, будут досланы. - Этап 2: Мониторинг в реальном времени: скрипт переключается на непрерывное отслеживание лога с помощью
tail -n 0 -F. При появлении новой строки происходит мгновенная отправка.
Ключевые возможности и механизмы защиты
- Дозапись при обрывах: Используется флаг
-P(--partial). При обрыве связи недописанный файл сохраняется на удаленной стороне, а при следующей попыткеrsyncпродолжит передачу с места обрыва. - Цикл автоповторов: При сбое сети скрипт делает до 5 попыток отправки с паузой в 10 секунд.
- Сохранение структуры каталогов (Опция): По умолчанию утилита
rsyncзапускается с флагом-R(--relative). Это позволяет полностью воссоздать исходную иерархию папок на удаленном сервере.- Пример: Если папка назначения на сервере указана как
/archive/, а исходный файл лежит по пути/db/sports/ai/file.tar, то на целевой машине будет автоматически создана цепочка каталогов, и файл займет точное место:/archive/db/sports/ai/file.tar.
- Отключение: Если создавать вложенные папки не нужно (и все файлы должны складываться напрямую в корень целевой директории), просто удалите флаг
-Rиз строки вызоваrsyncв коде скрипта.
- Пример: Если папка назначения на сервере указана как
- Очистка устаревших исходных файлов: Скрипт автоматически контролирует свободное место в исходном каталоге AI-архивов. Раз в сутки (а также при старте после синхронизации истории) запускается проверка, удаляющая локальные файлы старше заданного срока (по умолчанию
RETENTION_DAYS=0дней). Если в настройках поставить0, функция автоматически отключится – по умолчанию в скрипте это сделано для безопасности. Поиск ограничен первым уровнем каталога (-maxdepth 1) и исключает рабочий лог.archival.log. - Корректный Shutdown (Без перезапуска): Скрипт регулярно проверяет наличие динамического стоп-файла. Как только файл обнаружен, скрипт завершает текущую отправку, удаляет стоп-файл и завершает работу со специфическим статус-кодом
120. Демонsystemdперехватывает этот код и оставляет службу выключенной навсегда, предотвращая циклический авторестарт.
Пошаговая инструкция по настройке
Шаг 1. Настройка беспарольного доступа по SSH
Чтобы утилита rsync могла работать в автоматическом фоновом режиме, необходимо настроить авторизацию по SSH-ключам. Выполните следующие команды на исходном сервере:
- Проверьте наличие существующего ключа:
ls -l ~/.ssh/id_rsa.pub
Если файл найден, переходите к пункту 3. Если выдана ошибка No such file or directory, ключ нужно создать.
- Сгенерируйте новый SSH-ключ (если его не было):
ssh-keygen -t rsa -b 4096
На все вопросы системы нажимайте Enter. Не вводите пароль (passphrase), иначе автоматизация не сможет работать без участия человека.
- Скопируйте публичный ключ на удаленный сервер (замените
userиremote-hostна ваши данные):
ssh-copy-id user@remote-host
Система в последний раз попросит ввести пароль от вашей учетной записи на удаленном сервере для сохранения ключа.
- Проверьте авторизацию:
ssh user@remote-host
Если вы успешно вошли в консоль удаленной машины без ввода пароля, настройка выполнена верно. Введите exit, чтобы вернуться.
Шаг 2. Исходный код скрипта (syncai_manager.sh)
Создайте файл syncai_manager.sh и вставьте в него следующий код:
#!/bin/bash
# ==========================================
# ВХОДНЫЕ ПАРАМЕТРЫ И ВАЛИДАЦИЯ
# ==========================================
LOG_FILE="$1" # Полный путь к файлу лога передается аргументом от systemd
if [ -z "$LOG_FILE" ]; then
echo "[КРИТИЧЕСКАЯ ОШИБКА] Не указан путь к лог-файлу!"
echo "Использование: $0 <полный_путь_к_логу>"
exit 1
fi
if [ ! -f "$LOG_FILE" ]; then
echo "[КРИТИЧЕСКАЯ ОШИБКА] Лог-файл $LOG_FILE не найден!"
exit 1
fi
# ==========================================
# НАСТРОЙКИ RSYNC И СИГНАЛОВ
# ==========================================
REMOTE_SERVER="user@remote-host"
REMOTE_DIR="/path/to/destination/"
MAX_RETRIES=5 # Попыток для rsync при сбое сети
RETRY_DELAY=10 # Пауза между попытками в секундах
RETENTION_DAYS=0 # Срок хранения исходных локальных файлов в днях (0 - отключить)
CLEANUP_INTERVAL=86400 # Интервал запуска очистки в секундах (86400 = 1 раз в сутки)
LAST_CLEANUP_TIME=0 # Таймер последней очистки
AI_ARCHIVE_DIR="" # Определяется автоматически из пути скопированных файлов
# Динамическое имя стоп-файла (без экранирования)
LOG_HASH=$(echo -n "$LOG_FILE" | md5sum | awk '{print $1}')
STOP_FILE="/tmp/sync_${LOG_HASH}.stop"
echo "STOP_FILE для остановки: $STOP_FILE"
# Удаляем старый стоп-файл при старте, если он остался
if [ -f "$STOP_FILE" ]; then
rm -f "$STOP_FILE"
fi
# ==========================================
# ФУНКЦИЯ ОЧИСТКИ УСТАРЕВШИХ ИСХОДНЫХ ФАЙЛОВ
# ==========================================
cleanup_old_files() {
local target_dir="$1"
if [ -z "$target_dir" ] || [ ! -d "$target_dir" ] || [ "$RETENTION_DAYS" -le 0 ]; then
return 0
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Запуск очистки файлов старше $RETENTION_DAYS дн. в каталоге: $target_dir"
find "$target_dir" -maxdepth 1 -type f -mtime +"$RETENTION_DAYS" ! -name "*.archival.log" ! -name ".*" -print0 | while IFS= read -r -d '' old_file; do
echo "[$(date '+%H:%M:%S')] Удаление устаревшего файла: $old_file"
rm -f "$old_file"
done
}
# ==========================================
# ФУНКЦИЯ ОТПРАВКИ ЧЕРЕЗ RSYNC
# ==========================================
sync_file_with_retry() {
local file_path="$1"
if [ ! -f "$file_path" ]; then
echo "[$(date '+%H:%M:%S')] Предупреждение: Файл $file_path отсутствует на диске."
return 1
fi
local attempt=1
local success=0
while [ $attempt -le $MAX_RETRIES ]; do
if [ -f "$STOP_FILE" ]; then
echo "[$(date '+%H:%M:%S')] Обнаружен стоп-файл. Прерывание отправки файла $file_path."
return 1
fi
echo "[$(date '+%H:%M:%S')] Попытка $attempt из $MAX_RETRIES: Отправка $file_path"
rsync -azRP -e "ssh -o StrictHostKeyChecking=no" "$file_path" "$REMOTE_SERVER:$REMOTE_DIR"
if [ $? -eq 0 ]; then
echo "[$(date '+%H:%M:%S')] Успешно отправлено."
AI_ARCHIVE_DIR=$(dirname "$file_path")
success=1
break
else
echo "[$(date '+%H:%M:%S')] [Сбой] Ошибка rsync на попытке $attempt."
if [ $attempt -lt $MAX_RETRIES ]; then
sleep $RETRY_DELAY
fi
((attempt++))
fi
done
if [ $success -eq 0 ]; then
echo "[ОШИБКА] Файл пропущен после $MAX_RETRIES попыток: $file_path"
return 1
fi
return 0
}
# ==========================================
# ЭТАП 1: ОБРАБОТКА ИСТОРИИ И НАКОПЛЕННЫХ ЛОГОВ
# ==========================================
echo "=== ЭТАП 1: Проверка пропущенных файлов для лога $LOG_FILE ==="
# За один проход AWK собирает два независимых списка файлов
eval "$(awk -F',' '
$1 == "0001" {
# Складываем во временный буфер текущего окна бэкапа
tmp[++tmp_count] = $9
next
}
$1 == "0032" {
# Встретили бэкап. Переносим всё из временного буфера в закрытый интервал (Интервал А)
delete final
final_count = 0
for (i = 1; i <= tmp_count; i++) {
final[++final_count] = tmp[i]
}
# Очищаем временный буфер
delete tmp
tmp_count = 0
}
END {
# Формируем переменные для Bash.
# В final остались строки между предпоследним и последним бэкапом.
for (i = 1; i <= final_count; i++) {
print "echo \"" final[i] "\" >> /tmp/interval_A_$$"
}
# В tmp осталось то, что накопилось ПОСЛЕ самого последнего бэкапа до конца файла.
for (i = 1; i <= tmp_count; i++) {
print "echo \"" tmp[i] "\" >> /tmp/interval_B_$$"
}
}' "$LOG_FILE")"
# --- Обработка Интервала А (Между бэкапами) ---
if [ -f "/tmp/interval_A_$$" ]; then
echo "1.1 Синхронизация истории между последними бэкапами (Интервал А)..."
while read -r hist_file; do
if [ -f "$STOP_FILE" ]; then
echo "[ВНИМАНИЕ] Получен сигнал остановки. Выход."
rm -f "/tmp/interval_A_$$" "/tmp/interval_B_$$" 2>/dev/null
exit 120
fi
if [ -n "$hist_file" ]; then
sync_file_with_retry "$hist_file"
echo "----------------------------------------"
fi
done < "/tmp/interval_A_$$"
rm -f "/tmp/interval_A_$$"
else
echo "1.1 В закрытом интервале бэкапов файлов не найдено."
fi
# --- Обработка Интервала Б (Текущий хвост) ---
if [ -f "/tmp/interval_B_$$" ]; then
echo "1.2 Синхронизация файлов, накопленных после последнего бэкапа (Интервал Б)..."
while read -r tail_file; do
if [ -f "$STOP_FILE" ]; then
echo "[ВНИМАНИЕ] Получен сигнал остановки. Выход."
rm -f "/tmp/interval_B_$$" 2>/dev/null
exit 120
fi
if [ -n "$tail_file" ]; then
sync_file_with_retry "$tail_file"
echo "----------------------------------------"
fi
done < "/tmp/interval_B_$$"
rm -f "/tmp/interval_B_$$"
else
echo "1.2 Накопленных файлов после последнего бэкапа не обнаружено."
fi
# Первичная очистка устаревших файлов после синхронизации истории
# $SECONDS — встроенная переменная Bash (счетчик секунд с момента запуска скрипта).
if [ -n "$AI_ARCHIVE_DIR" ]; then
cleanup_old_files "$AI_ARCHIVE_DIR"
LAST_CLEANUP_TIME=$SECONDS
fi
# ==========================================
# ЭТАП 2: МОНИТОРИНГ РЕАЛЬНОГО ВРЕМЕНИ
# ==========================================
echo "=== ЭТАП 2: Запуск непрерывного мониторинга лога $LOG_FILE ==="
exec 3< <(tail -n 0 -F "$LOG_FILE" | awk -F',' '$1 == "0001" {print $9; fflush()}')
TAIL_PID=$!
while true; do
if [ -f "$STOP_FILE" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Обнаружен стоп-файл $STOP_FILE. Завершение работы."
kill $TAIL_PID 2>/dev/null
rm -f "$STOP_FILE"
echo "Скрипт успешно и корректно остановлен."
exit 120
fi
# Плановая проверка на очистку устаревших файлов (раз в сутки по внутреннему таймеру $SECONDS)
if [ -n "$AI_ARCHIVE_DIR" ] && [ $((SECONDS - LAST_CLEANUP_TIME)) -ge $CLEANUP_INTERVAL ]; then
cleanup_old_files "$AI_ARCHIVE_DIR"
LAST_CLEANUP_TIME=$SECONDS
fi
if read -t 2 -u 3 live_file; then
if [ -n "$live_file" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Поступила новая запись из лога"
sync_file_with_retry "$live_file"
echo "----------------------------------------"
fi
fi
done
Шаг 3. Конфигурация параметров
Откройте созданный файл в редакторе (например, nano syncai_manager.sh) и отредактируйте переменные в блоке НАСТРОЙКИ:
- LOG_FILE — полный путь к отслеживаемому лог-файлу
dbname.archival.log. - REMOTE_SERVER — имя пользователя и хост/IP удаленного сервера.
- REMOTE_DIR — папка назначения на удаленном хосте.
Шаг 4. Назначение прав на выполнение
Задайте исполняемый флаг для созданного файла скрипта:
chmod +x syncai_manager.sh
Варианты запуска скрипта
Интерактивный запуск (для тестов и отладки)
Запустите скрипт напрямую в сессии терминала. Вы будете видеть логи обработки истории и поступающие файлы в реальном времени:
./syncai_manager.sh "/path/to/your/dbname.archival.log"
Прервать выполнение и выйти можно сочетанием клавиш Ctrl + C.
Фоновый запуск (промышленный режим)
Для работы в постоянном автономном режиме запустите менеджер через утилиту nohup. Скрипт продолжит работу даже при закрытии SSH-сессии, а логи будут направляться в указанный файл:
nohup ./syncai_manager.sh "/path/to/your/dbname.archival.log" > /var/log/syncai_manager.log 2>&1 &
Примечание:
В современных дистрибутивах Linux (например, Ubuntu, Debian, CentOS), использующих systemd, по умолчанию может быть включена жесткая очистка пользовательских сессий. Когда вы полностью выходите из системы (разрываете SSH-соединение), systemd игнорирует nohup и принудительно завершает все процессы, принадлежащие этой сессии.
Как исправить:
Проверьте конфигурацию /etc/systemd/logind.conf. Откройте файл и найдите строку:
KillUserProcesses=yes
no и перезапустите службу sudo systemctl restart systemd-logind.Альтернативные варианты
screenилиtmux — полноценные менеджеры терминальных сессий. Вы запускаете утилиту, открываете внутри неё скрипт и можете спокойно закрывать терминал. Процесс гарантированно продолжит жить.
Интеграция с systemd
Для управления процессами мониторинга разных баз данных используется один системный шаблон-сервис.
- Скопируйте скрипт
syncai_manager.shв каталог/usr/local/bin/. - Создайте файл конфигурации сервиса-шаблона:
sudo nano /etc/systemd/system/syncai_manager@.service
Вставьте в него следующее содержимое:
[Unit] Description=Отказоустойчивая синхронизация AI-логов для %I After=network.target [Service] Type=simple # Используем systemd-escape для корректного обратного преобразования пути к логу # %f автоматически подставит правильный абсолютный путь С ПЕРВЫМ СЛЭШЕМ ExecStart=/usr/local/bin/syncai_manager.sh %f #Restart=always Restart=on-failure RestartSec=10 # Указываем systemd, что код 120 — это штатный останов, перезапуск не требуется! SuccessExitStatus=120 StandardOutput=append:/var/log/syncai_manager_%i.log StandardError=append:/var/log/syncai_manager_%i.log [Install] WantedBy=multi-user.target
- Перезагрузите конфигурацию демона
systemd:
sudo systemctl daemon-reload
Как запускать мониторинг для баз данных
Так как логи баз данных лежат в разных каталогах (например, /var/db/sports/sports.archival.log и /opt/billing/billing.archival.log), пути содержат символы /. Чтобы передать такой путь в имя шаблонного сервиса systemd, его нужно экранировать утилитой systemd-escape.
Пример: Запуск мониторинга для базы Sports
- Сгенерируйте имя сервиса для пути лога:
systemd-escape -p "/var/db/sports/sports.archival.log"
Система выдаст строку вида: var-db-sports-sports.archival.log
- Запустите и добавьте сервис в автозапуск:
sudo systemctl start syncai_manager@var-db-sports-sports.archival.log.service sudo systemctl enable syncai_manager@var-db-sports-sports.archival.log.service
Управление и просмотр статуса systemd
- Проверить статус конкретного лога:
sudo systemctl status sync_manager@var-db-sports-sports.archival.log.service
- Логи работы каждой конкретной базы будут автоматически писаться в раздельные файлы:
/var/log/syncai_manager_var-db-sports-sports.archival.log - Для остановки использовать
stop:
sudo systemctl stop sync_manager@var-db-sports-sports.archival.log.service
Остановка в автономном режиме
Если скрипт запускался вручную в автономном режиме, то остановить его можно через STOP_FILE, который можно найти в логе:
grep STOP_FILE /var/log/syncai_manager_var-db-sports-sports.archival.log
Или путем генерации имени файла вручную. Поскольку имя стоп-файла привязано к хэшу пути конкретного лога, то необходимо узнать точное имя файла и создать его. Имя стоп-файла вычисляется скриптом автоматически на основе пути к логу. Чтобы узнать его для вашей конкретной базы данных, выполните в терминале команду (подставив ваш путь к логу):
LOG_FILE="/var/db/sports/sports.archival.log"
echo "/tmp/sync_$(echo -n "$LOG_FILE" | md5sum | awk '{print $1}').stop"
Система выдаст уникальный путь, например: /tmp/sync_c83903581b662cf561ecfa1bf4f968f0.stop
Создайте этот стоп-файл:
touch /tmp/sync_c83903581b662cf561ecfa1bf4f968f0.stop
В течение максимум 2 секунд скрипт обнаружит файл, завершит текущую активную отправку rsync (если она шла) и выведет строки в лог менеджера:
[2026-10-22 13:05:00] Обнаружен стоп-файл tmp/sync_c839...stop. Завершение работы. Скрипт успешно и корректно остановлен.
Если вы введете systemctl status syncai_manager@var-db-sports..., вы увидите статус inactive (dead), а в логе systemd будет написано: (code=exited, status=120). При этом служба сама больше не поднимется, пока вы не введете команду systemctl start вручную. Скрипт сам удалит за собой стоп-файл. Вы можете убедиться в этом, проверив папку /tmp — файла там больше быть не должно.
Настройка ротации логов (logrotate)
Чтобы файлы логов самого менеджера синхронизации не переполняли сервер, настроим автоматическую ротацию по маске.
- Создайте файл конфигурации ротации:
sudo nano /etc/logrotate.d/syncai_manager
- Вставьте настройки для всех логов менеджера:
/var/log/syncai_manager_*.log {
weekly
rotate 4
size 50M
compress
delaycompress
missingok
notifempty
copytruncate
}
Описание параметров ротации
- weekly — ротация будет производиться раз в неделю.
- rotate 4 — в системе будет храниться максимум 4 архивных файла (логи за последний месяц). Старые архивы будут автоматически удаляться.
- size 50M — если файл разрастется быстрее, чем за неделю, и превысит 50 Мегабайт, утилита произведет внеочередную ротацию.
- compress — архивные логи будут автоматически сжиматься в формат .gz для жесткой экономии места.
- delaycompress — откладывает сжатие самого последнего архива на один цикл ротации (чтобы к нему сохранялся быстрый текстовый доступ в случае разбора недавних инцидентов).
- missingok — если файла лога временно не существует, утилита не будет выдавать системную ошибку.
- notifempty — не ротировать пустой файл лога.
- copytruncate — критически важный параметр. Вместо физического перемещения файла, он копирует его содержимое в архив, а исходный рабочий файл мгновенно обнуляет (обрезает до 0 байт). Это позволяет нашему фоновому процессу фонового скрипта продолжать непрерывную запись в дескриптор файла без сбоев и необходимости перезапуска самого менеджера.
ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ И ГАРАНТИЙ
ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ ГАРАНТИЯМИ ТОВАРНОЙ ПРИГОДНОСТИ, СООТВЕТСТВИЯ ОПРЕДЕЛЕННОМУ НАЗНАЧЕНИЮ И НЕНАРУШЕНИЯ ПРАВ. НИ В КОЕМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ПО КАКИМ-ЛИБО ИСКАМ, ЗА УЩЕРБ ИЛИ ПО ИНЫМ ОБЯЗАТЕЛЬСТВАМ, БУДЬ ТО В РЕЗУЛЬТАТЕ ДЕЙСТВИЯ ДОГОВОРА, ДЕЛИКТА ИЛИ ИНЫМ ОБРАЗОМ, ВОЗНИКАЮЩИМ ИЗ-ЗА ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ, В СВЯЗИ С НИМ, ЕГО ИСПОЛЬЗОВАНИЕМ ИЛИ ИНЫМИ ДЕЙСТВИЯМИ С НИМ.

