KnowHow

技術的なメモを中心にまとめます。
検索にて調べることができます。

[Thread版:改良1]pythonとbashを用いたLinuxサーバのバックアッププログラム

登録日 :2024/09/22 20:40
カテゴリ :Python基礎

Homeディレクトリに多数のユーザがいる場合、バックアップに時間がかかる。

バックアップサーバとメインサーバ間はインフィニバンドで接続してデータ転送速度が良いため、IOバウンドがボトルネックとなる。そのため、できる限り帯域を効率的に用いるには、Threadなどでバックアップ処理を並列化したほうが良い。

Home領域の増減もあるため、pythonを用いて自動的にhome領域のディレクトリを取得して、Threadでバックアップ処理を実施するプログラムを考えてみた。

フォルダ構成

[root@nis1 backup_script]# ll

-rwxr-xr-x. 1 root root 7452  9 16 18:40 backup_homedir.py
drwxr-xr-x. 4 root root   55  9 16 17:18 config
drwxr-xr-x. 2 root root   66  9 16 17:19 log
drwxr-xr-x. 3 root root   59  9 16 17:48 script

バックアップログラム

まず、バックアップを行うシェル
script/backup_script_args.sh

#!/bin/bash

# 使用方法を表示する関数
usage() {
    echo "Usage: $0 <source_directory>"
    echo "Example: $0 /home/user/data"
}

