PHPとrsyncでディレクトリをローテーションバックアップ
通常ならこういったバッチ処理は.sh等のシェルスクリプトで記述するところかと思いますが、個人的に使い慣れたPHPで書いています。
PHPで行なっているのはローテーションさせるためのバックアップ先ディレクトリ名の管理くらいで、バックアップ処理そのものは単純にrsyncに任せています。
使用方法
backup.phpと、バックアップ対象パスなどrsyncへ渡すオプションを設定するbackup_config.phpを同一のディレクトリへ置いて使用します。
ローテーション回数の設定はbackup.php側にあります。
ローテーション回数を2以上に設定すると、--link-destオプションを使用した差分バックアップを行います。初回はフルバックアップになりますが、2回目以降は変化のなかったファイルに対してはハードリンクのみを作成し、変化のあったファイルのみが実体ファイルとしてコピーされますので、ローテーション回数が比較的多めでもディスク消費量やバックアップにかかる時間を必要最小限に抑えることができます。
動作の概要についてはこちらを。
ローテーションを1回に指定した場合は--link-destオプションは使わず、既存のバックアップの中のいちばん新しいものに対して、--deleteオプションを付けて実行します。
ローテーション有り(2回以上)か無し(1回)かのどちらか一方で動作させるだけでも良いですが、backup.phpを例えばbackup_sub.phpという名前でコピーし、backup.php側をローテーション指定で1日1回実行、backup_sub.php側をローテーション無し(1回)指定で例えば1時間おきに実行させることで、1時間おきに最新の状態にミラーリングさせつつ、1日1回履歴バックアップを残すといった使い方もできます。
PHPファイルですが、Webアプリケーションではないのでシェルから実行します。
YYYYMMDD_hhmmss.bak という名前のディレクトリにバックアップ、
YYYYMMDD_hhmmss.log にrsyncのログを保存します。
cronでスケジューリングする場合は以下のようになります。
設定例(毎日午前4時に実行)
0 4 * * * php /スクリプト設置パス/backup.php &> /dev/null
設定例 毎日午前4時に履歴差分バックアップをしつつ、毎正時にミラーリング
(backup.php側でローテーション回数2以上で設定、backup.phpをコピーしたbackup_sub.php側でローテーション回数1を指定している前提です。)
0 4 * * * php /スクリプト設置パス/backup.php &> /dev/null
# sub mirroring
0 * * * * php /スクリプト設置パス/backup_sub.php &> /del/null
設定によっては複数が同時に実行開始される場合がありますが、実行の際にはロックファイルを用いて排他処理しているので、同時に複数のバックアップ処理が回ってしまうことはありません。
また、ローテーション無しでの実行の場合はバックアップ開始まで5秒遅らせるようにしてありますので、同時に走り出してもローテーション有りのほうが優先して実行されるようになっています。
オプションでold_delete.phpとcapacity_check.phpも用意しました。
backup.phpでローテーション回数を多く設定した場合のサポート的な位置付けです。
使用する場合は、どちらもbackup.phpと同じディレクトリへ設置してください。
old_delete.phpは古いバックアップを間引くスクリプトで、1日以上経過したらその日のバックアップの中で最終のものを残して削除、1か月以上経過したらその月のバックアップの中で最終のものを残して削除します。
capacity_check.phpはバックアップディスクの容量をチェックし、指定した割合に達したら下回るまで古いバックアップから順に削除するスクリプトです。
どちらか一方でも、両方設置しても問題ありません。
cronでスケジュール実行する場合は、1日1回程度backup.phpとは時間をずらして実行すればよいかと思います。
設定例 old_delete.phpを毎日5時に、capacity_check.phpを毎日5時5分に実行するよう設定しています。
0 4 * * * php /スクリプト設置パス/backup.php &> /dev/null
# sub mirroring
0 * * * * php /スクリプト設置パス/backup_sub.php &> /del/null
# olddata delete
0 5 * * * php /スクリプト設置パス/old_delete.php &> /dev/null
# diskcapacity check delete
5 5 * * * php /スクリプト設置パス/capacity_check.php &> /dev/null
スクリプト
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 |
<?php /** * rsyncでバックアップ元ディレクトリをバックアップ先ディレクトリへ * 命名規則 YYYYMMDD_hhmmss.bak で指定した世代分ローテーションバックアップ */ $gen = 7; // バックアップのローテーション数 $test = 0; // 0:実行 1:実際には実行せずコマンドのみ表示 /** * */ set_time_limit(0); if(!file_exists(__DIR__. '/backup_config.php')) { print "Configuration file 'backup_conf.php' not found.\n"; exit; } include(__DIR__. '/backup_config.php'); // デリミタ補正 $sourceDir = preg_replace('|/+$|', '/', $sourceDir.'/'); $backupDir = preg_replace('|/+$|', '/', $backupDir.'/'); // ロックファイル名 $lockFilename = (preg_match('/:/', $backupDir) ? __DIR__.'/' : $backupDir). 'backup.lock'; // バックアップ元・バックアップ先が無かったら終了 if(!preg_match('/:/', $sourceDir) && !file_exists($sourceDir) || !preg_match('/:/', $backupDir) && !file_exists($backupDir)) { print "The source '{$sourceDir}' or backup '{$backupDir}' destination directory does not exist.\n"; exit; } // ローテーション無しの場合は5秒遅延(ローテーションありと同時に実行開始された場合にそちらを優先させる) if($gen <= 1) sleep(5); // ロックファイルが存在していたら同名のプロセス実行中なので終了 if(file_exists($lockFilename)) { print "A process with the same name is running.\n"; exit; } else if(!$test) { // 無ければロックファイル作成 file_put_contents($lockFilename, ''); } // 除外指定からexcludeオプション構築 if(isset($excludes) && is_array($excludes)){ foreach($excludes as $k => $v) { if(trim($v) == '') { unset($excludes[$k]); } else { $excludes[$k] = "--exclude='{$v}'"; } } $excludes = implode(' ', $excludes); } else $excludes = ''; // 既にあるバックアップ済みディレクトリ名を取得 $backupList = []; $logList = []; if($dir = opendir($backupDir)) { while($fn = readdir($dir)) { if(preg_match('/^\w+\.bak$/i', $fn)) { $backupList[] = $fn; } elseif(preg_match('/^\w+\.log$/i', $fn)) { $logList[] = $fn; } } closedir($dir); } if($test) print "-- Check that the following command is correct. --\n"; $nowDate = date('Ymd_His'); // 新規バックアップディレクトリ名 $backupName = "{$nowDate}.bak"; // ローテーション外の古いバックアップは削除 $linkDest = ''; if(count($backupList)) { rsort($backupList); if($gen > 1) { $linkDest = "--link-dest={$backupDir}{$backupList[0]}"; } else { // ローテーション無しの場合最新バックアップディレクトリ自体をバックアップ先に指定 $backupName_ = $backupList[0]; } // ローテーション外バックアップの削除(ローテーション無し時は除く) if($gen > 1 && count($backupList) >= $gen) { $delNames = array_slice($backupList, $gen > 1 ? $gen -1 : 1); foreach($delNames as $del) { $command = "rm -rf {$backupDir}{$del}"; print "$command\n"; if(!$test) exec($command); } } } $logName = "{$nowDate}.log"; // ローテーション外のログファイルの削除 rsort($logList); if(!$test && count($logList) >= $gen) { $delNames = array_slice($logList, $gen -1); if($gen > 1) { foreach($delNames as $del) { if(file_exists("{$backupDir}{$del}")) unlink("{$backupDir}{$del}"); } } else { if(file_exists("{$backupDir}{$logList[0]}")) unlink("{$backupDir}{$logList[0]}"); } } // rsyncコマンドでバックアップ $command = implode(" ", [ "rsync -av", $gen <= 1 ? '--delete' : '', $excludes, $otherOptions, $linkDest, $sourceDir, sprintf("%s%s", $backupDir, isset($backupName_) ? $backupName_ : $backupName), "--log-file={$backupDir}{$logName}" ]); print "$command\n"; if(isset($backupName_)) $rename = "mv {$backupDir}{$backupName_} {$backupDir}{$backupName}"; if(!$test) { exec($command); if(isset($rename)) { exec($rename); } chmod("{$backupDir}{$logName}", 0777); // ロックファイル削除 unlink($lockFilename); } if(isset($rename)) print "$rename\n"; exit; |
backup_config.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?php /** * rsyncオプション設定 */ // バックアップ元ディレクトリ $sourceDir = '/var/www/html/source/'; // バックアップ先ディレクトリ $backupDir = '/home/username/backup/'; // 除外指定 例: ['/exclude/', '*.bak']; $excludes = []; // その他のオプション 例: '--update --dirs'; $otherOptions = ''; /** * */ date_default_timezone_set('Asia/Tokyo'); |
old_delete.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 |
<?php /** * 1か月以上経過したものはその月の最終以外のものを、1日以上経過したものはその日の最終以外のものを削除 */ set_time_limit(0); if(!file_exists(__DIR__. '/backup_config.php')) { print "Configuration file 'backup_conf.php' not found.\n"; exit; } include(__DIR__. '/backup_config.php'); // 既にあるバックアップ済みディレクトリ名を取得 $backupList = []; $logList = []; if($dir = opendir($backupDir)) { while($fn = readdir($dir)) { if(preg_match('/^\w+\.bak$/i', $fn)) { $backupList[] = $fn; } } closedir($dir); } $processed = []; foreach($backupList as $backupName) { if(!preg_match('/^(\d{4})(\d\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か月以上経過しているものはその月の最終のもの以外を削除 if(time() >= strtotime("$fDate +1 month")) { $pickup = []; foreach($backupList as $tmp) { if(substr($tmp, 0, 6) == "$year$month" && substr($tmp, 0, 8) <= "$year$month$day") $pickup[] = $tmp; } rsort($pickup); foreach(array_slice($pickup, 1) as $tmp) { deleteBackup($tmp); } } // 1日以上経過しているものはその日の最終のもの以外を削除 elseif(time() >= strtotime("$fDate +1 day")) { $pickup = []; foreach($backupList as $tmp) { if(substr($tmp, 0, 8) == "$year$month$day" && $tmp <= $backupName) $pickup[] = $tmp; } rsort($pickup); foreach(array_slice($pickup, 1) as $tmp) { deleteBackup($tmp); } } } function deleteBackup($str) { global $backupDir; global $processed; if(isset($processed[$str])) return; // 該当ディレクトリ削除 if(file_exists("{$backupDir}{$str}")) { $command = "rm -rf {$backupDir}{$str}"; print"$command\n"; exec($command); $processed[$str] = 1; } // 同じ日時のログファイルがあれば削除 $logFile = $backupDir. preg_replace("/\..*$/", '', $str). '.log'; if(file_exists($logFile)) { unlink($logFile); } } |
capacity_check.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 |
<?php /** * 該当ドライブのディスク使用割合を確認し、指定した割合に達していたら下回るまで古いバックアップから順に削除 * 但し最低1つはバックアップを残す */ // ディスク使用許容割合(%) // 使用量がこの値に達したら下回るまで古いバックアップから順に削除 $threshold = 95; set_time_limit(0); if(!file_exists(__DIR__. '/backup_config.php')) { print "Configuration file 'backup_conf.php' not found.\n"; exit; } include(__DIR__. '/backup_config.php'); // 既にあるバックアップ済みディレクトリ名を取得 $backupList = []; $logList = []; if($dir = opendir($backupDir)) { while($fn = readdir($dir)) { if(preg_match('/^\w+\.bak$/i', $fn)) { $backupList[] = $fn; } } closedir($dir); } sort($backupList); // ディスク使用量が指定割合を下回るまで古いバックアップから削除 while(checkPercentage($threshold) && count($backupList) > 1) { print "delete $backupList[0]\n"; deleteBackup($backupList[0]); array_shift($backupList); } function checkPercentage($n) { global $backupDir; exec("df {$backupDir}", $ret); if(!isset($ret[1])) return false; if(preg_match('/(\d+)\%/', $ret[1], $ret)) { if($ret[1] >= $n) return true; } return false; } function deleteBackup($str) { global $backupDir; // 該当ディレクトリ削除 if(file_exists("{$backupDir}{$str}")) { $command = "rm -rf {$backupDir}{$str}"; exec($command); } // 同じ日時のログファイルがあれば削除 $logFile = $backupDir. preg_replace("/\..*$/", '', $str). '.log'; if(file_exists($logFile)) { unlink($logFile); } } |