前言

本期分享vps重要数据如何备份,以个人为例,我备份的内容是图床和书签导航站,这两个会在使用过程中不断添加数据。那既然是备份,自然不可能再备份到vps本机,所以我使用alist+reclone把谷歌云盘挂载到了vps上,这样谷歌云盘就相当于vps的本地盘,方便互传文件。起初是想使用rsync直接对目录同步备份,但是尝试下来发现特别慢,因为小文件太多了,而且不知道为啥会自动断开,故采用先压缩成zip包再上传到云盘。如何自动化压缩并上传呢,这就需要shell脚本来实现了。shell压缩脚本也大致分两种备份方案,一个就是固定时间压缩上传备份,比如每天或每周的固定时间备份,另一个就是根据要备份的目录是否发生变化再判断是否备份。我使用的是后者,使用两个脚来实现备份,监控脚本会实时监控备份目录是否发生变化,发生变化后会生成标志文件,备份脚本会在每天的凌晨两点去找昨天的标志文件,发现标志文件就开始压缩和上传,备份成功后会删掉标志文件。简单说就是在目录发生变化的第二天凌晨两点会自动备份。这种其实也有丢数据的风险,比如当天目录发生了变化,恰巧你的vps在第二天凌晨两点前又出问题了,比如晚上9点机房失火了,那就会丢失当天的数据。最后,备份方案的选择需要根据自身数据的特性来选择,如果经常使用且很重要还是想办法实时备份。我的数据变动的不频繁,每个月偶尔会上传图片和更新书签,所以此备份方案比较适合。

工作原理说明

  1. 监控系统:

    monitor.sh 使用 inotifywait 持续监控指定目录

    当检测到文件变化时,创建当天的标记文件

    每个项目都有独立的标记文件

  2. 备份系统:

    backup.sh 在凌晨2点运行

    检查每个项目的标记文件

    只备份当天有变化(有标记文件)的项目

    每个项目最多保留3个备份版本

  3. 日志系统:

    所有操作都有日志记录

    方便追踪问题和监控系统状态

准备工作

安装必要的软件包

1
2
3
4
5
6
# 安装 inotify-tools(用于文件监控)
apt-get update
apt-get install inotify-tools

# 安装 zip(用于文件压缩)
apt-get install zip

创建目录结构

1
2
3
# 创建脚本和配置文件目录
mkdir -p /root/data/shell_script/backup
mkdir -p /root/data/shell_script/backup/marks # 用于存放标记文件

部署脚本文件

创建监控配置文件

1
2
# 创建并编辑配置文件
vim /root/data/shell_script/backup/monitor_config.conf

写入以下内容(可自己根据需求修改备份目录):

1
2
3
4
5
6
7
8
9
# 监控配置
MARK_DIR="/root/data/shell_script/backup/marks"
SLEEP_INTERVAL=86400

# 监控目录
WATCH_DIRS=(
["easyimage"]="/root/data/docker_data/easyimage"
["onenav"]="/root/data/docker_data/onenav"
)

部署监控脚本

1
2
# 创建并编辑监控脚本
vim /root/data/shell_script/backup/monitor.sh
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#!/bin/bash

# 颜色配置
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # 无颜色

# 将脚本转入后台运行
if [ "$1" != "background" ]; then
echo -e "${YELLOW}正在后台启动监控脚本...${NC}"
# 检查锁文件,防止启动多个进程
if [ -f "${LOCK_FILE}" ]; then
pid=$(cat "${LOCK_FILE}")
if ps -p "${pid}" > /dev/null 2>&1; then
echo -e "${RED}错误: 监控脚本已经在运行,PID: ${pid}。退出...${NC}"
exit 1
fi
fi
$0 background > /dev/null 2>&1 &
echo -e "${GREEN}监控脚本成功启动! (PID: $!)${NC}"
echo "你可以查看日志: /root/data/shell_script/backup/monitor.log"
exit 0
fi

# 配置部分
CONFIG_FILE="/root/data/shell_script/backup/monitor_config.conf"
LOG_FILE="/root/data/shell_script/backup/monitor.log"
LOCK_FILE="/var/run/backup_monitor.pid"

# 默认配置
MARK_DIR="/root/data/shell_script/backup/marks"
SLEEP_INTERVAL=86400 # 24小时

# 监控目录配置
declare -A WATCH_DIRS=(
["easyimage"]="/root/data/docker_data/easyimage"
["onenav"]="/root/data/docker_data/onenav"
)

# 日志函数
log() {
local level=$1
shift
local message=$@
echo "[$(date +'%Y-%m-%d %H:%M:%S')] [${level}] ${message}" >> "${LOG_FILE}"
}