# バックアップ元とバックアップ先のディレクトリを設定
if [ $# -eq 0 ]; then
    usage
    exit 1
fi

SOURCE_DIR="$1"
BACKUP_DIR="/backup_dir"
FULL_BACKUP_DIR="$BACKUP_DIR/full/$1"
INCREMENTAL_BACKUP_DIR="$BACKUP_DIR/incremental"
#DATE=$(date +%Y%m%d)
DATE=$(date +%Y-%m-%d)
DOW=$(date +%u)  # 1-7 (月-日)

# ディレクトリの存在確認
if [ ! -d "$SOURCE_DIR" ]; then
    echo "Error: Source directory does not exist."
    exit 1
fi

# フルバックアップ(毎週日曜日)
if [ "$DOW" -eq 7 ]; then
    mkdir -p "$FULL_BACKUP_DIR"
    rsync -av --delete \
        --exclude='.cache' \
        --exclude='*.tmp' \
        "$SOURCE_DIR/" "$FULL_BACKUP_DIR/"
    echo "Full backup completed."
fi

# 増分バックアップ(毎日)
INCREMENTAL_DATE_DIR="$INCREMENTAL_BACKUP_DIR/$DATE/$1"
#INCREMENTAL_DATE_DIR="$INCREMENTAL_BACKUP_DIR/$1"
mkdir -p "$INCREMENTAL_DATE_DIR"
rsync -av \
    --link-dest="$FULL_BACKUP_DIR" \
    --exclude='.cache' \
    --exclude='*.tmp' \
    "$SOURCE_DIR/" "$INCREMENTAL_DATE_DIR/"

echo "Incremental backup completed for $DATE."

# 古い増分バックアップの削除(30日以上前)
find "$INCREMENTAL_BACKUP_DIR" -maxdepth 1 -type d -mtime +30 -exec rm -rf {} \;

echo "Old incremental backups removed."


#
#
#このスクリプトの特徴:
#フルバックアップは毎週日曜日に実行され、常に同じディレクトリ($FULL_BACKUP_DIR)を更新します。
#増分バックアップは毎日実行され、日付ごとのディレクトリに保存されます。
#--link-destオプションにより、変更されていないファイルはハードリンクとして保存され、デ ィスク容量を節約します。
#--excludeオプションで不要なファイルやディレクトリを除外し、バックアップサイズを削減し ます。
#30日以上経過した古い増分バックアップは自動的に削除されます。
#このアプローチにより、フルバックアップは常に最新の状態を保ちつつ単一のディレクトリに保存され、
#増分バックアップは日付ごとに管理されます。また、除外オプションとハードリンクの使用により、
#全体的なバックアップサイズを最小限に抑えることができます。
#
#

次にpythonプログラム
backup_homedir.py

#!/usr/bin/python3

from abc import ABC, abstractmethod
import subprocess
from subprocess import PIPE
import queue
import threading
import logging
import time
import datetime
import signal
import os
import sys
import gc
import socket

dir_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(dir_path)

from config import settings

"""
created by Me.
submit shell command
method Thread
version 2024.09.22.1
"""

logging.basicConfig(
        filename=settings.LOG_FILE,
        level=logging.INFO,
        format='%(threadName)s: %(message)s')
logger = logging.getLogger(__name__)

logger.debug({'add path': dir_path})


class ShellCommand(object):
    def __init__(self, dt_now, timeout: int, command: str):
        self.stdout = False
        self.stderr = False
        self.returncode = False
        self.command = False
        self._timeout = timeout
        self._command = command
        self._dt_now = dt_now
        self._command_result = False
        self._errlog = False

    def submit_command(self, command):
        self.command = command
        result = subprocess.run(
                self.command,
                shell=True,
                stdout=PIPE,
                stderr=PIPE,
                timeout=self._timeout)
        self.stdout = result.stdout.decode('utf-8')
        self.stderr = result.stderr.decode('utf-8')
        self.returncode = result.returncode

        if result.returncode != 0:
            raise Exception(self.stderr)

    def execute_command(self):
        try:
            self.submit_command(self._command)
            self._command_result = self.stdout
        except Exception as e:
            self._command_result = self.stderr
            self._errlog = str(e)
            logger.error({
                'time': self._dt_now,
                'status': 'failed',
                'action':'ExceuteShellComand',
                'error': self._errlog,
                'command': self._command})


class FetchHomeDir(object):
    def __init__(self, dt_now, timeout, home):
        self._dt_now = dt_now
        self._timeout = timeout
        self._home = home
        self._status = None
        self._command = 'ls -a ' + home
        self.shell = ShellCommand(dt_now, timeout, self._command)
        self.homedirs = []

    def run_command(self):
        self.shell.execute_command()
        #print({'return command result': self.shell._command_result})
        if not self.shell._errlog and self.shell._command_result != "":
            self._status = 'success'
            homedirs = self.shell._command_result.split('\n')
            for _home in homedirs[2:]:
                # skip '.', '..'
                if _home != "":
                    _path = self._home + '/' + _home
                    #print(_path)
                    self.homedirs.append(_path)
        else:
            self._status = 'failed'
            logger.error({
                'time': self._dt_now,
                'status': self._status,
                'action': FetchHomeDir,
                'command': self._command,
                'home': self._home})

        if settings.DEBUG:
            #print(f'{self._status}: {__file__} from {self._home}')
            print(f'{self._status}: FetchHomeDir from {self._home}')


class IThreadWorker(ABC):
    def __init__(self, dt_now, queue, num_of_thread, timeout):
        self.dt_now = dt_now
        self.queue = queue
        self.num_of_thread = num_of_thread
        self.timeout = timeout
        self.command = None

    def run(self):
        ts = []
        for _ in range(self.num_of_thread):
            t = threading.Thread(target=self.worker)
            t.start()
            ts.append(t)
        [self.queue.put(None) for _ in range(len(ts))]
        [t.join() for t in ts]

    @abstractmethod
    def worker(self):
        logging.debug('start')
        while True:
            item = self.queue.get()
            if item is None:
                break
            print({'thread': item})
            self.some_process()
            self.queue.task_done()
        logging.debug('end')

    def some_process(self):
        pass


class ThreadHomeDirChecker(IThreadWorker):
    def __init__(self, dt_now, queue, num_of_thread, timeout, backup_script):
        super().__init__(dt_now, queue, num_of_thread, timeout)
        #self.command = 'ls -l '
        #self.command = 'sleep 3 || ls -l '
        self.command = backup_script + ' '
        self.result = []

    def worker(self):
        logging.debug('start')
        while True:
            path_dir = self.queue.get()
            if path_dir is None:
                break
            self.check_home_dir(path_dir)
            self.queue.task_done()
        logging.debug('end')

    def check_home_dir(self, path_dir):
        try:
            _command = self.command + path_dir
            _shell = ShellCommand(self.dt_now, self.timeout, _command)
            _shell.execute_command()
            logger.info({
                'date': self.dt_now,
                'status': 'success',
                'path': path_dir,
                'command': self.command,
                'result': _shell._command_result.split('\n')[0],
                })

        except Exception as e:
            print({'command Error': str(e)})
            logger.error({
                'time': self.dt_now,
                'status': 'failed',
                'action': 'ThreadHomeDirChecker',
                'message': str(e),
                'path': path_dir})


"""
test code
"""
def test_shell_command():

    timeout = settings.TIMEOUT
    dt_now = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')

    home = '/home'
    check_home_dir = FetchHomeDir(dt_now, timeout, home)
    check_home_dir.run_command()

    print({'result': check_home_dir.homedirs})


if __name__ == '__main__':

    #test_shell_command()

    home = settings.HOME_DIR
    timeout = settings.TIMEOUT
    threads = settings.THREADING_NUM
    backup_script = settings.BACKUP_SCRIPT

    dt_now = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')
    fetch_home_dir = FetchHomeDir(dt_now, timeout, home)
    homedirs_queue = queue.Queue()
    start = time.time()

    logger.info({'backup start date': dt_now})

    fetch_home_dir.run_command()
    for homedir in fetch_home_dir.homedirs:
        homedirs_queue.put(homedir)

    thread_homedir_checker = ThreadHomeDirChecker(
            dt_now, homedirs_queue, threads, timeout, backup_script)
    thread_homedir_checker.run()

    end = time.time()

    logger.info({'thread action': 'end'})
    print('thread time: {: 4f}\n'.format(end - start))

    logger.info({
        'time': dt_now,
        'action': 'threads',
        'status': 'finished',
        'elapsed time': 'time: {: 4f}'.format(end - start)})


    del fetch_home_dir,thread_homedir_checker,homedirs_queue
    gc.collect()

configファイル
config/settings.py

"""
created by Me
version 2024.9.17.1
Please Change Option
 number of threading -> integer
 Timeout             -> integer
"""

LOG_FILE       = "/root/tools/python/backup_script/log/check_result.log"
BACKUP_SCRIPT  = "/root/tools/python/backup_script/script/backup_script_args.sh"
THREADING_ON   = True
THREADING_NUM  = 4
PROCESSES_NUM  = 4
TIMEOUT        = 172800
DEBUG          = True
TEST_CODE      = False
HOME_DIR       = '/home'

実行ログ

logフォルダに以下のように結果が出力される

[root@nis1 backup_script]# ll log
合計 284
-rw-r--r--. 1 root root 268143  9月 16 18:40 check_result.log

以下のような内容で出力される

[root@nis1 backup_script]# cat log/check_result.log
[root@nis1 backup_script]# cat log/check_result.log
MainThread: {'backup start date': '2024/09/22 20:37:02'}
Thread-1: {'date': '2024/09/22 20:37:02', 'status': 'success', 'path': '/home/n1092', 'command': '/root/tools/python/backup_script/script/backup_script_args.sh ', 'result': 'sending incremental file list'}
Thread-2: {'date': '2024/09/22 20:37:02', 'status': 'success', 'path': '/home/n1093', 'command': '/root/tools/python/backup_script/script/backup_script_args.sh ', 'result': 'sending incremental file list'}
Thread-3: {'date': '2024/09/22 20:37:02', 'status': 'success', 'path': '/home/n1094', 'command': '/root/tools/python/backup_script/script/backup_script_args.sh ', 'result': 'sending incremental file list'}
Thread-4: {'date': '2024/09/22 20:37:02', 'status': 'success', 'path': '/home/n1095', 'command': '/root/tools/python/backup_script/script/backup_script_args.sh ', 'result': 'sending incremental file list'}
Thread-1: {'date': '2024/09/22 20:37:02', 'status': 'success', 'path': '/home/n1096', 'command': '/root/tools/python/backup_script/script/backup_script_args.sh ', 'result': 'sending incremental file list'}
Thread-2: {'date': '2024/09/22 20:37:02', 'status': 'success', 'path': '/home/n1097', 'command': '/root/tools/python/backup_script/script/backup_script_args.sh ', 'result': 'sending incremental file list'}
Thread-3: {'date': '2024/09/22 20:37:02', 'status': 'success', 'path': '/home/n1098', 'command': '/root/tools/python/backup_script/script/backup_script_args.sh ', 'result': 'sending incremental file list'}
Thread-4: {'date': '2024/09/22 20:37:02', 'status': 'success', 'path': '/home/n1099', 'command': '/root/tools/python/backup_script/script/backup_script_args.sh ', 'result': 'sending incremental file list'}
Thread-1: {'date': '2024/09/22 20:37:02', 'status': 'success', 'path': '/home/n1100', 'command': '/root/tools/python/backup_script/script/backup_script_args.sh ', 'result': 'sending incremental file list'}
Thread-2: {'date': '2024/09/22 20:37:02', 'status': 'success', 'path': '/home/user01', 'command': '/root/tools/python/backup_script/script/backup_script_args.sh ', 'result': 'sending incremental file list'}
MainThread: {'thread action': 'end'}
MainThread: {'time': '2024/09/22 20:37:02', 'action': 'threads', 'status': 'finished', 'elapsed time': 'time:  1.785831'}

Apendix

バックアップスクリプトについて

このスクリプト部分は増分バックアップを実行するための重要な手順を示しています。以下に各行の説明を記します:

BACKUP_DIR="$DEST/incrementa/$SOURCE"
増分バックアップの保存先ディレクトリを設定します。
$DESTはバックアップの基本ディレクトリ、incrementaは増分バックアップ用のサブディレクトリ、$SOURCEはバックアップ元のパスを表します。

mkdir -p "$BACKUP_DIR"
増分バックアップ用のディレクトリを作成します。
-pオプションにより、必要な親ディレクトリも同時に作成されます。
LATEST_FULL="$DEST/full/$SOURCE"
最新のフルバックアップのパスを設定します。
これは増分バックアップの基準点となります。

# 増分バックアップの実行
rsync -av --delete --link-dest="$LATEST_FULL" "$SOURCE/" "$BACKUP_DIR"
実際の増分バックアップを実行するrsyncコマンドです。
-a:アーカイブモードで、ファイルの属性を保持します。
-v:詳細な出力を表示します。
--delete:送信元にないファイルを宛先から削除します。
--link-dest="$LATEST_FULL":最新のフルバックアップと変更がないファイルはハードリンクを作成します。
"$SOURCE/":バックアップ元のディレクトリです。
"$BACKUP_DIR":バックアップ先のディレクトリです。

このスクリプトは、効率的な増分バックアップを実現します。最新のフルバックアップと比較して変更されたファイルのみをコピーし、変更のないファイルはハードリンクを作成することで、ストレージ容量を節約しつつ、完全なバックアップを維持します


# フルバックアップの実行
    rsync -av --delete "$SOURCE/" "$BACKUP_DIR"
このコマンドはフルバックアップを実行するためのrsyncコマンドです。各部分について説明します:
rsync: ファイル同期ツールです。
-av:
-a: アーカイブモードを意味し、再帰的にディレクトリをコピーし、シンボリックリンク、パーミッション、タイムスタンプ、所有者、グループ情報を保持します。
-v: 詳細な出力を提供します。
--delete:
このオプションは、送信元($SOURCE)に存在しないファイルやディレクトリを宛先($BACKUP_DIR)から削除します。
これにより、バックアップ先が送信元の正確なコピーになります。
"$SOURCE/":
バックアップ元のディレクトリです。
末尾のスラッシュ(/)は、ディレクトリの内容をコピーすることを意味します。
"$BACKUP_DIR": バックアップ先のディレクトリです。

このコマンドの動作:
$SOURCEディレクトリの内容を$BACKUP_DIRに完全にコピーします。
既存のファイルは更新され、新しいファイルが追加されます。
$SOURCEに存在しないファイルは$BACKUP_DIRから削除されます。
詳細な進行状況が表示されます。

注意点:
--deleteオプションは慎重に使用する必要があります。誤って重要なファイルを削除する可能性があります。
初回実行時や大規模な変更がある場合、処理に時間がかかる可能性があります。
セキュリティのため、重要なデータをバックアップする際は暗号化された通信チャネルの使用を検討してください。
このコマンドは、$SOURCEの完全で正確なバックアップを$BACKUP_DIRに作成するのに適しています。