[Thread版:改良2]pythonとbashを用いたLinuxサーバのバックアッププログラム
| 登録日 | :2024/10/02 04:46 |
|---|---|
| カテゴリ | :Python基礎 |
バックアッププログラムの改良版。ソースディレクトリ、バックアップディレクトリをsettings.pyで指定するようにしました。
スクリプトパスについても、os, sysを使ってパス情報を自動で取得するようにして、初期設定やメンテナンスをしやすくしました。
ーーー
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> <backup_directory>"
echo "Example: $0 /home/user/data /backup_dir"
}
# バックアップ元とバックアップ先のディレクトリを設定
if [ $# -eq 0 ]; then
usage
exit 1
fi
SOURCE_DIR="$1"
BACKUP_DIR="$2"
FULL_BACKUP_DIR="$BACKUP_DIR/full/$1"
INCREMENTAL_BACKUP_DIR="$BACKUP_DIR/incremental"
DATE=$(date +%Y-%m-%d)
DOW=$(date +%u) # 1-7 (月-日)
STATUS="status:"
# ディレクトリの存在確認
if [ ! -d "$SOURCE_DIR" ]; then
echo "Error: Source directory does not exist."
exit 1
fi
# フルバックアップ(毎週日曜日=>7)
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."
STATUS=$STATUS"Full backup completed for $DATE. "
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."
STATUS=$STATUS"Incremental backup completed for $DATE."
# 古い増分バックアップの削除(30日以上前)
find "$INCREMENTAL_BACKUP_DIR" -maxdepth 1 -type d -mtime +30 -exec rm -rf {} \;
echo $STATUS
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 os
import sys
import gc
dir_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(dir_path)
from config import settings
"""
created by Tagawa.
submit shell command
method Thread
version 2024.09.22.1
"""
logging.basicConfig(
filename=settings.LOG_FILE,
level=logging.INFO,
format='%(asctime)s:%(threadName)s:%(levelname)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({
'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({
'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, backup_dir, 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.backup_dir = backup_dir
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 + ' ' + self.backup_dir
_shell = ShellCommand(self.dt_now, self.timeout, _command)
_shell.execute_command()
messages = _shell._command_result.split('\n')
comment = messages[len(messages)-3]
logger.info({
'status': 'success',
'source_dir': path_dir,
'backup_dir': self.backup_dir,
'command': _command,
'result': comment,
})
except Exception as e:
print({'command Error': str(e)})
logger.error({
'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()
#import settings
home = settings.HOME_DIR
backup_dir = settings.BACKUP_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 directory list
fetch_home_dir = FetchHomeDir(dt_now, timeout, home)
homedirs_queue = queue.Queue()
start = time.time()
logger.info({'backup start date': dt_now})
#make queue(thread)
fetch_home_dir.run_command()
for homedir in fetch_home_dir.homedirs:
homedirs_queue.put(homedir)
#start backup script by thread
thread_homedir_checker = ThreadHomeDirChecker(
dt_now, homedirs_queue, backup_dir, 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({
'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
import os
import sys
dir_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(dir_path)
"""
created by Tagawa
version 2024.10.02.1
Please Change Option
number of threading -> integer
Timeout -> integer
"""
LOG_FILE = dir_path + "/log/check_result.log"
BACKUP_SCRIPT = dir_path + "/script/backup_script_args.sh"
THREADING_ON = True
THREADING_NUM = 4
#PROCESSES_NUM = 4
TIMEOUT = 172800
DEBUG = True
TEST_CODE = False
HOME_DIR = '/home'
BACKUP_DIR = '/backup_dir'
実行
プログラムを実行すると、以下のようにsettings.pyで指定したバックアップディレクトリにfull, incrementalが作成されて、そこにそれぞれデータが保存されていく。
[root@nis1 backup_script]# ls /backup_dir/
archive
[root@nis1 backup_script]# ./backup_homedir.py
success: FetchHomeDir from /home
thread time: 1.831041
[root@nis1 backup_script]# ls /backup_dir/
archive full incremental
[root@nis1 backup_script]#
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
2024-10-02 04:56:57,807:MainThread:INFO:{'backup start date': '2024/10/02 04:56:57'}
2024-10-02 04:56:57,888:Thread-1:INFO:{'status': 'success', 'source_dir': '/home/app', 'backup_dir': '/backup_dir', 'command': '/root/tools/python/backup_script/script/backup_script_args.sh /home/app /backup_dir', 'result': 'status:Full backup completed for 2024-10-02. Incremental backup completed for 2024-10-02.'}
2024-10-02 04:56:57,894:Thread-2:INFO:{'status': 'success', 'source_dir': '/home/n1001', 'backup_dir': '/backup_dir', 'command': '/root/tools/python/backup_script/script/backup_script_args.sh /home/n1001 /backup_dir', 'result': 'status:Full backup completed for 2024-10-02. Incremental backup completed for 2024-10-02.'}
2024-10-02 04:56:57,909:Thread-3:INFO:{'status': 'success', 'source_dir': '/home/n1002', 'backup_dir': '/backup_dir', 'command': '/root/tools/python/backup_script/script/backup_script_args.sh /home/n1002 /backup_dir', 'result': 'status:Full backup completed for 2024-10-02. Incremental backup completed for 2024-10-02.'}
2024-10-02 04:56:57,920:Thread-4:INFO:{'status': 'success', 'source_dir': '/home/n1003', 'backup_dir': '/backup_dir', 'command': '/root/tools/python/backup_script/script/backup_script_args.sh /home/n1003 /backup_dir', 'result': 'status:Full backup completed for 2024-10-02. Incremental backup completed for 2024-10-02.'}
2024-10-02 04:56:57,977:Thread-1:INFO:{'status': 'success', 'source_dir': '/home/n1004', 'backup_dir': '/backup_dir', 'command': '/root/tools/python/backup_script/script/backup_script_args.sh /home/n1004 /backup_dir', 'result': 'status:Full backup completed for 2024-10-02. Incremental backup completed for 2024-10-02.'}
2024-10-02 04:56:57,984:Thread-2:INFO:{'status': 'success', 'source_dir': '/home/n1005', 'backup_dir': '/backup_dir', 'command': '/root/tools/python/backup_script/script/backup_script_args.sh /home/n1005 /backup_dir', 'result': 'status:Full backup completed for 2024-10-02. Incremental backup completed for 2024-10-02.'}
2024-10-02 04:56:58,000:Thread-3:INFO:{'status': 'success', 'source_dir': '/home/n1006', 'backup_dir': '/backup_dir', 'command': '/root/tools/python/backup_script/script/backup_script_args.sh /home/n1006 /backup_dir', 'result': 'status:Full backup completed for 2024-10-02. Incremental backup completed for 2024-10-02.'}
2024-10-02 04:56:58,012:Thread-4:INFO:{'status': 'success', 'source_dir': '/home/n1007', 'backup_dir': '/backup_dir', 'command': '/root/tools/python/backup_script/script/backup_script_args.sh /home/n1007 /backup_dir', 'result': 'status:Full backup completed for 2024-10-02. Incremental backup completed for 2024-10-02.'}
2024-10-02 04:56:58,060:Thread-1:INFO:{'status': 'success', 'source_dir': '/home/n1008', 'backup_dir': '/backup_dir', 'command': '/root/tools/python/backup_script/script/backup_script_args.sh /home/n1008 /backup_dir', 'result': 'status:Full backup completed for 2024-10-02. Incremental backup completed for 2024-10-02.'}
2024-10-02 04:56:58,066:Thread-2:INFO:{'status': 'success', 'source_dir': '/home/n1009', 'backup_dir': '/backup_dir', 'command': '/root/tools/python/backup_script/script/backup_script_args.sh /home/n1009 /backup_dir', 'result': 'status:Full backup completed for 2024-10-02. Incremental backup completed for 2024-10-02.'}
2024-10-02 04:56:58,077:Thread-3:INFO:{'status': 'success', 'source_dir': '/home/n1010', 'backup_dir': '/backup_dir', 'command': '/root/tools/python/backup_script/script/backup_script_args.sh /home/n1010 /backup_dir', 'result': 'status:Full backup completed for 2024-10-02. Incremental backup completed for 2024-10-02.'}
2024-10-02 04:56:59,602:MainThread:INFO:{'thread action': 'end'}
2024-10-02 04:56:59,603:MainThread:INFO:{'action': 'threads', 'status': 'finished', 'elapsed time': 'time: 1.795348'}
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に作成するのに適しています。