问题背景

系统的日志、缓存,或程序的输出、结果等文件写入频率很高,随着时间的推移,磁盘空间的占用慢慢增长,这些超过一定时限的数据,比如 3 个月前的数据,可能没有太大的价值(在此假设重要的事件已经收集并上报),那么在系统空间不足时(如剩余 10% 的可用空间),可删除这些文件,以确保系统和服务的正常运转。

Linux 解决方案

特性

  • 支持清理多个文件夹
  • 删除过期文件的空间,配合 cron 可清理自动删除
  • 触发清理的阈值可调,阈值与已占用空间的百分比进行比较
  • 支持空跑模式,空跑时不实际删除文件,而是将待删除的文件打印出来进行调试
  • 可自定义时间周期,比如依次删除 3 个月,2 个月,1 个月,两周,一周前的数据

用法

直接调用

# 先调试,打印待删除文件
DEBUG=1 /usr/local/bin/remove_old_files.sh 80
# 实际删除
/usr/local/bin/remove_old_files.sh 80

定期调用

比如每天凌晨 3 点,自动对目录进行清理。

# 进入 cron 定期任务编辑模式
crontab -e
# m h  dom mon dow   command
0 3 * * * /usr/local/bin/remove_old_files.sh 80

Bash 代码

#!/bin/bash

# Target directories
# 目标目录,比如高频输出结果的文件夹,临时目录,缓存目录等
DIRS=("/path/to/directory1" "/path/to/directory2" "/path/to/directory3")

# Usage space threshold (in percent)
# 等到占用空间高于多少百分比时执行清理操作,默认 90,即剩余 10% 空间时触发清理
USAGE_PERCENT=${1:-90}

# Debug flag
# 是否仅打印文件名,而不实际删除,即 dry-run 空跑
DEBUG=${DEBUG:-0}

# Days old that files must be to get removed
# 时间范围依次递减,若更旧的文件删除后,复查已用空间百分比已满足要求,就不再删除时间较新的文件
MTIMES=(90 60 30 15 7)

# Function to check disk usage
# 检查已用空间是否超标
check_disk_usage() {
    local DIR=$1
    # Return true if the disk usage is greater than or equal to the passed value
    # 调用 df 命令,略过第一行,输出第 5 列 `Use%`,并去掉末尾的百分号,再跟设定的已用空间百分比阈值比较;
    # 已用空间大于阈值时,返回真,表示需要清理;否则,返回假。
    if [ $(df $DIR | awk 'NR==2 {print $5}' | sed 's/%//g') -ge $USAGE_PERCENT ]; then
        return 0
    else
        return 1
    fi
}

# Function to find and remove files older than a certain number of days
# 根据 mtime 范围来查找并删除文件,比如 +30 表示 30 天前的文件,而 -30 表示 30 内的文件。
# `-mtime` 参数解释:
#  -mtime n
#  File was last modified n*24 hours ago.  When find figures out how many 24-hour periods ago the file was last modification, any fractional  part is ignored, so to match -atime +1, a file has to have been accessed at least two days ago.(换句话,今天已经流逝的时间不算在内(不能被 24 整数,所以忽略掉了),然后再向前数 N 个 24 小时前)
remove_old_files() {
    local DIR=$1
    local MTIME=$2

    if [ "$DEBUG" -eq 1 ]; then
        find $DIR -type f -mtime +$MTIME -print
    else
        find $DIR -type f -mtime +$MTIME -exec rm -f {} \;
    fi
}

# Iterate over the directories and time periods and try to remove old files
# 外遍历目标目录,再内遍历时间范围,组合两层循环的参数,调用删除旧文件的函数
for DIR in ${DIRS[@]}; do
    for MTIME in ${MTIMES[@]}; do
        if check_disk_usage $DIR; then
            remove_old_files $DIR $MTIME
        fi
    done

    # Check one final time if the disk space usage is still greater than or equal to the specified percent
    # 经过了上面的删除后,最后进行一次复查,如果已使用空间仍然大于阈值,则输出日志
    if check_disk_usage $DIR; then
        # If so, output a message that will be mailed to the user
        # 当此脚本被 crontab 定期执行时,任何输出都会发送到用户的邮箱
        echo "Warning: Disk space usage in $DIR is still greater than or equal to $USAGE_PERCENT% after deleting old files."
    fi
done

Windows 解决方案

考虑到 .bat 支持的语言特性较少, 缺少 array, Pipe 语法和 sed, df 这些常用命令,因此改用 Python 实现。

特性

Bash 版本基本一致,只不过定期执行需要配合 Windows 的 taskschd.msc 计划任务程序。

用法

直接调用

REM 输出待删除文件
python remove_old_files.py -p 80 -d
REM 实际删除
python remove_old_files.py -p 80

定期调用

配合 taskschd 计划任务,设定重复执行的周期。

Python 代码

#!/usr/bin/env python
# -*- coding:utf-8 -*-

# author: Muwaii
# datetime: 6/6/2023 5:20 PM
# software: PyCharm

import argparse
import os
import pathlib
import shutil
import time

def parse_cmd_args():
    # Parsing arguments for custom percent and debug mode
    parser = argparse.ArgumentParser()
    parser.add_argument("target_directories", type=pathlib.Path, nargs="+", help="targets")
    parser.add_argument("-m", "--mtimes", type=int, nargs="+", default=[90, 60, 30],
                        help="Disk usage percent to trigger file removal")
    parser.add_argument("-p", "--percent", type=int, default=90, help="Disk usage percent to trigger file removal")
    parser.add_argument("-d", "--debug", action="store_true", help="Enable debug mode (does not actually delete files)")
    args = parser.parse_args()
    return args

def check_disk_usage(_dir, usage_percent):
    total, used, free = shutil.disk_usage(_dir)
    percent_used = used / total * 100
    return percent_used >= usage_percent

def remove_old_files(_dir, days_old, is_debug=False):
    cutoff_time = time.time() - days_old * 24 * 60 * 60
    for root, dirs, files in os.walk(_dir):
        for file in files:
            file_path = os.path.join(root, file)
            if os.path.getmtime(file_path) < cutoff_time:
                if is_debug:
                    print(f"\tWould remove: {file_path}")
                else:
                    print(f"\tReal remove: {file_path}")
                    os.remove(file_path)

def main():
    args = parse_cmd_args()
    # Target directories
    dirs = args.target_directories

    # Days old that files must be to get removed
    mtimes = args.mtimes

    for _dir in dirs:  # type: pathlib.Path
        if not _dir.exists():
            print(f"dir `_dir` not found, skip.")
            continue
        for mtime in mtimes:
            print(f"check mtime {mtime} with dir {_dir}...")
            if check_disk_usage(_dir, args.percent):
                remove_old_files(_dir, mtime, is_debug=args.debug)

        # Check one final time if the disk space usage is still greater than or equal to the specified percent
        if check_disk_usage(_dir, args.percent):
            print(f"Warning: Disk space usage in {_dir} is still greater than or "
                  "equal to {args.percent}% after deleting old files.")

if __name__ == '__main__':
    main()

小结

本文针对临时文件过多导致磁盘空间不足的场景,提出了两个常用平台的解决方案,Linux 使用原生的 Bash 实现,而 Windows 受限于 bat 批处理语法过于简陋,改用 Python 实现,整体上两者实现的特性和用法差异不大,都能够解决开头提到的问题。

添加新评论