NASのバックアップ対象ディレクトリをinotifyで監視して更新されたディレクトリのみをrsyncでミラーリング

前回[**こちら**](https://qiita.com/akebi_mh/items/56be96a98b73fe940168)で書いたミラーリングと世代管理バックアップの2系統のスクリプトのうち、ミラーリングのほうのスクリプトについてパフォーマンスアップの修正を行ないました。

前回のスクリプトでは大まかな流れとしてduコマンドの結果を元に更新対象ディレクトリを絞り込んでrsyncに渡すという動作を行なっていましたが、監視対象内のサブディレクトリ数が多くなるとduコマンドの実行時間もそれなりにかかるようになるので、この部分をinotifywaitコマンドでの監視に置き換えています。

###inotifywaitを使えるよう準備

inotify-toolsがインストールされていない場合inotifywaitコマンドも使えませんので、その場合はまずinotify-toolsをインストールします。

**inotify-toolsをインストール(Debian系の場合)**

~~~~
$ sudo apt install inotify-tools
~~~~

inotifywaitで監視可能な対象数はデフォルトで8192になっているかと思いますが、NAS全体を監視対象にしようとするとサブディレクトリ数も多くなり8192では不足する場合もままありますので、環境に合わせてこの設定を増やしておきます。

**設定値の確認**

~~~~
$ cat /proc/sys/fs/inotify/max_user_watches
~~~~

例として262144に増やす場合

~~~~
$ sudo sysctl fs.inotify.max_user_watches=262144
~~~~

この設定は再起動すると初期値に戻ってしまいますので、起動時に自動で設定されるようにしておきます。
いくつか方法はありますが、私はrc.localに追記しました。

~~~~
$ sudo nano /etc/rc.local
~~~~

以下を追記

~~~~
sysctl fs.inotify.max_user_watches=262144
~~~~

以降は[**前回**](https://qiita.com/akebi_mh/items/56be96a98b73fe940168#crontab%E8%A8%AD%E5%AE%9A%E4%BE%8B)と同じ方法でmirroring.phpの実行設定をすれば完了です。

###スクリプト

~~~php:mirroring.php
<?php
/**
 *  rsync ミラーリング
 *  inotifiwait監視版
 */

// ミラーリング元ディレクトリ
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);
}

// 各ディレクトリ名のデリミタ補正
$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;
}

// inotifywaitログファイルパス
$inotifyLog = TEMP_DIR. '/inotify.log';

// inotifywaitプロセス管理
$res = inotifywaitProcessManage($sourceDir, $inotifyLog);

// inotifywaitログが空か最終更新からの経過時間が2秒未満なら終了
if(file_exists($inotifyLog) && (filesize($inotifyLog) == 0 || time() - filemtime($inotifyLog) < 2) && !$res) {
    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);
}

// 更新対象ディレクトリ取得
$updateDirList = getUpdataDirList($inotifyLog);
if(!$updateDirList) {
    $updateDirList[] = $sourceDir;
}

// 更新対象ディレクトリに対してrsync実行
foreach($updateDirList as $dir) {
    if(!file_exists($dir)) continue;
    $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;

/**
 *
 */

// inotifywaitログから更新対象ディレクトリ取得
function getUpdataDirList($inotifyLog) {
    $retArr = [];
    if(file_exists($inotifyLog)) {
        $fp = fopen($inotifyLog, 'r+');
        $tmpArr = [];
        while(($l = fgets($fp)) !== false) {
            $l = trim($l);
            if(!$l) continue;
            $l = preg_replace('|/[^/]+$|', '/', $l);
            $tmpArr[$l] = true;
        }
        if($tmpArr) ftruncate($fp, 0);
        fclose($fp);

        $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);
}

// inotifywaitプロセス管理
function inotifywaitProcessManage($sourceDir, $inotifyLog) {
    $command = "inotifywait -mr -o {$inotifyLog} -e create,delete,modify,moved_to,moved_from --format %w {$sourceDir}";

    // 既存inotifywaitプロセス存在チェック
    exec('ps x', $res);
    $pf = 0;
    foreach($res as $tmp) {
        if(strpos($tmp, $command) !== false) $pf++;
    }

    // ログが無いかログ最終更新から1時間以上経過していたら
    // 既存inotifywaitプロセスをkillした上で再度inotifywaitをバックグラウンド実行
    if(!file_exists($inotifyLog) || time() - filemtime($inotifyLog) >= 3600 * 1 || !$pf) {
        // inotifywaitコマンドが見つからなかったら終了
        if(exec('type inotifywait', $o, $r) && $r !== 0) {
            print "Cannot find `inotifywait` command.\n";
            exit;
        }
        exec("pkill -f \"{$command}\"");
        $command .= " &";
        print "$command\n";
        if(file_exists($inotifyLog)) unlink($inotifyLog);
        exec($command);
        chmod($inotifyLog, 0600);
        return true;
    }
    return false;
}
~~~

ディレクトリの更新監視にinotifywaitを利用してはいますが、更新があった場合に自動的にmirroring.phpが実行されるわけではなくmirroring.phpはこれまで通りcronで定期的に実行し、スクリプト内でinotifywaitのログを参照することでその間に更新のあった監視対象内のサブディレクトリを取得してrsyncに渡すという動作になっています。

ここまで書いておいて何ですが、inotifyで監視してrsyncで同期という流れはLsyncdと似たようなことをしているわけなので、人によっては素直にLsyncdを導入したほうが楽かもしれません。