编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • ScriptHub 脚本工具箱
  • Python

  • Shell-Bash

    • Shell & Bash 从0到1实战专栏
    • Shell 入门与变量
    • 流程控制与函数
    • 数据与 IO 处理
    • grep 搜索实战
    • sed 与 awk 编程
    • 文件查找与统计
    • 日志监控与告警
    • 备份进程与磁盘
      • 8.1 备份恢复
        • 8.1.1 tar 压缩归档——最基础的备份方式
        • 8.1.2 全量备份与增量/差异备份策略
        • 8.1.3 rsync 远程同步——增量备份利器
        • 8.1.4 数据库备份——MySQL/PostgreSQL
        • 8.1.5 远程备份——scp/sftp/rsync over SSH
        • 8.1.6 定时备份 cron + 自动清理过期备份
        • 8.1.7 备份实战:通用备份框架脚本
      • 8.2 进程管理
        • 8.2.1 ps / top / htop——三剑客查看进程
        • 8.2.2 kill / pkill / killall——信号控制进程
        • 8.2.3 nohup / disown / tmux——后台运行与断连续跑
        • 8.2.4 进程守护——while+sleep 自愈模式
        • 8.2.5 supervisor / systemd——生产级进程管理
        • 8.2.6 进程排查实战——卡死/僵尸/资源泄漏
      • 8.3 磁盘清理
        • 8.3.1 df / du——磁盘空间诊断
        • 8.3.2 大文件/大目录定位与清理
        • 8.3.3 logrotate——日志自动轮转
        • 8.3.4 临时文件与缓存清理
        • 8.3.5 Docker 磁盘空间回收
        • 8.3.6 inode 耗尽排查与解决
      • 8.4 新手陷阱与思考题
        • 陷阱 Top 5
        • 综合思考题
    • 用户与服务管理
    • 网络调度与部署
    • 调试与脚本规范
    • 安全与兼容处理
    • 性能与打包分发
  • 工具脚本

  • ScriptHub
  • Shell-Bash
杨充
2024-09-11
目录

备份进程与磁盘

# 第 8 章 备份进程与磁盘

# 目录介绍

  • 8.1 备份恢复
    • 8.1.1 tar 压缩归档——最基础的备份方式
    • 8.1.2 全量备份与增量/差异备份策略
    • 8.1.3 rsync 远程同步——增量备份利器
    • 8.1.4 数据库备份——MySQL/PostgreSQL
    • 8.1.5 远程备份——scp/sftp/rsync over SSH
    • 8.1.6 定时备份 cron + 自动清理过期备份
    • 8.1.7 备份实战:通用备份框架脚本
  • 8.2 进程管理
    • 8.2.1 ps / top / htop——三剑客查看进程
    • 8.2.2 kill / pkill / killall——信号控制进程
    • 8.2.3 nohup / disown / tmux——后台运行与断连续跑
    • 8.2.4 进程守护——while+sleep 自愈模式
    • 8.2.5 supervisor / systemd——生产级进程管理
    • 8.2.6 进程排查实战——卡死/僵尸/资源泄漏
  • 8.3 磁盘清理
    • 8.3.1 df / du——磁盘空间诊断
    • 8.3.2 大文件/大目录定位与清理
    • 8.3.3 logrotate——日志自动轮转
    • 8.3.4 临时文件与缓存清理
    • 8.3.5 Docker 磁盘空间回收
    • 8.3.6 inode 耗尽排查与解决
  • 8.4 新手陷阱与思考题

# 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

压缩算法对比:

压缩格式 选项 速度 压缩率 场景
.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

# 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

# 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

数据库备份安全提醒:

# ✅ 密码不要写在命令行(会被 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

进程管理方案对比:

方案 适用场景 优点 缺点
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

# 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

# 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

# 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

# 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

# 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

# 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

# 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: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

陷阱 3:rsync 源路径末尾斜杠的含义

rsync -av /src/dir  /dst/       # → /dst/dir/
rsync -av /src/dir/ /dst/       # → /dst/ (同步 dir 的内容到 dst)
# 差一个 / 后果完全不同!建议始终用 --dry-run 先预览
1
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

陷阱 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

# 综合思考题

  1. 备份验证:写一个脚本,在备份完成后自动解压验证备份文件完整性(对比文件数量和关键文件 checksum)。

  2. 增量备份恢复:有一个全量备份 full.tar.gz 和 3 个增量 incr1.tar.gz incr2.tar.gz incr3.tar.gz,写出恢复脚本(按正确顺序合并)。

  3. 进程异常检测:写脚本监控某进程的 RSS 内存,如果 10 分钟内增长超过 200%,判定为内存泄漏,自动 dump /proc/<pid>/smaps 并发送告警。

  4. 磁盘空间预测:根据 /var/log/metrics/system.csv 中每天记录的磁盘使用率,用线性回归预测磁盘将在多少天后满(提示:取最近 7 天数据,用 awk 计算斜率)。

  5. 综合:开发环境重置脚本:写一个脚本自动清理开发机上的以下内容:

    • Docker 停止的容器/未使用的镜像/构建缓存
    • npm / pip / go mod 缓存
    • /tmp 下超过 3 天的临时文件
    • 所有 .log 文件超过 50MB 的截断到 10MB
    • 输出清理前后磁盘使用率对比
#Shell#运维
上次更新: 2026/06/17, 12:47:39
日志监控与告警
用户与服务管理

← 日志监控与告警 用户与服务管理→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式