# 加载配置文件
load_config() {
if [ -f "${CONFIG_FILE}" ]; then
log "信息" "加载配置文件: ${CONFIG_FILE}"
source "${CONFIG_FILE}"
else
log "警告" "配置文件未找到,使用默认配置"
# 创建默认配置文件
cat > "${CONFIG_FILE}" << EOF
# 监控配置
MARK_DIR="/root/data/shell_script/backup/marks"
SLEEP_INTERVAL=86400

# 监控目录
WATCH_DIRS=(
["easyimage"]="/root/data/docker_data/easyimage"
["onenav"]="/root/data/docker_data/onenav"
)
EOF
fi
}

# 检查是否已运行
check_running() {
if [ -f "${LOCK_FILE}" ]; then
pid=$(cat "${LOCK_FILE}")
if ps -p "${pid}" > /dev/null 2>&1; then
log "错误" "监控脚本已经在运行,PID: ${pid}"
echo -e "${RED}错误: 监控脚本已经在运行,PID: ${pid}。退出...${NC}"
exit 1
else
log "警告" "发现过期的锁文件,正在移除"
rm -f "${LOCK_FILE}"
fi
fi
echo $$ > "${LOCK_FILE}"
}

# 清理函数
cleanup() {
log "信息" "清理并退出"
rm -f "${LOCK_FILE}"
pkill -P $$ # 终止所有子进程
exit 0
}

# 监控单个目录
monitor_directory() {
local project_name=$1
local dir=$2

# 检查目录是否存在
if [ ! -d "${dir}" ]; then
log "错误" "项目 ${project_name} 的目录 ${dir} 不存在"
return 1
fi

log "信息" "开始监控项目 ${project_name},目录: ${dir}"

# 启动单个 inotifywait 进程
inotifywait -m -r -e modify,create,delete,move "${dir}" 2>> "${LOG_FILE}" | while read -r directory events filename; do
local TODAY=$(date +"%Y-%m-%d")
local MARK_FILE="${MARK_DIR}/${project_name}_${TODAY}.mark"

if [ ! -f "${MARK_FILE}" ]; then
touch "${MARK_FILE}"
log "信息" "检测到 ${project_name} 的变化,创建标记文件: ${MARK_FILE}"
fi
done &
}

# 主函数
main() {
# 设置退出时的清理
trap cleanup SIGTERM SIGINT

# 检查必要的工具
if ! command -v inotifywait &> /dev/null; then
log "错误" "inotify-tools 未安装,请先安装它。"
echo -e "${RED}错误: inotify-tools 未安装,请先安装它。${NC}"
exit 1
fi

# 创建必要的目录
mkdir -p "${MARK_DIR}"

# 加载配置
load_config

# 检查是否已运行
check_running

log "信息" "监控脚本已启动"

# 启动所有目录的监控
for project in "${!WATCH_DIRS[@]}"; do
monitor_directory "${project}" "${WATCH_DIRS[${project}]}"
log "信息" "已启动监控进程,项目: ${project}, 目录: ${WATCH_DIRS[${project}]}" # 记录启动的项目
done

# 保持主进程运行
wait
}

# 运行主函数
main

监控日志:

监控日志

部署备份脚本

1
2
# 创建并编辑备份脚本
vim /root/data/shell_script/backup/backup.sh
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
#!/bin/bash

# 日志文件定义
LOG_FILE="/root/data/shell_script/backup/backup.log"

# 重定向标准输出和错误输出到日志文件
exec >>"${LOG_FILE}" 2>&1

# 打印时间戳以标记任务开始
echo "==================== $(date +"%Y-%m-%d %H:%M:%S") ===================="

