rsyncでNASのバックアップ(ミラーリングと世代管理バックアップ分離版)
今回もまた前回と同じような内容ですが、しばらく使っていると “こうしたほうが使いやすそう” と感じる部分もいくつか見えてくるのでその都度反映させていたりします。
LinuxディストリビューションとSambaで構築したNASにネットワーク共有用とバックアップ用それぞれ独立したHDDを載せて、以下のような構成で運用しているものに対してミラーリング及び世代管理バックアップを行う前提で記述しています。
ルート直下に
/data/ … ネットワークドライブ用
というディレクトリを作成。
これをマウントポイント
/home/nas/
にマウントしてあり、
/home/nas/data/
をsambaで共有ディレクトリに設定することでNASのネットワークドライブとして使用。
ルート直下に
/data/ … ミラーリング用
/generation/ … 世代管理バックアップ用
というディレクトリを作成。
これをマウントポイント
/home/nas_backup/
にマウントすることで、
/home/nas_backup/data/
を/home/nas/data/のミラーリング先、
/home/nas_backup/generation/
を/home/nas_backup/data/の世代バックアップ先としています。
mirroring.php
NAS共有ディレクトリをバックアップ元として、バックアップ先ドライブに対してrsyncの--deleteオプションを使用したミラーリングを行うスクリプトです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
<?php /** * rsync ミラーリング */ // ミラーリング元ディレクトリ define('SOURCE_DIR', '/home/nas/data/'); // ミラーリング先ディレクトリ define('BACKUP_DIR', '/home/nas_backup/data/'); // その他のrsyncオプション 例: '--exclude=/temp/ --exclude=/*.bak'; define('OTHER_OPTIONS', ''); /** * */ set_time_limit(0); date_default_timezone_set('Asia/Tokyo'); // 一時ファイル保存用ディレクトリ define('TEMP_DIR', (file_exists('/dev/shm/') ? '/dev/shm/.' : '/var/tmp/.'). md5(__DIR__)); if(!file_exists(TEMP_DIR)) { mkdir(TEMP_DIR); chmod(TEMP_DIR, 0700); } $tempFile = TEMP_DIR. '/mirroring.tmp'; $temps = getTmpFile($tempFile); // 各ディレクトリ名のデリミタ補正 $sourceDir = preg_replace('|/+$|', '/', SOURCE_DIR. '/'); $backupDir = preg_replace('|/+$|', '/', BACKUP_DIR. '/'); // バックアップ元・バックアップ先が無かったら終了 if(!file_exists($sourceDir) || strpos($backupDir, ':') === false && !file_exists($backupDir)) { print "The source '{$sourceDir}' or backup '{$backupDir}' destination directory does not exist.\n"; exit; } // バックアップ元ディスク使用量をチェック、前回から変化が無ければ何もせず終了 // 但しリネームや小サイズの更新ではブロックサイズが変化しない場合もあるので // 前回ミラーリングから1時間以上経過している場合はブロックサイズの変化に関わらずミラーリングを行う exec("df {$sourceDir}", $ret); $usedSize = (preg_split('/\s+/', $ret[1]))[2]; $prevUsedSize = isset($temps['prev_used_size']) ? (time() - filemtime($tempFile) < 3600 ? $temps['prev_used_size'] : 0) : 0; if($usedSize == $prevUsedSize) exit; // ロックファイル名 $lockFilename = TEMP_DIR. '/backup.lock'; // ロックファイルが存在していたら同名のプロセス実行中とみなし終了 if(file_exists($lockFilename)) { print "A process with the same name is running.\n"; exit; } else { // ロックファイル作成 if(!@file_put_contents($lockFilename, 'Process is running.')) { print "Could not create `$lockFilename`.\nSet the permissions of the directory `". TEMP_DIR. "` to 0700.\n"; exit; } chmod($lockFilename, 0600); } // tmpファイルに保存する情報更新 // ミラーリングの場合はバックアップ元の使用ブロック数 $temps['prev_used_size'] = $usedSize; setTmpFile($tempFile, $temps); $updateDirList = getUpdataDirList($sourceDir); if(!$updateDirList) { $updateDirList[] = $sourceDir; } foreach($updateDirList as $dir) { $path = str_replace($sourceDir, '', $dir); // rsyncコマンド $command = implode(" ", [ 'rsync -avH', '--delete', OTHER_OPTIONS, '"'. preg_replace('|/+$|', '/', ($sourceDir. $path. '/')). '"', '"'. preg_replace('|/+$|', '/', ($backupDir. $path. '/')). '"', ]); print "$command\n"; exec($command); } // ロックファイル削除 unlink($lockFilename); exit; /** * */ // tmpファイル取得 function getTmpFile($fn) { if(file_exists($fn)) { $tmp = file_get_contents($fn); return(json_decode($tmp, true)); } return []; } // tmpファイル保存 function setTmpFile($fn, $temps) { if(getTmpFile($fn) != json_encode($temps)) { if(!@file_put_contents($fn, json_encode($temps))) { print "Could not create `$fn`.\nSet the permissions of the directory `". TEMP_DIR. "` to 0700.\n"; exit; } chmod($fn, 0600); } } // 更新ディレクトリ取得 function getUpdataDirList($sourceDir) { $duFile = TEMP_DIR. '/prev_du.txt'; $prevDirList = duToArray($duFile); exec("du {$sourceDir} > {$duFile}"); chmod($duFile, 0600); $dirList = duToArray($duFile); $tmpArr = []; foreach($dirList as $k => $v) { if(isset($prevDirList[$k]) && $prevDirList[$k] != $v) $tmpArr[$k] = $v; } unset($prevDirList, $dirList); $retArr = $tmpArr; foreach($tmpArr as $k => $v) { foreach($tmpArr as $k_ => $v_) { if($k == $k_) continue; if(isset($retArr[$k]) && strpos($k_, $k) === 0) unset($retArr[$k]); } } return array_keys($retArr); } // duコマンドの結果を配列に変換 function duToArray($duFile) { $retArr = []; if(file_exists($duFile)) { if($fp = @fopen($duFile, 'r')) { while(($l = fgets($fp)) !== false) { $l = trim($l); if(!$l) continue; $l = explode("\t", $l); $retArr[$l[1]] = $l[0]; } fclose($fp); } } return $retArr; } |
バックアップ元ディスク容量が前回実行時から変化していなければrsyncは行わず終了するようにしてありますので頻繁に実行しても極端に負荷が高くなることは無いとは思いますが、その辺りは環境に合わせて加減してください。
容量チェックはdfコマンドを使用したものでファイル名の変更や小サイズの変更などブロックサイズの変化しない更新は察知できませんので、前回実行から1時間以上経過していたらバックアップ元ディスク容量が変化していなくてもrsyncを実行するようにしています。
define(‘SOURCE_DIR’, ‘/home/nas/data/’);
ミラーリング元となるディレクトリを指定。
define(‘BACKUP_DIR’, ‘/home/nas_backup/data/’);
ミラーリング先となるディレクトリを指定。
こちらは先頭に「ユーザーアカウント@ホスト名:」等を含めたリモートでの指定も可能ですが、cronでの自動実行時にはリモートへのログイン時にパスワード入力待ちが発生しないようパスワード無しでの鍵認証ログインができるよう適宜設定しておく必要があります。
generation.php
ミラーリングされたディレクトリを元に、世代管理用ディレクトリに対してrsyncの--link-destオプションを使用したバックアップを行うスクリプトです。
バックアップ用ドライブがNAS本体とは別のリモートにある場合はこのスクリプトもリモート側へ設置します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 |
<?php /** * rsync 世代バックアップ */ // バックアップ元ディレクトリ define('SOURCE_DIR', '/home/nas_backup/data/'); // バックアップ先ディレクトリ define('BACKUP_DIR', '/home/nas_backup/generation/'); // その他のrsyncオプション 例: '--exclude=/temp/ --exclude=/*.bak'; define('OTHER_OPTIONS', ''); // バックアップ世代数 define('BACKUP_GENERATION', 200); // 古いバックアップを削除するディスク容量閾値(%) // 0の場合はディスク容量のチェックは行いません define('THRESHOLD', 95); /** * */ set_time_limit(0); date_default_timezone_set('Asia/Tokyo'); // 一時ファイル保存用ディレクトリ define('TEMP_DIR', (file_exists('/dev/shm/') ? '/dev/shm/.' : '/var/tmp/.'). md5(__DIR__)); if(!file_exists(TEMP_DIR)) { mkdir(TEMP_DIR); chmod(TEMP_DIR, 0700); } // 各ディレクトリ名のデリミタ補正 $sourceDir = preg_replace('|/+$|', '/', SOURCE_DIR. '/'); $backupDir = preg_replace('|/+$|', '/', BACKUP_DIR. '/'); // バックアップ元・バックアップ先が無かったら終了 if(!file_exists($sourceDir) || !file_exists($backupDir)) { print "The source '{$sourceDir}' or backup '{$backupDir}' destination directory does not exist.\n"; exit; } $nowDate = date('Y-m-d_Hi'); // ロックファイル名 $lockFilename = TEMP_DIR. '/backup.lock'; // ロックファイルが存在していたら同名のプロセス実行中とみなし2分まで待機、その間に開放されなければ終了 $time = time(); while(file_exists($lockFilename)) { sleep(1); if($time + 120 < time()) { print "A process with the same name is running.\n"; exit; } } // ロックファイル作成 if(!@file_put_contents($lockFilename, 'Process is running.')) { print "Could not create `$lockFilename`.\nSet the permissions of the directory `". TEMP_DIR. "` to 0700.\n"; exit; } chmod($lockFilename, 0600); // バックアップ済みディレクトリ名を取得 $backupList = getBackupList($backupDir); // 古いバックアップを間引き $processed = []; foreach($backupList as $backupName) { if(!preg_match('/^(\d{4})-(\d\d)-(\d\d)_(\d\d)(\d\d)/', $backupName, $m) || isset($processed[$backupName])) continue; list($year, $month, $day, $hour, $minute) = array_slice($m, 1); $fDate = "$year-$month-$day $hour:$minute"; // 1か月以上経過しているものはその月の最終のもの以外を削除 if(time() >= strtotime("$fDate +1 month")) { $pickup = []; foreach($backupList as $tmp) { if(substr($tmp, 0, 7) == "{$year}-{$month}" && substr($tmp, 0, 10) <= "{$year}-{$month}-{$day}") $pickup[] = $tmp; } rsort($pickup); foreach(array_slice($pickup, 1) as $tmp) { deleteBackup($backupDir, $tmp, $processed); } } // 1日以上経過しているものはその日の最終のもの以外を削除 elseif(time() >= strtotime("$fDate +1 day")) { $pickup = []; foreach($backupList as $tmp) { if(substr($tmp, 0, 10) == "{$year}-{$month}-{$day}" && $tmp <= $backupName) $pickup[] = $tmp; } rsort($pickup); foreach(array_slice($pickup, 1) as $tmp) { deleteBackup($backupDir, $tmp, $processed); } } } // バックアップ済みディレクトリ名を再取得 $backupList = getBackupList($backupDir); // ディスク使用量が指定割合を下回るまで古いバックアップから削除 sort($backupList); while(THRESHOLD && checkPercentage($backupDir) && count($backupList) > 1) { $command = "rm -rf {$backupDir}{$backupList[0]}"; array_shift($backupList); print "$command\n"; exec($command); } // 既存世代バックアップがある場合 if(count($backupList)) { rsort($backupList); // 保存世代数を超えるバックアップを古いものから削除 if(count($backupList) >= BACKUP_GENERATION) { $delNames = array_slice($backupList, BACKUP_GENERATION -1); foreach($delNames as $del) { $command = "rm -rf {$backupDir}{$del}"; print "$command\n"; exec($command); } } } // 新規バックアップディレクトリ名 $backupName = "{$nowDate}/"; // rsyncコマンド $command = implode(" ", [ "rsync -av", OTHER_OPTIONS, "--link-dest={$sourceDir}", $sourceDir, sprintf("%s%s", $backupDir, $backupName), ]); print "$command\n"; exec($command); // バックアップ済みディレクトリ名を再取得 $backupList = getBackupList($backupDir); // 1世代前のバックアップとの差分でログのみ取得 if(count($backupList) > 1) { rsort($backupList); $command = "rsync -avn --delete --exclude=/_rsync.log {$backupDir}{$backupList[0]}/ {$backupDir}{$backupList[1]}/ > {$backupDir}_rsync.log"; exec($command); exec("mv {$backupDir}_rsync.log {$backupDir}{$backupList[0]}"); } // ロックファイル削除 unlink($lockFilename); exit; /** * */ // 既存バックアップディレクトリ名取得 function getBackupList($backupDir) { $backupList = []; if($dir = opendir($backupDir)) { while($fn = readdir($dir)) { if(preg_match('/^\w{4}-\w{2}-\w{2}_\w{4,6}$/', $fn) && is_dir("{$backupDir}{$fn}")) { $backupList[] = $fn; } } closedir($dir); } return $backupList; } // バックアップ削除 function deleteBackup($backupDir, $str, &$processed) { if(isset($processed[$str])) return; if(file_exists("{$backupDir}{$str}")) { $command = "rm -rf {$backupDir}{$str}"; print"$command\n"; exec($command); $processed[$str] = 1; } } // ディスク使用量チェック function checkPercentage($backupDir) { exec("df {$backupDir}", $ret); if(!isset($ret[1])) return false; if(preg_match('/(\d+)\%/', $ret[1], $ret)) { if($ret[1] >= THRESHOLD) return true; } return false; } |
実行日時を名前としたディレクトリを作成し、その中にその時点のバックアップを残していきます。
rsyncの–link-destオプションを使うことで新規追加や変化のあったファイルのみが実体として保存され、それ以外のファイルはハードリンクが追加されるだけですので、ディスク容量消費や処理時間は増分バックアップと同程度でありながら作成されるバックアップはそれぞれがフルバックアップ相当になるという特徴があります。
1日以上経過したバックアップはその日の最終版のみ残して削除、1か月以上経過したバックアップはその月の最終版を残して削除、THRESHOLDで指定したディスク使用量に達した場合は下回るまで古いバックアップから削除といった処理もこちらのスクリプトで行なっています。
ハードリンクを有効に活用するため–link-destには1世代前のバックアップを指定するのが一般的ですが、今回の場合$sourceDir自身が既にミラーリングされたバックアップの一部なのでこちらを–link-destに指定しています。
こうすることで、容量の節約と同時に処理速度の短縮も図れます。
define(‘SOURCE_DIR’, ‘/home/nas_backup/data/’);
mirroring.phpでミラーリング先となったディレクトリを指定します。
define(‘BACKUP_DIR’, ‘/home/nas_backup/generation/’);
世代管理バックアップ保存先を指定します。
このディレクトリの下に
YYYY-MM-DD_HHMM
形式でディレクトリが作成され、その中に各世代のバックアップが保存されていきます。
rsyncの--link-destオプションを使用し変更のないファイルは実体ではなくハードリンクが作成されますので、必要以上にディスク容量を消費しません。
define(‘BACKUP_GENERATION’, 200);
保存したい世代数を指定します。
世代バックアップ数がこの値を超えたら古いバックアップから削除されますが、間引き処理やディスク容量による削除処理の兼ね合いで、ここで指定した数に達する前に削除が行なわれる場合もあります。
// 0の場合はディスク容量のチェックは行いません
define(‘THRESHOLD’, 95);
dfコマンドでバックアップ先のディスク使用量(%)をチェックし、この値に達していたら値を下回るまで古いバックアップから順に削除を行います。
0では削除処理を行わなくなりますが、バックアップ先の空き容量が無くてもrsyncの実行を抑制する等の処理は行いません。
crontab設定例
* * * * * php /スクリプト設置パス/mirroring.php &> /dev/null
* * * * * sleep 30; php /スクリプト設置パス/mirroring.php &> /dev/null
0 */6 * * * php /スクリプト設置パス/generation.php &> /dev/null
上記の例では前半ブロックで30秒ごとのミラーリングを、後半ブロックで6時間ごとに世代管理バックアップを行なっています。