Bareos: ленты, Hyper-V и ещё всякое

в 22:19, , рубрики: bacula, bareos, hyper-v, а забэкапил ли я сервер, резервное копирование, системное администрирование

Это пост про жизнь после Getting Started with Bareos, и о чем пришлось дольше всего читать всеобъемлющий мануал.

Bareos: ленты, Hyper-V и ещё всякое - 1
Стородж

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

Ситуация

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

Почему бареос

Потому что доступен виндовый клиент. Как известно, Bareos — это драматический форк Bacula, заслуженной и проверенной. Но во время выбора бакула зажимала исходники (да и бинари) своего fd для Windows, поэтому нет. Veeam хорош, но стóит как кресло стадиона ФК «Зенит». Был DPM, но сколько мы с Антоаном из техподдержки Майкрософта с ним не боролись, любви так и не возникло.

Установка

была описана неоднократно. Кто такой директор и чем он занимается с другими демонами — можно почитать, например, здесь. Замечу только, что dir и sd крайне желательно должны быть одной версии, версия fd не так важна. По ощущениям от чейнджлогов, версию лучше иметь 16 или выше.

Базовая настройка

По DPM-овской привычке хотел создать большое задание и в него много напихать. Оказалось, что маленькие задания удобнее: при неудачном выполнении маленькое быстрее выполнится повторно и лучше пролезает через спулер (об этом дальше). На процесс восстановления размер задания не влияет, разве что задание с сотнями тысяч файлов может подтормаживать на этапе их выбора.

Hyper-V

Виртуальные машины (ВМ) работают на кластерах Hyper-V. В пределах кластера на нодах настройки fd одинаковы, хостнеймом у всех указано имя кластера. В директоре в качестве клиента тоже указан кластер со своим кластерным адресом. ВМ может переехать на другой том кластера, поэтому указываем не конкретный путь, а путь к скрипту:

Скрытый текст
FileSet {
    # в названии набора файлов указываем имя виртуалки, как это удобно
    Name = "VM_lamachine-fs"
    Include {
        # указываем имя ВМ (наличие/отсутствие "example.com" тоже важно)
        File = "\|C:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -file c:/cmd/search-vm.ps1 -machine lamachine.example.com"
        Options {
            # диски ВМ очень хорошо жмутся
            Compression = LZO
            # исключаем большой лишний файл
            RegexFile = ".*/Virtual Machines/.*.bin"
            Exclude = yes
        }
    }
}

А вот c:cmdsearch-vm.ps1, который и отдаёт путь к машине:

Param(
  [string]$level,
  [string]$machine = "NOEXISTENTVM.example.com"
)

Import-Module failoverclusters

$backuppath = @()
$Cluster = Get-Cluster

$ClusterMachines = @()
$ClusterMachines += Get-ClusterResource -Cluster $Cluster | where { $_.ResourceType -like "Virtual Machine" } | where { $_.Name -like "*$machine"} | `
		select -Property OwnerNode,Name, @{
			Name ="VmID";Expression ={ (Get-ClusterParameter -Cluster $Cluster -InputObject $_ | where { $_.Name -eq "VmID" } | select -Property Value).Value }
		}

if ($ClusterMachines.Count -eq 0 ) {
    "NO MACHINES"
    exit 2
}

foreach ($ClusterMachine in $ClusterMachines){
    $VM = Get-VM -ComputerName $ClusterMachine.OwnerNode -Id $ClusterMachine.VmID
    $path = $VM.Path.Replace('','/')
    $backuppath += $path
    foreach ($HardDrive in $VM.HardDrives){
        $drivepath = $HardDrive.Path | Split-Path -Parent
        $drivepath = $drivepath.Replace('','/')
        if ($drivepath -notin $backuppath){
            $backuppath += $drivepath
        }
    }
}
$backuppath

Перед бэкапом делается снапшот, после бэкапа он удаляется, для этого есть пара чьих-то грубо допиленных скриптов.

Скрытый текст

Создание:

#Copyright disclaimer:
#    Copyright (C) 2015,  ITHierarchy Inc (www.ithierarchy.com). ALl rights reserverd.
#    This program is free software: you can redistribute it and/or modify
#        it under the terms of the GNU General Public License as published by
#        the Free Software Foundation, either version 3 of the License, or
#        (at your option) any later version.
#
#        This program is distributed in the hope that it will be useful,
#        but WITHOUT ANY WARRANTY; without even the implied warranty of
#        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#        GNU General Public License for more details.
#
#        You should have received a copy of the GNU General Public License
#        along with this program.  If not, see <http://www.gnu.org/licenses/>.

Param(
  [string]$level,
  [string]$machine = "noexist.example.com",
  #[string]$prefix = "",
  [int]$DayOfWeekForFullBackup = 2
)

Import-Module failoverclusters

"Processing $machine via $env:computername"

$dow = [int]$(get-date).DayOfWeek
if ($dow -eq $DayOfWeekForFullBackup){
    $prefix="Weekly"
}

$DateStamp=$(((get-date)).ToString("yyyyMMddTHHmmss"))
if ($level -eq "Full"){$Backup=" Bacula -*"}Else{$Backup=" Bacula -$level*"}

#$HyperVPath="C:Hyper-V"  #Set path to your Hyper-V Machines to be backed up

#Sort out Actual Volume path to VM
#$VMDrive=$HyperVPath.Substring(0,1)
#$volume=Get-Volume $VMDrive
#$TrueHyperVPath=$($HyperVPath.Replace("$($VMDrive):",$($Volume.path)))

    #Get List of VMs

$Cluster = Get-Cluster

# let's initialize it like array (for simplier size check)
$ClusterMachines = @()
$ClusterMachines += Get-ClusterResource -Cluster $Cluster | where { $_.ResourceType -like "Virtual Machine" } | where { $_.Name -like "*$machine"} | `
		select -Property OwnerNode,Name, @{
			Name ="VmID";Expression ={ (Get-ClusterParameter -Cluster $Cluster -InputObject $_ | where { $_.Name -eq "VmID" } | select -Property Value).Value }
		}

