备份进程与磁盘
# 第 8 章 备份进程与磁盘
# 目录介绍
# 8.1 备份恢复
# 8.1.1 tar 压缩归档——最基础的备份方式
tar 把多个文件和目录打包成一个归档文件,配合压缩算法减少体积:
#!/bin/bash
# ===== tar 基础命令 ===
# 打包 = c (create),解包 = x (extract),查看 = t (list)
# 打包(不压缩)
tar -cvf backup.tar /path/to/dir # c=创建 v=显示过程 f=指定文件名
# 打包 + gzip 压缩(.tar.gz / .tgz)
tar -czvf backup.tar.gz /path/to/dir # z = gzip(最通用)
# 打包 + bzip2 压缩(.tar.bz2)——更小但更慢
tar -cjvf backup.tar.bz2 /path/to/dir # j = bzip2
# 打包 + xz 压缩(.tar.xz)——最小但最慢
tar -cJvf backup.tar.xz /path/to/dir # J = xz
# ===== 解压 =====
tar -xzvf backup.tar.gz # 解压到当前目录
tar -xzvf backup.tar.gz -C /restore/path # 解压到指定目录
tar -xjvf backup.tar.bz2 # 解压 bzip2
tar -xJvf backup.tar.xz # 解压 xz
# ===== 查看压缩包内容(不解压)=====
tar -tzvf backup.tar.gz # 列出所有文件
tar -tzvf backup.tar.gz | grep ".conf" # 查找特定文件
# ===== 常用选项 =====
# --exclude 排除某些文件/目录
tar -czvf backup.tar.gz /app --exclude="*.log" --exclude="node_modules"
# --exclude-from 从文件读取排除列表
tar -czvf backup.tar.gz /app --exclude-from=exclude.txt
# -C 切换工作目录后再打包(去掉路径前缀)
tar -czvf backup.tar.gz -C /path/to parent_dir
# --strip-components=N 解压时去掉前 N 层目录
tar -xzvf backup.tar.gz --strip-components=1
# ===== 技巧:打包时保留权限 =====
tar -czvpf backup.tar.gz /etc # p = 保留文件权限/所有者
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
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
压缩算法对比:
| 压缩格式 | 选项 | 速度 | 压缩率 | 场景 |
|---|---|---|---|---|
.tar (无压缩) | 无 | 最快 | 无 | 本地快速打包、管道传输 |
.tar.gz | -z | 快 | 中 | 日常备份首选 |
.tar.bz2 | -j | 中等 | 较好 | 大文件备份 |
.tar.xz | -J | 慢 | 最好 | 归档长期存储 |
# 8.1.2 全量备份与增量/差异备份策略
#!/bin/bash
# ===== 全量备份 (Full Backup) =====
# 每次备份全部数据——简单但慢、占空间
full_backup() {
local src="$1"
local dest="$2"
local timestamp=$(date '+%Y%m%d_%H%M%S')
tar -czf "${dest}/backup_full_${timestamp}.tar.gz" "$src"
echo "全量备份完成: ${dest}/backup_full_${timestamp}.tar.gz"
}
# ===== 增量备份 (Incremental Backup) =====
# 只备份自上次备份以来「变化」的文件
# tar 使用 --listed-incremental 快照文件追踪变化
incremental_backup() {
local src="$1"
local dest="$2"
local timestamp=$(date '+%Y%m%d_%H%M%S')
local snapshot="/var/backups/snapshot.snar"
# 第一次:创建全量备份 + 快照文件
if [[ ! -f "$snapshot" ]]; then
tar --listed-incremental="$snapshot" -czf "${dest}/backup_full_${timestamp}.tar.gz" "$src"
echo "初始全量备份(含快照): ${dest}/backup_full_${timestamp}.tar.gz"
else
tar --listed-incremental="$snapshot" -czf "${dest}/backup_incr_${timestamp}.tar.gz" "$src"
echo "增量备份: ${dest}/backup_incr_${timestamp}.tar.gz"
fi
}
# 恢复时:先解压全量,再按时间顺序依次解压每个增量
# ===== 差异备份 (Differential Backup) =====
# 备份自上次「全量备份」以来变化的所有文件
# 用 find -newer 实现
diff_backup() {
local src="$1"
local dest="$2"
local last_full="${3}" # 指向全量备份的参考文件
find "$src" -type f -newer "$last_full" -print0 \
| tar --null -T - -czf "${dest}/backup_diff_$(date '+%Y%m%d').tar.gz"
}
# 恢复时:只需解压全量 + 最近的差异备份
# ===== 实战:7 天轮转备份策略 =====
# 每周日全量备份 + 周一到周六差异备份
rotation_backup() {
local src="$1"
local dest="$2"
local day_of_week=$(date +%u) # 1=周一 ... 7=周日
if [[ "$day_of_week" -eq 7 ]]; then
# 周日:全量备份
full_backup "$src" "$dest"
# 删除 30 天前的全量备份
find "$dest" -name "backup_full_*" -mtime +30 -delete
else
# 周一到周六:差异备份
local latest_full=$(ls -t "$dest"/backup_full_*.tar.gz 2>/dev/null | head -1)
if [[ -n "$latest_full" ]]; then
diff_backup "$src" "$dest" "$latest_full"
else
full_backup "$src" "$dest" # 没有全量就先做一次
fi
fi
}
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
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
# 8.1.3 rsync 远程同步——增量备份利器
rsync 是增量同步之王——只传输变化的文件部分:
#!/bin/bash
# ===== rsync 基础语法 =====
# rsync [选项] 源 目标
# 本地同步(类似 cp -r 但更强)
rsync -av /source/dir/ /backup/dir/ # a=归档模式 rlptgoD v=详细输出
# 注意:源路径末尾 / 的含义:
# /source/dir/ → 同步目录「内容」
# /source/dir → 同步目录「本身」(在目标创建 dir/)
# ===== 核心选项速查 =====
rsync -avz --progress /src/ /dst/ # z=压缩传输 progress=进度条
rsync -av --delete /src/ /dst/ # delete=目标多出的文件也删除(镜像)
rsync -av --exclude="*.log" /src/ /dst/ # 排除匹配文件
rsync -av --exclude-from=exclude.txt /src/ /dst/
rsync -avn /src/ /dst/ # n=dry-run 只预览不执行
rsync -av --max-size=100M /src/ /dst/ # 跳过 >100MB 文件
rsync -av --bwlimit=1000 /src/ /dst/ # 限速 1000KB/s(避免占满带宽)
# ===== 远程同步(通过 SSH)=====
rsync -avz /local/dir/ user@remote:/backup/dir/ # 本地→远程
rsync -avz user@remote:/backup/dir/ /local/restore/ # 远程→本地
rsync -avz -e "ssh -p 2222" /src/ user@host:/dst/ # 指定 SSH 端口
# ===== 增量同步原理 =====
# rsync 对比源和目标的时间戳+大小,只传输变化的块(不是整个文件)
# 大文件只改了 1KB → 只传 1KB,不是重新传整个文件
# ===== 实战:网站代码同步部署 =====
deploy_code() {
local src="./dist/"
local server="user@prod-server:/var/www/html/"
# 1. dry-run 预览
echo "=== 预览变更 ==="
rsync -avn --delete "$src" "$server"
# 2. 确认后执行
read -p "确认部署?(yes/no): " confirm
if [[ "$confirm" == "yes" ]]; then
rsync -avz --delete --exclude=".git" "$src" "$server"
echo "部署完成"
fi
}
# ===== 实战:定时备份到远程服务器 =====
# crontab: 0 2 * * * /path/to/rsync_backup.sh
cat > rsync_backup.sh << 'SCRIPT'
#!/bin/bash
SRC="/data/"
DST="backup@backup-server:/backups/$(hostname)/"
LOG="/var/log/rsync_backup.log"
echo "[$(date)] 开始备份..." >> "$LOG"
rsync -avz --delete \
--exclude="*.log" \
--exclude="node_modules" \
--exclude=".git" \
"$SRC" "$DST" >> "$LOG" 2>&1
echo "[$(date)] 备份结束,退出码: $?" >> "$LOG"
SCRIPT
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
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
# 8.1.4 数据库备份——MySQL/PostgreSQL
#!/bin/bash
# ===== MySQL / MariaDB 备份 =====
# 使用 mysqldump 导出 SQL 文件
mysql_backup() {
local db_name="$1"
local backup_dir="${2:-/backup/mysql}"
local timestamp=$(date '+%Y%m%d_%H%M%S')
mkdir -p "$backup_dir"
# 单库备份
mysqldump -u root -p"$MYSQL_PWD" \
--single-transaction \ # InnoDB 一致性备份(不锁表)
--routines \ # 导出存储过程
--triggers \ # 导出触发器
--events \ # 导出事件调度器
"$db_name" | gzip > "${backup_dir}/${db_name}_${timestamp}.sql.gz"
echo "MySQL 备份完成: ${db_name}_${timestamp}.sql.gz"
}
# 全库备份
mysql_backup_all() {
local backup_dir="${1:-/backup/mysql}"
local timestamp=$(date '+%Y%m%d_%H%M%S')
mysqldump -u root -p"$MYSQL_PWD" \
--single-transaction \
--all-databases | gzip > "${backup_dir}/all_dbs_${timestamp}.sql.gz"
echo "全库备份完成: all_dbs_${timestamp}.sql.gz"
}
# 恢复 MySQL
mysql_restore() {
local backup_file="$1"
local db_name="$2"
if [[ "$backup_file" == *.gz ]]; then
gunzip < "$backup_file" | mysql -u root -p"$MYSQL_PWD" "$db_name"
else
mysql -u root -p"$MYSQL_PWD" "$db_name" < "$backup_file"
fi
echo "MySQL 恢复完成: $db_name"
}
# ===== PostgreSQL 备份 =====
# pg_dump 导出单库,pg_dumpall 导出全库
pg_backup() {
local db_name="$1"
local backup_dir="${2:-/backup/postgres}"
local timestamp=$(date '+%Y%m%d_%H%M%S')
mkdir -p "$backup_dir"
# 单库备份(自定义格式,支持并行恢复)
PGPASSWORD="$PG_PASSWORD" pg_dump \
-h localhost \
-U postgres \
-Fc \ # 自定义压缩格式
-j 4 \ # 并行(4 线程)
"$db_name" > "${backup_dir}/${db_name}_${timestamp}.dump"
# SQL 文本格式(方便直接阅读/编辑)
PGPASSWORD="$PG_PASSWORD" pg_dump \
-h localhost -U postgres \
--inserts \ # SQL INSERT 语句
"$db_name" | gzip > "${backup_dir}/${db_name}_${timestamp}.sql.gz"
echo "PG 备份完成: ${db_name}_${timestamp}.dump"
}
# 恢复 PostgreSQL
pg_restore_backup() {
local backup_file="$1"
local db_name="$2"
pg_restore -h localhost -U postgres -d "$db_name" -j 4 "$backup_file"
}
# ===== 小型数据库快速备份(SQLite)=====
sqlite_backup() {
local db_path="$1"
local backup_dir="${2:-/backup/sqlite}"
local timestamp=$(date '+%Y%m%d_%H%M%S')
sqlite3 "$db_path" ".backup ${backup_dir}/$(basename $db_path)_${timestamp}.db"
}
# ⚠️ SQLite .backup 是原子性的,适合在服务运行时安全备份
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
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
数据库备份安全提醒:
# ✅ 密码不要写在命令行(会被 ps 看到)
# 方式 1:配置文件
cat > ~/.my.cnf << 'EOF'
[mysqldump]
user=root
password=your_password
EOF
chmod 600 ~/.my.cnf
mysqldump --defaults-file=~/.my.cnf db_name | gzip > backup.sql.gz
# 方式 2:环境变量
export MYSQL_PWD="your_password"
mysqldump -u root db_name | gzip > backup.sql.gz
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
# 8.1.5 远程备份——scp/sftp/rsync over SSH
#!/bin/bash
# ===== scp——安全复制(基于 SSH)=====
# 上传
scp backup.tar.gz user@remote:/backups/
scp -P 2222 backup.tar.gz user@remote:/backups/ # 指定端口
scp -r /data/config/ user@remote:/backups/config/ # 递归复制目录
# 下载
scp user@remote:/backups/db_20250601.sql.gz ./
scp -i ~/.ssh/backup_key user@remote:/backups/*.gz ./ # 指定私钥
# 限速(KB/s)
scp -l 8192 large_file.tar.gz user@remote:/backups/ # 限速 8MB/s
# ===== sftp——交互式文件传输 =====
# 命令行模式
echo "put backup.tar.gz /backups/" | sftp user@remote
# 脚本批量上传
sftp_batch_upload() {
local host="$1"
local src_dir="$2"
local dst_dir="$3"
sftp -b - "$host" << SFTP
cd $dst_dir
lcd $src_dir
put *.tar.gz
put *.sql.gz
bye
SFTP
}
# ===== rsync over SSH——生产推荐 =====
# 结合 rsync 的增量优势和 SSH 的安全加密
rsync -avz -e ssh /backups/ user@remote:/backups/
# 等同于上面,但 -e ssh 是默认行为
# ===== 实战:跨服务器自动备份脚本 =====
cat > remote_backup.sh << 'SCRIPT'
#!/bin/bash
# 将本地数据目录备份到远程服务器
LOCAL_DIR="/data"
REMOTE_HOST="backup@backup-server"
REMOTE_DIR="/backups/$(hostname)"
LOG="/var/log/remote_backup.log"
LOCK="/tmp/remote_backup.lock"
# 防重入
exec 200>"$LOCK"
flock -n 200 || { echo "[$(date)] 已有备份进程运行中" >> "$LOG"; exit 1; }
echo "[$(date)] ===== 远程备份开始 =====" >> "$LOG"
# 1. 使用 rsync 增量同步
rsync -avz --delete \
--exclude="*.log" \
--exclude="cache/" \
--exclude="tmp/" \
-e "ssh -i /root/.ssh/backup_key" \
"$LOCAL_DIR/" "${REMOTE_HOST}:${REMOTE_DIR}/" >> "$LOG" 2>&1
exit_code=$?
# 2. 检查结果
if [[ "$exit_code" -eq 0 ]]; then
echo "[$(date)] 备份成功" >> "$LOG"
else
echo "[$(date)] 备份失败!退出码: $exit_code" >> "$LOG"
# 发送告警
echo "远程备份失败 (退出码$exit_code)" | mail -s "备份告警" admin@example.com
fi
SCRIPT
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
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
# 8.1.6 定时备份 cron + 自动清理过期备份
#!/bin/bash
# ===== cron 表达式速查 =====
# 分 时 日 月 周 命令
# * * * * * 每分钟
# 0 * * * * 每小时整点
# 0 2 * * * 每天凌晨 2 点
# 0 2 * * 0 每周日凌晨 2 点
# 0 2 1 * * 每月 1 号凌晨 2 点
# */5 * * * * 每 5 分钟
# 0 8-18 * * * 每天 8~18 点每小时
# ===== 编辑 crontab =====
# crontab -e 编辑当前用户定时任务
# crontab -l 查看
# crontab -u username -e 编辑指定用户
# ===== 备份 crontab 模板 =====
# 每天凌晨 2 点:数据库备份
# 0 2 * * * /backup/scripts/db_backup.sh >> /backup/logs/db_backup.log 2>&1
# 每天凌晨 3 点:文件全量备份
# 0 3 * * * /backup/scripts/file_backup.sh >> /backup/logs/file_backup.log 2>&1
# 每 4 小时:rsync 增量同步
# 0 */4 * * * /backup/scripts/rsync_sync.sh
# 每周日 4 点:清理 30 天前的旧备份
# 0 4 * * 0 /backup/scripts/cleanup_old.sh
# ===== 自动清理过期备份脚本 =====
cat > cleanup_backups.sh << 'SCRIPT'
#!/bin/bash
BACKUP_DIR="/backups"
RETENTION_DAYS=30 # 全量保留 30 天
INCR_RETENTION=7 # 增量保留 7 天
echo "[$(date)] ===== 备份清理开始 ====="
# 清理老旧全量备份
echo "清理 ${RETENTION_DAYS} 天前的全量备份..."
find "$BACKUP_DIR" -name "backup_full_*.tar.gz" -mtime +$RETENTION_DAYS -print -delete
# 清理旧增量备份
echo "清理 ${INCR_RETENTION} 天前的增量备份..."
find "$BACKUP_DIR" -name "backup_incr_*.tar.gz" -mtime +$INCR_RETENTION -print -delete
# 清理旧数据库备份
echo "清理旧数据库备份..."
find "$BACKUP_DIR/mysql" -name "*.sql.gz" -mtime +$RETENTION_DAYS -print -delete
find "$BACKUP_DIR/postgres" -name "*.dump" -mtime +$RETENTION_DAYS -print -delete
# 如果磁盘使用率 > 80%,额外清理最近 7 天的日志备份
disk_usage=$(df "$BACKUP_DIR" | awk 'NR==2 {gsub(/%/,"",$5); print $5}')
if [[ "$disk_usage" -ge 80 ]]; then
echo "磁盘使用率 ${disk_usage}%,额外清理..."
find "$BACKUP_DIR" -name "*.log.gz" -mtime +7 -delete
fi
echo "[$(date)] ===== 备份清理结束 ====="
SCRIPT
# ===== crontab 调试技巧 =====
# 1. 先手动执行脚本确保无误
# 2. 用绝对路径(crontab 的 PATH 很短)
# 3. 重定向输出到日志文件
# 4. 在脚本开头 source /etc/profile
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
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
# 8.1.7 备份实战:通用备份框架脚本
#!/bin/bash
# ============================================
# 通用备份框架
# 配置备份源和目标后,放入 crontab 即可运行
# ============================================
set -euo pipefail
# ---- 配置区 ----
BACKUP_ROOT="/backups"
RETENTION_FULL=30
RETENTION_INCR=7
LOG_FILE="/var/log/backup_framework.log"
# 备份任务配置(数组:名称|源路径|类型)
BACKUP_TASKS=(
"nginx_conf|/etc/nginx|tar.gz"
"app_code|/opt/app|rsync"
"mysql_db|all|mysql"
"website|/var/www/html|tar.gz"
)
# ---- 工具函数 ----
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"; echo "$*"; }
backup_tar() {
local name="$1" src="$2" dest="$3"
local ts=$(date '+%Y%m%d_%H%M%S')
local file="${dest}/${name}_${ts}.tar.gz"
tar -czf "$file" -C "$(dirname "$src")" "$(basename "$src")"
log " TAR: $file ($(du -h "$file" | cut -f1))"
}
backup_rsync() {
local name="$1" src="$2" dest="$3"
rsync -az --delete "$src/" "$dest/${name}/"
log " RSYNC: $src → $dest/${name}/"
}
backup_mysql_() {
local name="$1" db="$2" dest="$3"
local ts=$(date '+%Y%m%d_%H%M%S')
local file="${dest}/${name}_${ts}.sql.gz"
if [[ "$db" == "all" ]]; then
mysqldump -u root -p"${MYSQL_PWD:-}" --all-databases --single-transaction | gzip > "$file"
else
mysqldump -u root -p"${MYSQL_PWD:-}" --single-transaction "$db" | gzip > "$file"
fi
log " MYSQL: $file ($(du -h "$file" | cut -f1))"
}
cleanup_old() {
local dir="$1"
find "$dir" -type f \( -name "*.tar.gz" -o -name "*.sql.gz" \) -mtime +$RETENTION_FULL -print -delete 2>/dev/null
log " CLEAN: 清理 $RETENTION_FULL 天前的备份"
}
# ---- 主流程 ----
main() {
log "======== 备份框架开始 ========"
for task in "${BACKUP_TASKS[@]}"; do
IFS='|' read -r name src type <<< "$task"
local dest="${BACKUP_ROOT}/${name}"
mkdir -p "$dest"
log "处理: $name ($type)"
case "$type" in
tar.gz) backup_tar "$name" "$src" "$dest" ;;
rsync) backup_rsync "$name" "$src" "$dest" ;;
mysql) backup_mysql_ "$name" "$src" "$dest" ;;
*) log " 未知类型: $type,跳过" ;;
esac
cleanup_old "$dest"
done
log "======== 备份框架结束 ========"
}
main
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
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
# 8.2 进程管理
# 8.2.1 ps / top / htop——三剑客查看进程
#!/bin/bash
# ===== ps —— 进程快照 =====
# 查看所有进程(BSD 风格)
ps aux
# a=所有用户 u=用户格式 x=无终端的进程
# 查看所有进程(标准风格)
ps -ef
# -e=所有进程 -f=完整格式
# 按 CPU 使用率排序
ps aux --sort=-%cpu | head -10
# 按内存使用率排序
ps aux --sort=-%mem | head -10
# 查看特定进程
ps aux | grep nginx
ps -C nginx -o pid,user,%cpu,%mem,comm # 按名称查(-C)
# 自定义输出列
ps -eo pid,ppid,user,%cpu,%mem,comm --sort=-%cpu | head -10
# 查看进程树(父子关系)
ps auxf # BSD 树状
ps -ejH # 标准缩进
pstree -p # 更直观的树(显示 PID)
# ===== 常用 ps 输出列说明 =====
# USER = 运行该进程的用户
# PID = 进程 ID
# %CPU = CPU 使用百分比
# %MEM = 内存使用百分比
# VSZ = 虚拟内存大小(KB)
# RSS = 常驻物理内存(KB)
# TTY = 终端
# STAT = 进程状态(见下方)
# START = 启动时间
# TIME = 累计 CPU 时间
# COMMAND= 命令
# ===== 进程状态 STAT 解读 =====
# R = 运行中 (Running)
# S = 可中断睡眠 (Sleeping)——等待某事件
# D = 不可中断睡眠 (Disk sleep)——通常等待 I/O
# Z = 僵尸进程 (Zombie)——已结束但未被父进程回收
# T = 停止 (Stopped)
# s = 会话首进程
# l = 多线程
# + = 前台进程组
# ===== 快速诊断命令 =====
ps aux | awk '$8 ~ /Z/ {print}' # 查找僵尸进程
ps aux | awk '$3 > 50 {print}' # CPU > 50% 的进程
ps aux | awk '$4 > 10 {print}' # 内存 > 10% 的进程
ps aux | awk '$8 ~ /D/ {print}' # 磁盘 I/O 阻塞进程
# ===== top —— 实时动态监控 =====
# top 交互快捷键:
# 1 = 显示每个 CPU 核心
# M = 按内存排序
# P = 按 CPU 排序
# c = 显示完整命令行
# k = kill 进程(输入 PID)
# q = 退出
# E = 切换内存单位(KB/MB/GB)
# d = 修改刷新间隔(秒)
# 非交互模式(脚本可用)
top -bn1 # 一次性输出
top -bn1 | head -20
top -bn1 -o %MEM | head -10 # 按内存排序的 Top 10
# ===== htop —— 更友好的 top =====
# 功能:彩色输出、鼠标支持、树状视图、一键 kill
# F3 搜索 / F5 树状 / F6 排序 / F9 kill / F10 退出
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
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
# 8.2.2 kill / pkill / killall——信号控制进程
#!/bin/bash
# ===== 常用信号速查表 =====
# 编号 名称 含义
# 1 SIGHUP 挂起(重新加载配置)
# 2 SIGINT 中断(Ctrl+C)
# 9 SIGKILL 强制杀死(不可捕获)
# 15 SIGTERM 终止(默认,可捕获做善后)
# 17 SIGSTOP 暂停(不可捕获)
# 18 SIGCONT 继续运行
# 19 SIGTSTP 终端停止(Ctrl+Z)
# ===== kill —— 按 PID =====
kill 1234 # 发送 SIGTERM(优雅终止)
kill -15 1234 # 等同于上面
kill -9 1234 # 强制杀死(SIGKILL)
kill -HUP 1234 # 重新加载配置(SIGHUP)
kill -l # 列出所有信号
# ⚠️ kill -9 的危险性
# SIGKILL 不给进程清理资源的机会:
# - 文件可能未正确关闭
# - 临时文件未删除
# - 数据库连接未释放
# - 共享内存/信号量可能残留
# 原则:先用 -15,不行再 -9
# ===== pkill —— 按名称匹配 =====
pkill nginx # 杀死所有 nginx 进程
pkill -f "python app.py" # 匹配完整命令行
pkill -HUP nginx # 给 nginx 发重载信号
pkill -u www-data # 杀死某用户的所有进程
# ===== killall —— 精确按名称 =====
killall nginx # 精确匹配(不是部分匹配)
killall -HUP php-fpm # 重载 PHP-FPM
# ===== 优雅关闭进程脚本 =====
graceful_kill() {
local proc="$1"
local pid=$(pgrep -f "$proc" | head -1)
if [[ -z "$pid" ]]; then
echo "进程未运行: $proc"
return 0
fi
echo "优雅关闭 $proc (PID: $pid)..."
kill -TERM "$pid"
# 等待最多 30 秒进程退出
for i in $(seq 1 30); do
if ! kill -0 "$pid" 2>/dev/null; then
echo "进程已优雅退出"
return 0
fi
sleep 1
done
# 超时后强制杀死
echo "进程未在 30s 内退出,强制 kill -9"
kill -9 "$pid"
}
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
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
# 8.2.3 nohup / disown / tmux——后台运行与断连续跑
#!/bin/bash
# ===== nohup —— 断开终端后继续运行 =====
# SIGHUP 信号会在终端断开时发给子进程
# nohup 忽略 SIGHUP,让进程继续运行
nohup python long_task.py & # 后台运行
# 输出重定向到 nohup.out
nohup python long_task.py > task.log 2>&1 & # 自定义输出文件
# 查看 nohup 启动的进程
jobs -l # 当前 shell 的后台任务
ps aux | grep python
# ===== disown —— 将后台任务脱离 Shell =====
python long_task.py &
disown # 脱离最近的后台任务
jobs -l # 任务已从列表中消失
# 效果:关闭终端不会 kill 该进程
# ===== nohup vs disown 对比 =====
# nohup = 从开始就忽略 HUP + 重定向输出
# disown = 先把进程放到后台,再移除 Shell 的作业控制
# 推荐:两者结合使用
nohup python task.py > task.log 2>&1 &
disown
# ===== screen / tmux —— 真正的不中断会话 =====
# screen(传统)
screen -S mysession # 创建新会话
screen -ls # 列出会话
screen -r mysession # 重新连接
# 快捷键:Ctrl+A, D = 分离会话
# tmux(现代化,推荐)
tmux new -s mysession # 创建新会话
tmux ls # 列出会话
tmux attach -t mysession # 重新连接
tmux kill-session -t mysession # 删除会话
# 快捷键:Ctrl+B, D = 分离
# ===== 实战:tmux 中运行长时间任务 =====
# 1. tmux new -s training
# 2. python train_model.py
# 3. Ctrl+B, D 断开(进程继续运行)
# 4. 退出 SSH
# 5. 第二天重新登录,tmux attach -t training 查看结果
# ===== 批量后台任务管理 =====
run_parallel() {
local cmd="$1"
local count="${2:-4}"
for i in $(seq 1 "$count"); do
nohup bash -c "$cmd" > "worker_${i}.log" 2>&1 &
echo "启动 worker_${i}, PID: $!"
done
echo "所有 worker 已启动,等待完成..."
wait # 等待所有后台任务完成
echo "全部完成"
}
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
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
# 8.2.4 进程守护——while+sleep 自愈模式
#!/bin/bash
# ===== 基础进程守护(while+sleep)=====
# 适用场景:运行一个一次性命令,挂了就重启
simple_guard() {
local cmd="$1"
local interval="${2:-5}"
while true; do
eval "$cmd"
echo "[$(date)] 进程退出,${interval}s 后重启..." >> /tmp/guard.log
sleep "$interval"
done
}
# 用法:simple_guard "python server.py" 10
# ===== 高级进程守护(检测存活 + 自动重启 + 重启次数限制)=====
advanced_guard() {
local name="$1" # 进程标识名
local cmd="$2" # 启动命令
local check_cmd="$3" # 检查命令(返回 0 = 存活)
local max_restart="${4:-5}" # 最多连续重启次数
local interval="${5:-30}" # 检测间隔(秒)
local restart_count=0
local last_restart=0
local cooldown=300 # 冷却期(秒),restart_count 在此时间内有效
while true; do
if eval "$check_cmd" 2>/dev/null; then
:
else
local now=$(date +%s)
# 如果距离上次故障超过冷却期,重置计数
if (( now - last_restart > cooldown )); then
restart_count=0
fi
restart_count=$((restart_count + 1))
last_restart=$now
if (( restart_count > max_restart )); then
echo "[$(date)] 💀 $name 连续重启 $max_restart 次,停止守护!"
return 1
fi
echo "[$(date)] $name 未检测到,第 $restart_count 次重启..."
eval "$cmd" &
fi
sleep "$interval"
done
}
# 用法示例
# advanced_guard "nginx" \
# "systemctl start nginx" \
# "pgrep nginx" \
# 5 30
# ===== 基于 PID 文件的守护(精准不重复)=====
pidfile_guard() {
local name="$1"
local cmd="$2"
local pidfile="/var/run/${name}.pid"
local interval="${3:-10}"
while true; do
if [[ -f "$pidfile" ]] && kill -0 $(cat "$pidfile") 2>/dev/null; then
:
else
echo "[$(date)] $name 停止,重新启动..." >> /var/log/guard.log
eval "$cmd"
echo $! > "$pidfile"
fi
sleep "$interval"
done
}
# ===== 多进程并行守护 =====
parallel_guard() {
# 配置:服务名=启动命令=检测命令
declare -A SERVICES=(
["nginx"]="systemctl start nginx|pgrep nginx"
["redis"]="systemctl start redis|pgrep redis-server"
["myapp"]="cd /opt/app && python main.py|pgrep -f 'python main.py'"
)
for name in "${!SERVICES[@]}"; do
IFS='|' read -r start_cmd check_cmd <<< "${SERVICES[$name]}"
advanced_guard "$name" "$start_cmd" "$check_cmd" 10 15 &
done
wait # 等待所有守护线程
}
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
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
# 8.2.5 supervisor / systemd——生产级进程管理
#!/bin/bash
# ===== 方式 1:systemd Service 文件 =====
# 位置:/etc/systemd/system/myapp.service
cat > /etc/systemd/system/myapp.service << 'EOF'
[Unit]
Description=My Application
After=network.target
[Service]
Type=simple
User=appuser
Group=appgroup
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/python /opt/myapp/main.py
ExecStop=/bin/kill -TERM $MAINPID
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure # 异常退出时自动重启
RestartSec=10 # 重启间隔 10 秒
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
# 资源限制
LimitNOFILE=65535
LimitNPROC=4096
MemoryMax=2G
CPUQuota=200%
[Install]
WantedBy=multi-user.target
EOF
# 管理命令
systemctl daemon-reload # 重载 service 文件
systemctl enable myapp # 开机自启
systemctl start myapp
systemctl status myapp
systemctl stop myapp
journalctl -u myapp -f # 查看日志
# ===== 方式 2:Supervisor =====
# 配置文件:/etc/supervisor/conf.d/myapp.conf
cat > /etc/supervisor/conf.d/myapp.conf << 'EOF'
[program:myapp]
command=/usr/bin/python /opt/myapp/main.py
directory=/opt/myapp
user=appuser
autostart=true # 随 supervisor 启动
autorestart=true # 异常退出自动重启
startretries=5 # 最多重试 5 次
redirect_stderr=true
stdout_logfile=/var/log/myapp/stdout.log
stderr_logfile=/var/log/myapp/stderr.log
environment=ENV="production",PORT="8080"
EOF
# 管理命令
supervisorctl reread # 重新读取配置
supervisorctl update # 更新运行中的进程
supervisorctl start myapp
supervisorctl stop myapp
supervisorctl status myapp
supervisorctl restart myapp
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
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
进程管理方案对比:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
while+sleep | 临时/简单守护 | 轻量、无需安装 | 无状态监控、简陋 |
systemd | Linux 系统服务 | 原生集成、资源限制、日志管理 | 仅 Linux |
supervisor | Python/Node 应用 | 跨平台、Web 界面、进程组 | 需额外安装 |
pm2 | Node.js 应用 | 热重载、集群模式、日志 | 仅 Node |
# 8.2.6 进程排查实战——卡死/僵尸/资源泄漏
#!/bin/bash
# ===== 场景 1:进程卡死——CPU 100% 排查 =====
# 找到高 CPU 进程
pid=$(ps aux --sort=-%cpu | awk 'NR==2{print $2}')
echo "高 CPU 进程 PID: $pid"
# 查看线程级 CPU(Java 等程序有用)
top -H -p "$pid" -bn1 | head -20
# 查看进程在干什么(系统调用)
strace -p "$pid" -c -f # 统计系统调用
strace -p "$pid" -T -e trace=read,write # 具体读写耗时
# 查看打开的文件
lsof -p "$pid" | head -30
# Java 进程:查看线程堆栈
jstack "$pid" > thread_dump_$(date +%s).txt
# ===== 场景 2:僵尸进程排查 =====
# 查找僵尸进程
zombies=$(ps aux | awk '$8 ~ /Z/ {print $2}')
if [[ -n "$zombies" ]]; then
echo "发现僵尸进程:"
for z in $zombies; do
ps -o pid,ppid,stat,comm -p "$z"
done
# 找僵尸的父进程
for z in $zombies; do
ppid=$(ps -o ppid= -p "$z" 2>/dev/null)
echo "僵死子进程 PID:$z 的父进程 PID:$ppid"
ps -o pid,comm -p "$ppid" 2>/dev/null
done
# 清除方法:kill 父进程(让它 wait() 回收子进程)
# 或 kill -HUP 父进程;如果父进程是系统进程则只能重启它
fi
# ===== 场景 3:内存泄漏排查 =====
# 监控某个进程的内存增长
watch_memory() {
local pid="$1"
local log="/tmp/mem_watch_${pid}.log"
echo "时间 RSS(KB)" > "$log"
for i in $(seq 1 60); do
rss=$(ps -o rss= -p "$pid" 2>/dev/null || echo "0")
echo "$(date '+%H:%M:%S') $rss" >> "$log"
sleep 10
done
echo "监控完成,日志: $log"
}
# ===== 场景 4:进程占用文件句柄泄漏 =====
check_fd_leak() {
local pid="$1"
local limit=$(cat /proc/$pid/limits 2>/dev/null | grep "open files" | awk '{print $4}')
local current=$(ls /proc/$pid/fd/ 2>/dev/null | wc -l)
echo "PID $pid 文件句柄: $current / $limit"
if [[ -n "$limit" && "$current" -ge $((limit * 80 / 100)) ]]; then
echo "⚠️ 文件句柄使用率超过 80%!"
fi
}
# ===== 场景 5:找出哪些进程在写磁盘(高 I/O)=====
top_io_processes() {
# 需要内核支持 TASK_DELAY_ACCT
iotop -bon1 | head -10
# 或者
pidstat -d 1 3
}
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
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
# 8.3 磁盘清理
# 8.3.1 df / du——磁盘空间诊断
#!/bin/bash
# ===== df —— 文件系统级磁盘使用 =====
df -h # 人类可读格式
df -h / /home # 查看特定挂载点
df -i # 查看 inode 使用情况
df -Th # 显示文件系统类型
# 只看物理磁盘(排除 tmpfs/overlay 等)
df -h -x tmpfs -x devtmpfs -x overlay
# ===== du —— 目录/文件级磁盘占用 =====
du -sh /var/log # 目录总大小
du -sh * | sort -hr | head -10 # 当前目录下 Top 10 子目录
du -h --max-depth=1 / # 根目录第一层各目录大小
# ===== 磁盘使用率告警 =====
check_disk_alert() {
local threshold=80
df -h | awk -v th="$threshold" '
NR>1 && /^\// {
gsub(/%/, "", $5)
if ($5+0 >= th)
printf "⚠️ %-20s 使用 %3d%% (已用 %s / 总共 %s)\n", $6, $5, $3, $2
}'
}
# ===== ncdu —— 交互式磁盘分析(需安装)=====
# ncdu / # 交互式浏览磁盘(比 du 直观)
# ===== 磁盘 I/O 延迟检查 =====
iostat -x 1 3 | awk '/Device/ || /sd|nvme|vd/ {
printf "%-10s %7s %7s %8s %7s\n", $1, "r/s", "w/s", "await", "%util"
getline
if ($1 ~ /sd|nvme|vd/) printf "%-10s %7.1f %7.1f %7.1fms %6.1f%%\n", $1, $4, $5, $10, $14
}'
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
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
# 8.3.2 大文件/大目录定位与清理
#!/bin/bash
# ===== 查找大文件(指定目录)=====
find_large_files() {
local dir="${1:-.}"
local min_size="${2:-100M}"
echo "=== $dir 下 > $min_size 的文件 ==="
find "$dir" -type f -size "+$min_size" -exec ls -lh {} \; \
| awk '{print $5, $NF}' | sort -hr | head -20
}
# ===== 查找大目录 =====
find_large_dirs() {
local dir="${1:-.}"
local depth="${2:-3}"
echo "=== $dir 下 Top 20 大目录 ==="
du -h --max-depth="$depth" "$dir" 2>/dev/null | sort -hr | head -20
}
# ===== 查找并安全删除空文件/空目录 =====
cleanup_empty() {
echo "空文件:"
find . -type f -empty -print
echo "空目录:"
find . -type d -empty -print
read -p "确认删除以上空文件/目录?(yes/no): " confirm
if [[ "$confirm" == "yes" ]]; then
find . -type f -empty -delete
find . -type d -empty -delete
echo "已删除"
fi
}
# ===== 查找重复文件(按文件大小 + MD5)=====
find_duplicates() {
local dir="${1:-.}"
echo "=== 查找重复文件 ==="
find "$dir" -type f -size +1M -exec md5sum {} \; \
| sort | awk '
{
hash=$1; file=substr($0, index($0,$2))
if (hash == prev_hash) {
print "重复: " file
print " 与: " prev_file
print ""
}
prev_hash=hash; prev_file=file
}'
}
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
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
# 8.3.3 logrotate——日志自动轮转
#!/bin/bash
# ===== logrotate 配置文件模板 =====
# 位置:/etc/logrotate.d/myapp
cat > /etc/logrotate.d/myapp << 'CONF'
/var/log/myapp/*.log {
daily # 每天轮转
rotate 30 # 保留 30 个归档
missingok # 日志不存在也不报错
notifempty # 空文件不轮转
compress # 压缩旧日志
delaycompress # 延迟压缩(当前 + 上一个不压缩)
dateext # 用日期做后缀(而不是数字)
dateformat -%Y%m%d
sharedscripts # 多个日志文件共享 postrotate 脚本
postrotate
/bin/kill -HUP $(cat /var/run/myapp.pid) 2>/dev/null || true
endscript
}
CONF
# 手动触发 logrotate
logrotate -f /etc/logrotate.d/myapp # -f = 强制执行
logrotate -d /etc/logrotate.d/myapp # -d = debug 模式(dry-run)
# ===== 手动日志轮转脚本(没有 logrotate 时)=====
manual_logrotate() {
local log_file="$1"
local max_size_mb="${2:-100}" # >100MB 才轮转
if [[ ! -f "$log_file" ]]; then
echo "日志文件不存在: $log_file"
return 1
fi
local size=$(stat -f%z "$log_file" 2>/dev/null || stat -c%s "$log_file" 2>/dev/null)
size=$((size / 1048576))
if (( size < max_size_mb )); then
return 0 # 还不够大,不轮转
fi
local archive="${log_file}.$(date '+%Y%m%d_%H%M%S')"
cp "$log_file" "$archive"
: > "$log_file" # 清空原日志
gzip "$archive" # 压缩归档
echo "[$(date)] 日志轮转: $archive.gz (${size}MB)"
}
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
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
# 8.3.4 临时文件与缓存清理
#!/bin/bash
# ===== 系统临时文件清理 =====
system_cleanup() {
echo "=== 系统临时文件 ==="
# /tmp 中超过 7 天的文件
echo "/tmp 下超过 7 天的文件:"
find /tmp -type f -atime +7 2>/dev/null | wc -l
# 谨慎删除:
# find /tmp -type f -atime +7 -delete 2>/dev/null
# /var/tmp 中超过 30 天的文件
echo "/var/tmp 下超过 30 天的文件:"
find /var/tmp -type f -atime +30 2>/dev/null | wc -l
# systemd 日志清理
echo "systemd journal 日志大小:"
journalctl --disk-usage
# 限制日志大小
# journalctl --vacuum-size=500M
# journalctl --vacuum-time=7d
}
# ===== 包管理器缓存清理 =====
pkg_cache_cleanup() {
# Debian/Ubuntu
if command -v apt-get &>/dev/null; then
apt-get clean # 清理下载的软件包
apt-get autoremove --purge -y # 清理不再需要的依赖
fi
# CentOS/RHEL
if command -v yum &>/dev/null; then
yum clean all
fi
}
# ===== 用户缓存清理 =====
user_cache_cleanup() {
# pip 缓存
pip cache purge 2>/dev/null || true
# npm 缓存
npm cache clean --force 2>/dev/null || true
# yarn 缓存
yarn cache clean 2>/dev/null || true
# Go 模块缓存
go clean -modcache 2>/dev/null || true
# 各用户 home 下的 .cache
find /home -maxdepth 2 -type d -name ".cache" -exec du -sh {} \; 2>/dev/null
}
# ===== 一键释放缓存内存(生产慎用)=====
release_memory_cache() {
echo "当前内存使用:"
free -h
echo ""
# 只清理 pagecache(相对安全)
sync
echo 1 > /proc/sys/vm/drop_caches
# echo 2 = 清理 dentries 和 inodes
# echo 3 = 清理 pagecache + dentries + inodes
echo "清理后:"
free -h
}
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
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
# 8.3.5 Docker 磁盘空间回收
#!/bin/bash
# ===== Docker 空间占用概览 =====
docker system df
# 输出:Images / Containers / Local Volumes / Build Cache 各自占用
docker system df -v # 详细版,显示每个对象的大小
# ===== 一键清理(小心!)=====
# 删除停止的容器、未使用的网络、悬挂镜像、构建缓存
docker system prune
# 更彻底的清理(包括所有未使用的镜像)
docker system prune -a # ⚠️ 删除所有未被容器使用的镜像
docker system prune -a --volumes # 同时删除未使用的卷
# ===== 分对象清理 =====
# 清理停止的容器
docker container prune
docker rm $(docker ps -aq) # 删除所有停止的容器
# 清理悬挂镜像(<none>:<none>)
docker image prune
# 清理所有未使用的镜像
docker image prune -a
# 清理未使用的卷
docker volume prune
# 清理构建缓存
docker builder prune
docker builder prune -a # 包括已使用的缓存
# ===== 查找 Docker 根目录磁盘大户 =====
du -sh /var/lib/docker/*
du -sh /var/lib/docker/overlay2/ | sort -hr | head -10
# ===== Docker 日志清理 =====
# Docker 容器日志默认不小——查找日志文件
find /var/lib/docker/containers -name "*.log" -size +100M -exec ls -lh {} \;
# 清理指定容器的日志(truncate 比 rm 安全)
truncate -s 0 /var/lib/docker/containers/*/*-json.log
# 或者配置 daemon.json 限制日志大小:
# { "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" } }
# ===== Docker 磁盘清理脚本 =====
cat > docker_cleanup.sh << 'SCRIPT'
#!/bin/bash
THRESHOLD=70 # 磁盘使用率阈值
disk_usage=$(df /var/lib/docker | awk 'NR==2 {gsub(/%/,"",$5); print $5}')
echo "Docker 磁盘使用率: ${disk_usage}%"
if [[ "$disk_usage" -ge "$THRESHOLD" ]]; then
echo "超过阈值,开始清理..."
# 1. 清理停止超过 24h 的容器
docker container prune -f --filter "until=24h" 2>/dev/null
# 2. 清理悬挂镜像
docker image prune -f 2>/dev/null
# 3. 清理构建缓存
docker builder prune -f 2>/dev/null
# 4. 清理未使用的卷(谨慎!)
# docker volume prune -f 2>/dev/null
# 5. 截断大日志文件
find /var/lib/docker/containers -name "*.log" -size +500M \
-exec truncate -s 100M {} \;
echo "清理完成"
fi
SCRIPT
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
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
# 8.3.6 inode 耗尽排查与解决
#!/bin/bash
# ===== inode 是什么 =====
# 每个文件/目录占用 1 个 inode(存储元数据:权限/所有者/大小/数据块位置)
# inode 总数在格式化时固定,即使磁盘空间没满,inode 用完也无法创建新文件
# ===== 检查 inode 使用 =====
df -i | awk 'NR==1 || /^\//'
# 重点关注 IUse% 列
# ===== 哪个目录 inode 消耗最多 =====
find_inode_hogs() {
local dir="${1:-/var}"
echo "=== $dir 下 inode 消耗 Top 10 目录 ==="
find "$dir" -xdev -type d 2>/dev/null \
| while read -r d; do
echo "$(find "$d" -maxdepth 1 2>/dev/null | wc -l) $d"
done | sort -rn | head -10
}
# ===== 常见 inode 杀手 =====
# 1. 大量小文件——session 文件(/tmp /var/lib/php/sessions)
find /var/lib/php/sessions -type f | wc -l
# 清理:删除超过对应 maxlifetime 的 session
find /var/lib/php/sessions -type f -mtime +1 -delete
# 2. 邮件队列积压
find /var/spool/postfix -type f | wc -l
# 清理积压邮件:
# postsuper -d ALL (删除所有队列邮件——谨慎)
# 3. 缓存目录
find /var/cache -type f | wc -l
find /tmp -type f | wc -l
# 4. Docker overlay2
find /var/lib/docker/overlay2 -type f | wc -l
# ===== inode 诊断脚本 =====
cat > inode_diagnose.sh << 'SCRIPT'
#!/bin/bash
echo "===== Inode 诊断 ====="
df -i | awk 'NR==1 || (/^\// && $5+0 > 50) {printf "%-25s %s/%s (%s)\n", $6, $3, $2, $5}'
echo ""
echo "===== Top 10 inode 消耗目录 ====="
for dir in /var /tmp /home /usr /opt /etc; do
[[ -d "$dir" ]] || continue
count=$(find "$dir" -xdev 2>/dev/null | wc -l)
printf "%-20s %10d\n" "$dir" $count
done | sort -k2 -rn
echo ""
echo "===== 可疑目录 ====="
# Session / mail / cache / docker
for suspect in /var/spool/postfix /var/lib/php/sessions /tmp /var/cache; do
[[ -d "$suspect" ]] || continue
count=$(find "$suspect" -xdev -type f 2>/dev/null | wc -l)
if (( count > 10000 )); then
printf "⚠️ %-40s %d files\n" "$suspect" $count
fi
done
SCRIPT
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
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
# 8.4 新手陷阱与思考题
# 陷阱 Top 5
陷阱 1:tar 覆盖已有文件
# ❌ tar 解压会直接覆盖已有文件(无提示)
tar -xzf backup.tar.gz
# ✅ 先列出来看看再决定
tar -tzf backup.tar.gz | head -30
# ✅ 解压到临时目录比对
mkdir /tmp/restore_test
tar -xzf backup.tar.gz -C /tmp/restore_test
diff -r /tmp/restore_test /target/
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
陷阱 2:mysqldump 锁表导致生产服务中断
# ❌ MyISAM 表默认会锁表
mysqldump -u root -p mydb
# ✅ InnoDB 用 --single-transaction 避免锁表
mysqldump -u root -p --single-transaction mydb
# ✅ 或用 --lock-tables=false(MySQL 8.0+)
mysqldump -u root -p --lock-tables=false mydb
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
陷阱 3:rsync 源路径末尾斜杠的含义
rsync -av /src/dir /dst/ # → /dst/dir/
rsync -av /src/dir/ /dst/ # → /dst/ (同步 dir 的内容到 dst)
# 差一个 / 后果完全不同!建议始终用 --dry-run 先预览
1
2
3
2
3
陷阱 4:kill -9 造成资源泄漏
# ❌ 直接 kill -9——不给进程清理机会
kill -9 12345
# 后果:共享内存段/Semaphore 可能残留,/tmp 临时文件泄漏
# ✅ 先 TERM 再 KILL
kill -TERM 12345
sleep 5
if kill -0 12345 2>/dev/null; then kill -9 12345; fi
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
陷阱 5:docker system prune -a 误删共享镜像
# ❌ 删除了多个服务共用的基础镜像
docker system prune -a -f
# ✅ 先看看哪些会被删除
docker system prune -a --dry-run
# ✅ 精确清理悬挂镜像
docker image prune -f
1
2
3
4
5
6
7
2
3
4
5
6
7
# 综合思考题
备份验证:写一个脚本,在备份完成后自动解压验证备份文件完整性(对比文件数量和关键文件 checksum)。
增量备份恢复:有一个全量备份
full.tar.gz和 3 个增量incr1.tar.gzincr2.tar.gzincr3.tar.gz,写出恢复脚本(按正确顺序合并)。进程异常检测:写脚本监控某进程的 RSS 内存,如果 10 分钟内增长超过 200%,判定为内存泄漏,自动 dump
/proc/<pid>/smaps并发送告警。磁盘空间预测:根据
/var/log/metrics/system.csv中每天记录的磁盘使用率,用线性回归预测磁盘将在多少天后满(提示:取最近 7 天数据,用 awk 计算斜率)。综合:开发环境重置脚本:写一个脚本自动清理开发机上的以下内容:
- Docker 停止的容器/未使用的镜像/构建缓存
- npm / pip / go mod 缓存
- /tmp 下超过 3 天的临时文件
- 所有 .log 文件超过 50MB 的截断到 10MB
- 输出清理前后磁盘使用率对比
上次更新: 2026/06/17, 12:47:39