[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に作成するのに適しています。