if ($ClusterMachines.count -gt 1){
    "Ambiguous machine name"
    exit 2
}

if ($ClusterMachines.count -ne 1){
    "Machine not found: absent, not in failover cluster or something"
    exit 2
}

foreach ($ClusterMachine in $ClusterMachines){
    $VM = Get-VM -ComputerName $ClusterMachine.OwnerNode -Id $ClusterMachine.VmID
    write-host "Working on VM $($vm.Name) @ '$($vm.Path)'"
            $CurrentSnapShots = $VM | Get-VMSnapshot
            foreach ($SnapShot in $CurrentSnapShots){
                if ($SnapShot.Name -like ("$($prefix)Backup*")){   
                    write-host "Removing VM Checkpoint '$($SnapShot.Name)'"
                    $SnapShot | Remove-VMSnapshot # -ComputerName $ClusterMachine.OwnerNode

                    $LoopCount=0
                    do {
                        Write-host "Waiting for snapshot '$($SnapShot.name)' to delete..."
                        
                        Start-Sleep -s 10 
                        $LoopCount=$LoopCount+1
                    }while ($VM.Status -eq "Merging disks" -and $LoopCount -lt 30)
                }
            }
            $label = "$($prefix)Backup-$level-$DateStamp"
            write-host "Creating Checkpoint $label ($($VM.Name))"
            $VM | Checkpoint-VM -SnapshotName  $label
}

Удаление:

#Copyright disclaimer:
#    Copyright (C) 2015,  ITHierarchy Inc (www.ithierarchy.com). ALl rights reserverd.
#    This program is free software: you can redistribute it and/or modify
#        it under the terms of the GNU General Public License as published by
#        the Free Software Foundation, either version 3 of the License, or
#        (at your option) any later version.
#
#        This program is distributed in the hope that it will be useful,
#        but WITHOUT ANY WARRANTY; without even the implied warranty of
#        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#        GNU General Public License for more details.
#
#        You should have received a copy of the GNU General Public License
#        along with this program.  If not, see <http://www.gnu.org/licenses/>.

Param(
  [string]$level,
  [string]$machine = "noexist.example.com",
  [string]$vmmserver = "vldvmm.example.com"
)

Import-Module failoverclusters

$Cluster = Get-Cluster

