Rails 5.0.5应用占用Dropbox文件无法删除问题排查求助
为什么文件会被应用持续占用?
核心问题出在你list_dropbox_images方法里的这行代码:
{ name: child.basename.to_s, time: File.new(child).ctime }
当你调用File.new(child)时,Ruby会创建一个File实例并打开对应的文件句柄,但你没有显式关闭这个句柄。虽然Ruby的垃圾回收(GC)理论上会在对象不再被引用时自动关闭句柄,但生产环境下Passenger的Ruby应用进程是长期驻留内存的,这些File实例可能不会被及时回收,导致文件句柄一直处于打开状态——这正是sudo lsof显示文件被ruby进程占用的原因。
而且你的应用会定期调用这个方法列出dropbox里的图片,每次调用都会打开一批文件句柄却不关闭,最终导致dropbox里的所有图片都被进程持有,自然无法删除。
不停止服务器的解决办法
1. 临时释放被占用的文件句柄(无需重启整个服务器)
Passenger支持优雅热重启,不会中断正在处理的请求。你只需要在服务器上执行这条命令:
touch /var/www/app/current/tmp/restart.txt
这会触发Passenger重启你的Rails应用进程:旧进程会处理完当前请求后退出,新进程启动后不会持有之前的文件句柄,此时你应该就能顺利删除dropbox里的文件了。
2. 修复代码,彻底避免这个问题
把获取文件创建时间的方式改成不需要打开文件句柄的写法——利用Pathname自带的ctime方法(因为dropbox_dir.children返回的是Pathname对象),直接读取文件元数据无需打开文件:
优化后的代码:
def self.list_dropbox_images # returns array of hashes with keys "name" (only JPGs) and "time" (creation time) # File::FNM_CASEFOLD makes it case-insensitive. Nice. dropbox_dir.children.map { |child| if child.fnmatch?('*.jpg', File::FNM_CASEFOLD) { name: child.basename.to_s, time: child.ctime } end }.compact end
修改代码后部署到生产环境,再执行一次touch tmp/restart.txt让新代码生效,以后就不会再出现文件被长期占用的问题了。
为什么本地无法复现?
本地开发环境的Rails进程通常不会长期驻留(比如用rails s时,代码变更会自动重启进程),而且垃圾回收的触发时机更频繁,文件句柄会被及时释放。但生产环境的Passenger进程是持续运行的,所以这个问题会明显暴露出来。
内容的提问来源于stack exchange,提问作者Jacob

