rsyncで世代バックアップ及びミラーリング
rsyncで世代バックアップやミラーリングを行うためのPHPスクリプトです。
こちらで作成したものと基本的に同内容ですが、各処理ごとに分けていたスクリプトを個人的に扱いやすいよう1つにまとめたものです。
■スクリプト
backup.php
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 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 |
<?php /** * rsync backup */ set_time_limit(0); date_default_timezone_set('Asia/Tokyo'); // configファイル存在チェック if(!file_exists(__DIR__. '/backup_config.php')) { print "Configuration file 'backup_config.php' not found.\n"; exit; } include(__DIR__. '/backup_config.php'); // 一時ファイル保存用ディレクトリを/var/tmp下に作成 define('TEMP_DIR', '/var/tmp/.'. md5(__FILE__)); if(!file_exists(TEMP_DIR)) { mkdir(TEMP_DIR); chmod(TEMP_DIR, 0700); } $tempFile = TEMP_DIR. '/backup.tmp'; $temps = getTmpFile($tempFile); // ローテーション数1以下の場合はミラーリング $mirroring = (BACKUP_GENERATION <= 1) ? 1 : 0; // 前回の世代バックアップ日時から規定時間が経過していない場合はミラーリング if(isset($temps['generation_backup_last']) && strtotime($temps['generation_backup_last']) + GENERATION_BACKUP_INTERVAL * 60 >= time() && (int)(strtotime(date('H:i')) / 60 - 900) % (GENERATION_BACKUP_INTERVAL ? GENERATION_BACKUP_INTERVAL : 1440)) $mirroring = 1; $generationBackupTime = time(); // 各ディレクトリ名のデリミタ補正 $sourceDir = preg_replace('|/+$|', '/', SOURCE_DIR. '/'); $backupDir = preg_replace('|/+$|', '/', BACKUP_DIR. '/'); // ミラーリングの場合はバックアップ元ディスク使用量をチェック、前回から変化が無ければ何もせず終了 if($mirroring) { exec("df {$sourceDir}", $ret); $usedSize = (preg_split('/\s+/', $ret[1]))[2]; $prevUsedSize = isset($temps['prev_used_size']) ? $temps['prev_used_size'] : 0; if($usedSize == $prevUsedSize) exit; } // バックアップ元・バックアップ先が無かったら終了 if(!file_exists($sourceDir) || !file_exists($backupDir)) { print "The source '{$sourceDir}' or backup '{$backupDir}' destination directory does not exist.\n"; 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ファイルに保存する情報更新 if($mirroring) { // ミラーリングの場合はバックアップ元の使用ブロック数 exec("df {$sourceDir}", $ret); $temps['prev_used_size'] = (preg_split('/\s+/', $ret[1]))[2]; } else { // 世代バックアップの場合は最終バックアップ日時 $temps['generation_backup_last'] = date('Y-m-d H:i', $generationBackupTime); } setTmpFile($tempFile, $temps); // バックアップ済みディレクトリ名を取得 $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_splice($m, 1); $fDate = "$year-$month-$day $hour:$minute"; // 1か月以上経過しているものはその月の最終のもの以外を削除 // (+5 minuteは他のバックアップ処理とのタイミングをずらすためのマージン) if(time() >= strtotime("$fDate +1 month +5 minute")) { $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 +5 minute")) { $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); } // 既存バックアップがある場合 $linkDest = ''; if(count($backupList)) { rsort($backupList); if($mirroring) { // ミラーリングの場合最新バックアップディレクトリ自体をバックアップ先に指定 $backupName_ = $backupList[0]; } else { // 世代バックアップの場合既存の最新バックアップディレクトリを--link-destの参照元に指定 $linkDest = "--link-dest=". (preg_match('/^\.+/', $backupDir) ? "../{$backupList[0]}" : "{$backupDir}{$backupList[0]}"); } // 保存世代数を超えるバックアップを古いものから削除 if(!$mirroring && count($backupList) >= BACKUP_GENERATION) { $delNames = array_slice($backupList, $mirroring ? 1 : BACKUP_GENERATION -1); foreach($delNames as $del) { $command = "rm -rf {$backupDir}{$del}"; print "$command\n"; exec($command); } } } // 新規バックアップディレクトリ名 $nowDate = date('Y-m-d_Hi'); $backupName = "{$nowDate}"; $logName = "{$nowDate}.log"; $backupLogDir = '__backup_log'; if($mirroring && file_exists("{$backupDir}{$backupName_}/{$backupLogDir}")) { exec("mv {$backupDir}{$backupName_}/{$backupLogDir} {$backupDir}"); } if(!file_exists("{$backupDir}{$backupLogDir}")) { exec("mkdir {$backupDir}{$backupLogDir}"); } // rsyncコマンドでバックアップ $command = implode(" ", [ "rsync -av", $mirroring ? '--delete' : '', OTHER_OPTIONS, $linkDest, $sourceDir, sprintf("%s%s", $backupDir, isset($backupName_) ? $backupName_ : $backupName), "--log-file={$backupDir}{$backupLogDir}/{$logName}" ]); print "$command\n"; exec($command); // ミラーリングの場合バックアップ先ディレクトリ名を更新 if(MIRRORING_DIR_NAME_UPDATE && isset($backupName_)) { if(!file_exists("{$backupDir}{$backupName}")) { $rename = "mv {$backupDir}{$backupName_} {$backupDir}{$backupName}"; print "$rename\n"; exec($rename); } } else { if(isset($backupName_)) $backupName = $backupName_; } // ログファイル移動 exec("mv {$backupDir}{$backupLogDir} {$backupDir}{$backupName}"); // ロックファイル削除 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; } // 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); } } |
backup_config.php
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 |
<?php /** * rsync backup configuration */ // バックアップ元ディレクトリ define('SOURCE_DIR', '/mnt/nas/'); // バックアップ先ディレクトリ define('BACKUP_DIR', '/mnt/nas_backup/'); // その他のrsyncオプション 例: '--exclude=/temp/ --exclude=/*.bak/'; define('OTHER_OPTIONS', ''); // バックアップ世代数 define('BACKUP_GENERATION', 200); // 世代バックアップ間隔(分) // 0の場合は1日間隔 define('GENERATION_BACKUP_INTERVAL', 360); // ミラーリング時にバックアップ先となったディレクトリ名の更新(0:更新しない 1:更新する) define('MIRRORING_DIR_NAME_UPDATE', 0); // 古いバックアップを削除するディスク容量閾値(%) // 0の場合はディスク容量のチェックは行いません define('THRESHOLD', 95); /** * */ |
■backup_config.phpの設定
define(‘SOURCE_DIR’, ‘/mnt/nas/’);
バックアップ元のディレクトリをフルパスで指定します。
リモートには非対応です。
define(‘BACKUP_DIR’, ‘/mnt/nas_backup/’);
バックアップ元のディレクトリをフルパスで指定します。
リモートには非対応です。
このディレクトリ以下に、バックアップ日時を名前としたサブディレクトリとともにバックアップが行なわれます。
define(‘BACKUP_GENERATION’, 200);
世代バックアップを行いたい場合、残したい世代数を2以上の値を指定します。
1以下にすると世代バックアップはせずに最新のバックアップに対するミラーリングとなります。
後述するバックアップ先容量監視での削除や間引き処理もありますので、この世代数に達していなくても削除が行なわれる場合があります。
間引き処理は以下のものがあります。
1日以上経過したバックアップはその日の最終のもののみ残し、それ以外を削除
1か月以上経過したバックアップはその月の最終のもののみ残し、それ以外を削除
// 0の場合は1日間隔
define(‘GENERATION_BACKUP_INTERVAL’, 360);
世代バックアップを行う間隔を分単位で指定します。
360で6時間間隔、720で12時間間隔になります。
define(‘MIRRORING_DIR_NAME_UPDATE’, 0);
ミラーリング時は既に存在する最新のバックアップに対してミラーリングを行いますが、ミラーリング先としたディレクトリ名を処理完了日時に合わせて変更するかを指定します。
// 0の場合はディスク容量のチェックは行いません
define(‘THRESHOLD’, 95);
バックアップ先のディスクに対して古いバックアップの削除を開始する容量の閾値を%で指定します。
指定した値に達したら、それを下回るまで古いバックアップから順に削除されます。
■設置と実行スケジューリング
backup.phpとbackup_config.phpを同一ディレクトリへ保存し、crontabにて以下の設定を行います。
上記の例では毎分ごとにスクリプトが実行されます。
バックアップ元のディスク使用量に変化が無ければ処理を行わず終了し、排他処理も行い同一プロセスが複数同時に走らないようにもしていますので必要以上の負担はかからないかと思いますが、気になるようでしたら実行間隔を広げてください。
逆に、もっと間隔を短くしたい場合は以下のような指定方法もあります。
* * * * * sleep 30; php /スクリプト設置パス/backup.php &> /dev/null
上記の例では毎分0秒と30秒に処理が実行されます。
実行した時の挙動は初回はフルバックアップ、GENERATION_BACKUP_INTERVAL で指定した間隔ごとに世代バックアップ、それ以外は最新バックアップに対するミラーリングとなります。