你挂了一个要跑很久的后台任务,去干别的了。半小时后回来,想看看它跑到哪一步——读取输出文件,报"文件不存在"。
现象
读取后台任务的输出文件时报错,大致是:
Error: File does not exist. It may have been deleted by another process.
长任务还在跑,可它已经产出的输出读不到了。如果这个输出是唯一的证据(例如一份耗时几小时的抓取日志),那就等于丢了。
有个很反直觉的点:任务本身可能一切正常、还在往输出里写,只是输出的落点消失了。所以你会看到"进程活着、文件没了"这种矛盾的组合。
顺带一个排查动作:发现输出文件读不到时,先确认任务进程是否还活着。如果进程在、文件却没了,基本就是被清理了,而不是任务提前退出——这个区分能帮你判断"要不要重跑"。
根因:输出落在了"不归你管"的目录
这类后台任务为了方便,会把输出写在应用自己管理的系统临时目录里。临时目录的特点是:
- 由系统或应用统一清理,清理时机不由你决定;
- 多个进程共享这个目录,别的程序可能删掉它认为"过期"的文件;
- 应用重启、会话切换时,目录可能被整体回收。
所以"文件被另一个进程删掉了"这句报错是字面意思,不是 bug。问题在于:你的任务在别的进程的地盘上放东西,而那个进程清理时并不关心你还需要它。
值得注意的是,这个错误是静默的——任务本身可能还在正常跑,只是你读不到它的进度了。等你发现时,往往已经过去了很久。
解决:输出自己接管
思路一句话:重要输出不要交给别人托管,重定向到你自己的路径。
-
把标准输出和错误输出都重定向到项目内的固定文件:
python long_task.py > /srv/project/logs/task.log 2>&1>覆盖、>>追加,按需选;2>&1把错误流也并进来,否则出错信息还是散落在别处。 -
用读取工具去读这个固定路径,不要读应用托管的那份。
-
日志按运行时间或 PID 命名,避免覆盖,方便同时跑多个任务时各自定位:
LOG=/srv/project/logs/task-$(date +%Y%m%d-%H%M%S).log python long_task.py > "$LOG" 2>&1 -
重要任务同时把关键进度写进一个"状态文件"(已完成的数量、最后处理到哪一条),即使日志丢了也能恢复断点。
另外,别忘了定期清理自己的日志目录:接管输出之后,"谁来删旧日志"这件事就落到了你头上。按日期或大小做轮转,既保留证据,又不至于把磁盘塞满。这一步是"自己接管"的配套义务。
还有一个容易忽略的细节:输出被清理,往往和"任务跑了多久"无关,而和"文件晾了多久"有关。 有些清理策略是按"最后访问时间"算的——你越是不去看那个文件,它越可能被判定为过期而回收。所以"长任务 + 长时间不查看"这个组合,恰恰是最容易丢输出的。想避免,就得让输出落在清理策略覆盖不到的地方,或者自己接管它的生命周期。
延伸与预防
这个坑的本质是所有权问题:写在谁管的目录,就受谁的规则支配。判断一个路径安不安全,问三句——
- 谁创建的?(可能是你)
- 谁清理它?(很可能不是你)
- 清理时会不会征求你同意?(不会)
/tmp、系统临时目录、应用缓存目录,全都属于"别人清理"。真正该放"必须留着的产物"的地方是:项目目录、专用的数据目录、你自己配置的日志目录。凡是"丢了会很麻烦"的文件,都要放在清理逻辑覆盖不到的地方。
一句话教训
临时目录不归你管,重要输出自己接管。