$ClusterMachines = Get-ClusterResource -Cluster $Cluster | where { $_.ResourceType -like "Virtual Machine" } | where { $_.Name -like "*$machine"} | `
		select -Property OwnerNode,Name, @{
			Name ="VmID";Expression ={ (Get-ClusterParameter -Cluster $Cluster -InputObject $_ | where { $_.Name -eq "VmID" } | select -Property Value).Value }
		}

# FIXME foreach по идее лишний
foreach ($ClusterMachine in $ClusterMachines){
    $VM = Get-VM -ComputerName $ClusterMachine.OwnerNode -Id $ClusterMachine.VmID
    write-host "Working on VM $($vm.Name) @ '$($vm.Path)'"
    $CurrentSnapShots = $VM | Get-VMSnapshot
    foreach ($SnapShot in $CurrentSnapShots){
        if ($SnapShot.Name -like ("Backup*")){   
            write-host "Removing VM Checkpoint '$($SnapShot.Name)'"
            $SnapShot | Remove-VMSnapshot # -ComputerName $ClusterMachine.OwnerNode
            $LoopCount=0
            do {
                Write-host "Waiting for snapshot '$($SnapShot.name)' to delete..."
                Start-Sleep -s 10 
                $LoopCount=$LoopCount+1
            }while ($VM.Status -eq "Merging disks" -and $LoopCount -lt 30)
        }
    }
}

Снапшот можно не удалять, тогда получится делать инкрементальные бэкапы (в скрипте есть зачатки такой функциональности — DayOfWeekForFullBackup).

Кассеты

Мы используем ленточные библиотеки: такая двухюнитовая двухъюнитовая двухвершковая коробка с кассетами, одним или двумя стриммерами и роботом-авточейнджером. Bacula, а по наследству и Bareos, с лентами отлично дружат (лучше, чем с HDD). Что меня смутило — в одной библиотеке бареос обнаружил два авточейнджера, такого быть не должно. Выяснилось, что устройство было программно разделено на две «логические библиотеки» ещё со времён борьбы с DPM. Идём в админку устройства и отключаем это ненужно, теперь в системе правильное количество чейнджеров — один. Устройства покажет команда «ls /dev/tape/by-id/», с суффиксом "-nst" — пишущий драйв, без оного — робот-авточейнджер.

Что касается использования двух приводов для параллельной записи в один пул (набор томов): это сократило бы время записи, но делать так не стали. В выделенное окно бэкапов и так вписываемся, а вот расход плёнки может увеличиться. Но если кто захочет параллельную запись, то не забудьте Prefer Mounted Volumes установить в No. Писать же в два разных пула можно без проблем и оверхеда.

Scratch-пулы. Хочу обратить на них внимание: из них бареос берёт кассеты для добавления в другие пулы, в которые он собирается писать. Незнакомую кассету без метки бареос в рабочий пул добавлять не станет. Поэтому все новые ленты добавляем в пул Scratch:

label barcodes storage=mylittlestorage slot=1 pool=Scratch

Можно добавить не в Scratch, а сразу в рабочий пул, но если пулов несколько, то не всегда можно предсказать, сколько кассет в каком понадобится. Поэтому пусть берёт сам по необходимости.

Размер блоков и файлов нужно поставить побольше, это благотворно скажется на скорости лент. «If you are configuring an modern drive like LTO-4 or newer, you probably will want to set the Maximum File Size to 20GB», так что не скромничайте.

Чтобы у бареоса не возникло искушения что-нибудь записать на кассету, которая уже находится в далёком сейфе, лучше перед выемкой сменить ей статус с Append на Used:

update volume=KYF389L6 volstatus=Used

Этой же командой можно менять другие свойства кассеты, скажем, переместить в другой пул:

update volume=KYF389L6 pool=Used

Кассету легко выкрасть (ну, легче, чем IBM DS8800), поэтому данные крайне желательно зашифровать. Можно сделать это средствами самой писалки, но я люблю софтовые решения как более универсальные и гибкие. Просто не забудьте.

Бывает, что бареос уже когда-то писал метку на кассету, но записи об этой кассете в базе не имеет. Второй раз label не сработает («error: already labeled»), есть команда add, но в моём случае она приводила к проблемам, после неё использовать кассету не получалось. На этот случай родился такой однострочник (выполняется в bash на сервере sd, сам bareos-sd должен быть остановлен):

mtx -f /dev/sg10 load 25 && mt -f /dev/st0 rewind && mt -f /dev/st0 weof && mt -f /dev/st0 rewind && mtx -f /dev/sg10 unload

Если не даёт сделать unload, то предварительно

mt -f /dev/st0 offline

Спулинг

Это когда данные пишутся сначала на SSD (или хотя бы быстрый HDD), а потом уже на ленту. По сравнению с последовательным выполнением, сокращается время работы стриммера (он пишет быстрее) и общее время выполнения заданий, если их много. Если задание одно, то время использования привода также сократится, но общее время выполнения задания вырастет.

Чтобы работало, сначала на стороне sd нужно указать расположение и размер спулера:

Device {
    Name = Drive-0
	...
	
    # для спулинга
    Maximum Concurrent Jobs = 20
    Spool Directory = /mnt/backup/spool
    Maximum Spool Size = 1950 G
    Maximum Job Spool Size = 1200 G
}

а затем уже включить для конкретных заданий:

JobDefs {
    Name = "SundayTape"
    ...
    Spool Data = Yes
}

У меня эта директива в шаблоне «ленточного» задания, а для «дисковых» заданий спулинг практически бесполезен.

Принципы такие:

  • задание может читаться из источника и писаться в спулер (далее «чтение») или читаться из спулера и писаться на ленту (далее «запись»). Одновременно то и другое — нет, для этого понадобится разбить задание на два. Поэтому я выше рекомендовал делать задания поменьше.
  • желательно чтобы писалка писала с максимальной скоростью, для этого в спулере всегда должны быть готовые к записи данные (в status storage=my-little-storage они отмечены флажком spool_wait).
  • нужно, чтобы спулер не переполнялся. Если переполнится, то все данные из него (сразу всех заданий!) захотят записаться на ленту (задеспулиться ), вызывая фрагментацию на ленте. Какие-нибудь 300 Гбайт могут размазаться по трём 2,5 Тбайтным лентам, а оно нам не надо.

Размер спулера и количество одновременных заданий зависят от общего количества заданий, размеров заданий, скорости чтения данных (которая может упираться в скорость сети или спулера), скорости записи ленты, соотношений размеров и скоростей разных заданий, человеческих желаний (побыстрее получить результат, или поменьше занимать писалку, или поменьше тратить кассет). Всё это можно связать адовым матаном, но я рекомендую настроить спулинг как бог на душу положит, потому что даже упрощённые правила непросты:

  • нужно выбрать количество одновременно запущенных заданий, чтобы в сумме они давали данных больше, чем пропускная способность ленты. Скорости разных источников могут сильно отличаться, даже скорость одного источника может сильно меняться. Например, fixed vhdx может отдавать 100 Мбайт/с в начале и 500 кБайт/с в конце, когда fd отдаёт пожатые нули. Это затрудняет расчёт, но c’est la vie, берите с запасом.
  • в идеале, спулер должен вмещать целиком все одновременно выполняемые задания. Но не у всех есть столько SSD, а скорость HDD может оказаться меньше скорости какого-нибудь LTO-6.
  • пока данные из одного источника ещё читаются (спулятся), другие данные могут уже записаться и освободить место в спулере, за счёт этого спулер можно сократить. Тут чем больше разница размеров заданий, тем лучше (если не получается все задания сделать маленькими).
  • если завершились все задания, кроме одного самого большого, то пусть оно спулится/деспулится сколько угодно, фрагментации это уже не сделает.
  • если вы хотите расставить свои задания в некотором порядке (скажем, сначала запустить пару маленьких и самое большое, а потом уже все остальные — это выгодно по времени), то задайте им разные расписания или разные приоритеты (но в этом случае не забудьте Allow Mixed Priority)

Количество одновременно выполняемых заданий ограничивается много где, ищите в документации «Concurrent Jobs =». Мне оказалось удобно везде поставить большое число с запасом, и ограничивать нужным числом на конкретном устройстве (sd device).

Про файлы

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

А ещё IBM рекомендует хранить одно задание в одном файле, и пока что я с ними согласен.

Мониторинг

Некоторые отчётные удобства на улице^W^W тоже пришлось добавить скриптами. Самым востребованным оказался скрипт, возвращающий статус последнего запуска задания.

Скрытый текст

#!/bin/bash

RED='33[0;31m'
NC='33[0m' # No Color
GREEN='33[0;32m'
YELLOW='33[0;33m'

JOBS=`su - postgres -c "psql -d bareos -c "WITH summary AS (
SELECT name,jobstatus,jobid,
ROW_NUMBER() OVER(PARTITION BY name ORDER BY starttime DESC) AS rk
FROM job p WHERE starttime > current_date - INTERVAL '5 days')
SELECT s.* FROM summary s WHERE s.rk=1;"" | grep "1$" | sed 's/ //g'`

#echo "$JOBS"

for job in $JOBS; do
        jobstatus=`echo $job | cut -d '|' -f2`
        jobname=`echo $job | cut -d '|' -f1`
        jobid=$(echo $job | cut -d '|' -f3)

        if  [ "$jobstatus" == "R" ]; then
                printf "%-30s" "$jobname ($jobid)"
                echo -e "$YELLOW running$NC ($jobstatus)"
        elif [ "$jobstatus" == "W" ]; then
                printf "%-30s" "$jobname ($jobid)"
                echo -e "$YELLOW warning$NC ($jobstatus)"
        elif [ "$jobstatus" == "T" ]; then
                if [[ $1 == "printall" ]]; then
                        printf "%-30s" "$jobname ($jobid)"
                        echo -e "$GREEN OK$NC ($jobstatus)"
                fi
        else
                printf "%-30s" "$jobname ($jobid)"
                echo -e "$RED failed$NC ($jobstatus)"
        fi
done

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

Есть в варианте для Zabbix

#!/bin/bash

JOBS=`su - postgres -c "psql -d bareos -c "
SELECT name,starttime,jobstatus
FROM job p WHERE starttime > current_date - INTERVAL '62 days' AND name = '$1'
ORDER BY starttime DESC LIMIT 1;"" | sed 's/ //g' | grep "|.$"`

for job in $JOBS; do
        jobname=`echo $job | cut -d '|' -f1`
        jobstatus=`echo $job | cut -d '|' -f3`
        if [ "$jobstatus" == "E" ] || [ "$jobstatus" == "f" ]; then
                #echo "Job $jobname failed ($jobstatus)."
                echo "3"
                exit
        elif [ "$jobstatus" == "W" ]; then
                #echo "Job $jobname with warning ($jobstatus)."
                echo "1"
                exit
        elif [ "$jobstatus" != "T" ] && [ "$jobstatus" != "R" ]; then
                #echo "Job $jobname not ok ($jobstatus)."
                echo "2"
                exit
        elif [ "$jobfiles" == 0 ] || [ "$jobbytes" == 0 ] ; then
                #echo "Job $jobname is empty."
                echo "4"
                exit
        else
                echo "0"
                exit
        fi
done

Скрипт для дискаверинга (LLD) приколочен к формату вывода bconsole и легко может сломаться, но пока работает. И JSON лепится руками, но пока тоже работает.

#!/bin/bash

FIRST=true

JOBS=$(echo "show jobs" | bconsole | grep "^ *Name = |Enabled = no" | sed 'N;/n  Enabled = no/d;P;D' | grep -v -e "-test"$" | cut -d'=' -f 2 | grep -o "[a-zA-Z0-9_-]*" )

echo '{
    "data": ['

for job in $JOBS; do
        if [ "$FIRST" = false ]; then
                echo -n ","
        fi
        FIRST=false
        echo ""
        echo "        {"
        echo "            "{#JOBNAME}": "$job""
        echo -n "        }"
done

echo '
    ]
}'

А ещё я люблю stacked-графики в заббиксе, вот, например, занятый разными заданиями объём пленки:
Bareos: ленты, Hyper-V и ещё всякое - 2
Видно, что синее задание пора поделить на несколько маленьких.

Приятные мелочи

  • С самого начала пользовался командой status storage=, но почему-то не приходило в голову сделать status director. Там обнаружилась удобная сводка (а если watch «echo 'status director' | bconsole», то практически дашборд), своей простотой особенно полезная для операторов-кассетоменятелей.
  • В bconsole работают башевские Ctrl+R (поиск по истории команд), Ctrl+W (удаление слова) и тому подобное.

Будущие свершения

  • Бутстрапы. Если при катастрофе вместе с бизнес-серверами будет потерян директор бареоса, то админа вызовут в кирдычную^W^W^W^W восстановление продакшен-серверов может затянуться. Чтобы быстро восстановить директора, директор бэкапит сам себя и шлёт мне на почту бутстрапы. Но я не знаю, что с ними делать, инструкции не особо внятные, тестить пока не тестил. Поэтому бареос в конце сессии бэкапов ещё тупо бэкапит виртуалку с собой и тут же восстанавливает её на другой сервер (команда крона или где). И ещё есть запасной директор с копией конфигов и репликой постгреса. Но бутстрапы надо будет обязательно освоить.
  • Сделать нормальную миграцию. Есть такая теоретически крутая штука — миграция: скажем, держим бэкапы за сегодня и вчера на дисках, а потом они переезжают на ленты. Но красиво это сейчас не работает. Бареос не станет писать несколько файлов-томов на одну кассету (которая тоже том). Он хочет писать каждый файл на отдельную кассету. Это логично, но крайне непрактично, ведь у нас в файле хранится одно задание, а задания маленькие, помните?
  • Отвязать пул от библиотеки. У меня библиотек две (одна из них двухприводная), и запись в каждый пул привязана к конкретному девайсу (восстанавливать данные можно на любом девайсе, с этим проблем нет). В описании пула можно указать несколько девайсов, тут вопрос времени: нужно сесть, сделать и потестить.
  • Заставить работать lz4 на linux-fd. Что-то собирал отсюда, но с ходу не заработало, пока не разобрался.

Задавайте вопросы, софтина чёткая, хотя и с характером, мне хочется поспособствовать её распространению.

И не забывайте проверять свои бэкапы.

Автор: muon

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js