# 定义备份函数
backup_directory() {
local SOURCE_DIR=$1
local BACKUP_DIR=$2
local PROJECT_NAME=$3
local DATE=$(date -d "-1 day" +"%Y-%m-%d")
local ZIP_FILE="/tmp/${DATE}_${PROJECT_NAME}.zip"
local MARK_FILE="/root/data/shell_script/backup/marks/${PROJECT_NAME}_${DATE}.mark"

# 检查是否存在此项目的标记文件
if [ ! -f "${MARK_FILE}" ]; then
echo "No changes detected for ${PROJECT_NAME} today, skipping backup."
return 0
fi

echo "Starting backup for ${PROJECT_NAME}..."

# 检查源目录是否存在
if [ ! -d "${SOURCE_DIR}" ]; then
echo "Error: Source directory ${SOURCE_DIR} does not exist. Skipping."
return 1
fi

# 检查备份目录是否可写
if [ ! -d "${BACKUP_DIR}" ] || [ ! -w "${BACKUP_DIR}" ]; then
echo "Error: Backup directory ${BACKUP_DIR} is not writable or does not exist. Skipping."
return 1
fi

# 检查是否存在同名文件并处理
if [ -f "${BACKUP_DIR}/$(basename ${ZIP_FILE})" ]; then
echo "Warning: A backup file with the same name already exists in ${BACKUP_DIR}. Renaming the new backup."
ZIP_FILE="/tmp/${DATE}_${PROJECT_NAME}_$(date +"%H%M%S").zip"
echo "New backup file name: ${ZIP_FILE}"
fi

# 压缩文件
echo "Compressing ${SOURCE_DIR} to ${ZIP_FILE}..."
zip -r "${ZIP_FILE}" "${SOURCE_DIR}" > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Error: Compression failed for ${PROJECT_NAME}. Skipping."
return 1
fi
echo "Compression completed: ${ZIP_FILE}"

# 移动到目标备份目录
echo "Moving ${ZIP_FILE} to ${BACKUP_DIR}..."
mv "${ZIP_FILE}" "${BACKUP_DIR}"
if [ $? -ne 0 ]; then
echo "Error: Failed to move file to ${BACKUP_DIR}. Skipping."
return 1
fi
echo "File moved successfully."

# 维护备份文件数量(最多保留3份)
echo "Maintaining backup files in ${BACKUP_DIR}..."
cd "${BACKUP_DIR}" || return 1
BACKUP_COUNT=$(ls -1 *_${PROJECT_NAME}.zip 2>/dev/null | wc -l)

if [ "${BACKUP_COUNT}" -gt 3 ]; then
echo "Deleting old backup files..."
ls -1t *_${PROJECT_NAME}.zip | tail -n +4 | xargs rm -f
if [ $? -eq 0 ]; then
echo "Old backups deleted successfully."
else
echo "Error: Failed to delete old backups."
fi
else
echo "No old backups to delete. Current count: ${BACKUP_COUNT}"
fi

# 备份完成后删除标记文件
rm -f "${MARK_FILE}"
echo "Removed mark file for ${PROJECT_NAME}"

echo "Backup completed for ${PROJECT_NAME}"
echo "----------------------------------------"
}

# 执行 easyimage 备份
backup_directory "/root/data/docker_data/easyimage" "/mnt/alist/Google/backup/easyimage" "easyimage"

# 执行 onenav 备份
backup_directory "/root/data/docker_data/onenav" "/mnt/alist/Google/backup/onenav" "onenav"

# 记录任务完成时间
echo "All backups completed at $(date +"%Y-%m-%d %H:%M:%S")."

backup.sh如何自定义使用:

只需要修改backup_directory函数的参数,一共三个参数,以easyimage备份为例。源目录(/root/data/docker_data/easyimage),目标目录(/mnt/alist/Google/backup/easyimage),和备份项目名称(easyimage)。

1
2
# 执行 easyimage 备份
backup_directory "/root/data/docker_data/easyimage" "/mnt/alist/Google/backup/easyimage" "easyimage"

设置脚本权限

1
2
3
# 设置可执行权限
chmod +x /root/data/shell_script/backup/monitor.sh
chmod +x /root/data/shell_script/backup/backup.sh

运行起来后备份脚本日志:

备份日志

在凌晨两点准时上传前一天的备份

云盘备份结果

配置自动启动

配置监控脚本开机自启

1
2
# 编辑 crontab
crontab -e

添加以下内容:

1
@reboot /root/data/shell_script/backup/monitor.sh

配置备份脚本定时执行

在同一个 crontab 中添加:

1
0 2 * * * /root/data/shell_script/backup/backup.sh

说明:0 2 * 表示每天凌晨2点执行,这个时间点系统负载通常较低。

日志文件

系统会自动创建两个日志文件:

监控日志:/root/data/shell_script/backup/monitor.log

备份日志:/root/data/shell_script/backup/backup.log

验证部署

启动监控

1
2
# 手动启动监控脚本
/root/data/shell_script/backup/monitor.sh

检查监控状态

1
2
3
4
5
6
# 查看监控进程是否运行
ps aux | grep monitor.sh
# 查看监控日志
tail -f /root/data/shell_script/backup/monitor.log
#停止监控
pkill -f monitor.sh

添加或修改备份

如需添加或修改备份项目:

  • 编辑 monitor_config.conf 添加或修改备份目录

  • 编辑 backup.sh 添加或修改备份目录

  • 重启 monitor.sh