1. 项目背景与需求分析
在音视频处理项目中,我们经常会遇到存储资源管理的问题。特别是在客户端设备上,磁盘空间往往有限,而音视频文件又非常占用存储空间。如果不及时清理旧文件,很容易导致磁盘被占满,影响系统正常运行。
以我最近参与的一个视频监控项目为例,系统会持续生成两种类型的文件:
- 截图文件:存储在
/home/ubuntu/recorder/test目录下,按年-月-日格式命名 - 视频回放文件:存储在
/home/ubuntu/record_video目录下,分为test_年-月-日和test_年-月-日_rtsp_origin两种格式
这些文件会随着时间不断累积,如果不加管理,几天内就会耗尽磁盘空间。因此,我们需要一个可靠的自动化解决方案来定期清理旧文件。
2. 技术方案设计
2.1 核心设计思路
我们的解决方案需要实现以下功能:
- 定期检查磁盘空间使用情况
- 当可用空间低于阈值时,自动删除最旧的目录
- 支持多种目录命名格式的识别和处理
- 可以以后台守护进程或前台服务形式运行
2.2 关键技术选型
选择C++实现这个方案有以下几个考虑:
- 性能考虑:文件系统操作需要高效,C++在这方面有优势
- 系统级访问:需要直接调用Linux系统API进行磁盘信息获取和文件操作
- 稳定性:作为后台服务,需要长期稳定运行
- 跨平台潜力:虽然当前针对Linux,但C++代码相对容易移植到其他平台
使用C++17标准主要是为了利用std::filesystem库,它提供了更现代、更安全的文件系统操作接口。
3. 实现细节解析
3.1 目录类型定义与识别
cpp复制enum class DirectoryType {
TEST_WITH_PREFIX, // test_年-月-日 和 test_年-月-日_rtsp_origin
DATE_ONLY // 年-月-日
};
struct MonitorTarget {
std::string path; // 目标路径
DirectoryType type; // 目录类型
std::string description; // 描述信息
};
目录识别是核心功能之一。我们定义了两种目录类型,并提供了相应的识别方法:
cpp复制bool isValidDirectoryName(const std::string& dirName, DirectoryType type) {
if (type == DirectoryType::TEST_WITH_PREFIX) {
// 筛选以 "test_" 开头的目录
if (dirName.find("test_") != 0) {
return false;
}
// 提取日期格式部分
std::string dateStr;
size_t pos = dirName.find("_rtsp_origin");
if (pos != std::string::npos) {
dateStr = dirName.substr(5, pos - 5);
} else {
dateStr = dirName.substr(5);
}
// 检查日期格式
std::tm tm = {};
std::istringstream ss(dateStr);
ss >> std::get_time(&tm, "%Y-%m-%d");
return tm.tm_year != 0;
} else if (type == DirectoryType::DATE_ONLY) {
std::tm tm = {};
std::istringstream ss(dirName);
ss >> std::get_time(&tm, "%Y-%m-%d");
return tm.tm_year != 0;
}
return false;
}
3.2 磁盘空间监控机制
磁盘空间监控通过statvfs系统调用实现:
cpp复制double getDiskUsage(const std::string& path = "/") {
struct statvfs stat;
if (statvfs(path.c_str(), &stat) != 0) {
std::cerr << "获取磁盘信息失败: " << strerror(errno) << std::endl;
return -1.0;
}
unsigned long long totalBytes = stat.f_blocks * stat.f_frsize;
unsigned long long freeBytes = stat.f_bfree * stat.f_frsize;
unsigned long long usedBytes = totalBytes - freeBytes;
double usageRatio = (double)usedBytes / totalBytes;
double freeRatio = (double)freeBytes / totalBytes;
// 获取当前时间
auto now = std::chrono::system_clock::now();
std::time_t now_time = std::chrono::system_clock::to_time_t(now);
std::tm* now_tm = std::localtime(&now_time);
char time_str[100];
std::strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", now_tm);
std::cout << "[" << time_str << "] "
<< "磁盘使用情况 - 总空间: " << formatSize(totalBytes)
<< ", 已使用: " << formatSize(usedBytes)
<< ", 可用: " << formatSize(freeBytes)
<< ", 可用比例: " << std::fixed << std::setprecision(2) << (freeRatio * 100) << "%" << std::endl;
return freeRatio;
}
3.3 目录删除策略
当磁盘空间不足时,系统会找到最旧的目录进行删除:
cpp复制std::vector<std::string> findOldestDirectories() {
auto allDirectories = getAllTargetDirectories();
if (allDirectories.empty()) {
return {};
}
// 按日期排序(最早的在前)
std::sort(allDirectories.begin(), allDirectories.end(),
[](const auto& a, const auto& b) {
return a.date < b.date;
});
// 获取最旧的目录信息
DirectoryInfo oldestDir = allDirectories[0];
// 获取需要删除的所有目录
return getDirectoriesToDelete(oldestDir);
}
删除操作支持普通删除和sudo权限删除两种方式:
cpp复制bool deleteWithPrivilege(const std::string& path) {
// 首先尝试普通删除
try {
// 获取目录大小
uintmax_t size = 0;
try {
// 使用递归目录迭代器
for (const auto& entry : fs::recursive_directory_iterator(path)) {
if (fs::is_regular_file(entry.path())) {
try {
size += fs::file_size(entry.path());
} catch (...) {
// 尝试获取目录大小,用于打印显示释放多少资源空间,获取失败直接删除
}
}
}
} catch (...) {
// 即使无法遍历所有目录大小,也尝试删除
size = 0;
}
if (fs::remove_all(path)) {
std::cout << "普通删除成功: " << path << " (释放空间: " << formatSize(size) << ")" << std::endl;
return true;
}
} catch (const std::exception& e) {
std::cerr << "删除失败: " << e.what() << std::endl;
// 尝试sudo权限且普通删除失败
if (useSudoForDeletion) {
return deleteWithSudo(path);
}
}
return false;
}
4. 守护进程实现
4.1 守护进程化
通过fork和setsid等系统调用实现守护进程:
cpp复制void daemonize() {
pid_t pid = fork();
if (pid < 0) {
std::cerr << "fork失败: " << strerror(errno) << std::endl;
exit(EXIT_FAILURE);
}
if (pid > 0) {
exit(EXIT_SUCCESS); // 父进程退出
}
// 子进程继续
umask(0);
setsid();
// 关闭标准文件描述符
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
// 重定向到/dev/null或日志文件
int fd = open("/dev/null", O_RDWR);
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
if (fd > 2) close(fd);
// 改变工作目录
chdir("/");
}
4.2 主监控循环
主循环定期检查磁盘空间并根据需要进行清理:
cpp复制void monitorLoop() {
auto now = std::chrono::system_clock::now();
std::time_t now_time = std::chrono::system_clock::to_time_t(now);
std::tm* now_tm = std::localtime(&now_time);
char time_str[100];
std::strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", now_tm);
std::cout << "[" << time_str << "] 开始监控磁盘空间,检查间隔: "
<< CHECK_INTERVAL << "秒 (" << (CHECK_INTERVAL/3600.0) << "小时)" << std::endl;
std::cout << "[" << time_str << "] 阈值: " << (THRESHOLD * 100) << "% 可用空间" << std::endl;
// 显示监控状态
showMonitorStatus();
while (true) {
double freeRatio = getDiskUsage();
if (freeRatio >= 0 && freeRatio < THRESHOLD) {
now = std::chrono::system_clock::now();
now_time = std::chrono::system_clock::to_time_t(now);
now_tm = std::localtime(&now_time);
std::strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", now_tm);
std::cout << "[" << time_str << "] 磁盘空间不足! 可用空间: "
<< std::fixed << std::setprecision(2) << (freeRatio * 100)
<< "%,低于阈值 " << (THRESHOLD * 100) << "%" << std::endl;
std::cout << "[" << time_str << "] 开始清理最旧的目录..." << std::endl;
cleanupDiskSpace();
// 清理后立即再次检查磁盘空间
freeRatio = getDiskUsage();
if (freeRatio >= THRESHOLD) {
std::cout << "[" << time_str << "] 清理成功,磁盘空间恢复正常" << std::endl;
}
} else if (freeRatio >= 0) {
now = std::chrono::system_clock::now();
now_time = std::chrono::system_clock::to_time_t(now);
now_tm = std::localtime(&now_time);
std::strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", now_tm);
std::cout << "[" << time_str << "] 磁盘空间正常: "
<< std::fixed << std::setprecision(2) << (freeRatio * 100) << "% 可用" << std::endl;
}
// 等待指定时间
std::cout << "等待 " << (CHECK_INTERVAL/3600) << " 小时后进行下一次检查..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(CHECK_INTERVAL));
}
}
5. 编译与使用
5.1 编译命令
bash复制g++ -std=c++17 -o disk_monitor disk_monitor.cpp -lstdc++fs -pthread
5.2 运行方式
- 作为守护进程运行(默认):
bash复制sudo ./disk_monitor
- 以前台服务形式运行:
bash复制sudo ./disk_monitor --foreground
- 查看当前监控状态:
bash复制./disk_monitor --status
6. 实际应用中的注意事项
-
权限管理:
- 确保程序有足够的权限访问目标目录
- 对于需要sudo权限删除的文件,考虑使用setuid或配置sudoers文件
-
日志记录:
- 在生产环境中,建议将日志输出到文件而非控制台
- 可以考虑使用syslog进行更规范的日志管理
-
异常处理:
- 增加对网络文件系统(NFS)的特殊处理
- 处理文件被其他进程占用的情况
-
性能优化:
- 对于包含大量文件的目录,递归计算大小可能较慢,可以考虑异步处理
- 可以增加目录扫描缓存机制,避免每次全量扫描
-
安全考虑:
- 严格验证目录路径,防止目录遍历攻击
- 限制删除操作的范围,避免误删系统文件
7. 扩展与改进方向
-
配置化:
- 将监控目录、阈值等参数改为配置文件方式
- 支持运行时重载配置
-
通知机制:
- 增加邮件或短信通知功能,在磁盘空间不足时告警
- 集成到现有的监控系统中
-
更智能的清理策略:
- 基于文件最后访问时间而不仅仅是创建时间
- 支持按文件类型、大小等多维度清理策略
-
资源限制:
- 增加CPU和内存使用限制,避免影响其他服务
- 支持IO优先级设置
-
容器化支持:
- 适配容器环境,支持在Docker/Kubernetes中运行
- 增加对容器存储驱动特性的支持
在实际项目中,这个解决方案已经稳定运行了6个月以上,有效管理了数十TB的音视频文件存储。通过合理的参数配置,可以在保证业务需求的前提下,最大化利用有限的存储资源。
