Ruby 分块创建 tar 球以避免内存不足错误
Posted
技术标签:
【中文标题】Ruby 分块创建 tar 球以避免内存不足错误【英文标题】:Ruby create tar ball in chunks to avoid out of memory error 【发布时间】:2013-05-21 12:04:05 【问题描述】:我正在尝试重新使用以下代码来创建一个 tar 球:
tarfile = File.open("#Pathname.new(path).realpath.to_s.tar","w")
Gem::Package::TarWriter.new(tarfile) do |tar|
Dir[File.join(path, "**/*")].each do |file|
mode = File.stat(file).mode
relative_file = file.sub /^#Regexp::escape path\/?/, ''
if File.directory?(file)
tar.mkdir relative_file, mode
else
tar.add_file relative_file, mode do |tf|
File.open(file, "rb") |f| tf.write f.read
end
end
end
end
tarfile.rewind
tarfile
只要涉及小文件夹,它就可以正常工作,但任何大文件夹都会失败并出现以下错误:
Error: Your application used more memory than the safety cap
如何分块执行以避免内存问题?
【问题讨论】:
是否有不能执行系统tar命令调用的原因? 是的,应用程序必须是可移植的。 您使用的是什么 ruby 环境?如果是 JRuby,可以通过 -J-Xmx 启动选项增加 JVM 可用的虚拟内存量;详细在这里:***.com/questions/6910429/… 我知道我可以增加 JVM 内存,但我希望应用程序能够处理它。想象一下,如果我必须创建一个千兆字节的 tar。 【参考方案1】:看起来问题可能出在这一行:
File.open(file, "rb") |f| tf.write f.read
您正在通过执行f.read
“啜饮”您的输入文件。 slurping 意味着整个文件被读入内存,这根本不可扩展,并且是使用 read
没有长度的结果。
相反,我会做一些事情来读取和写入块中的文件,这样你就有了一致的内存使用。这读取 1MB 块。您可以根据自己的需要进行调整:
BLOCKSIZE_TO_READ = 1024 * 1000
File.open(file, "rb") do |fi|
while buffer = fi.read(BLOCKSIZE_TO_READ)
tf.write buffer
end
end
这是the documentation 对read
的评价:
如果length是一个正整数,它会尝试读取length字节而不进行任何转换(二进制模式)。它返回 nil 或长度为 1 到 length 个字节的字符串。 nil 表示它在开始时遇到了 EOF。 1 到长度为 1 字节的字符串意味着它在读取结果后遇到了 EOF。长度字节字符串意味着它不符合 EOF。结果字符串始终为 ASCII-8BIT 编码。
另一个问题是您似乎没有正确打开输出文件:
tarfile = File.open("#Pathname.new(path).realpath.to_s.tar","w")
由于"w"
,您正在以“文本”模式编写它。相反,您需要以二进制模式编写 "wb"
,因为 tarball 包含二进制(压缩)数据:
tarfile = File.open("#Pathname.new(path).realpath.to_s.tar","wb")
重写原始代码,使其更像我想看到的那样,结果:
BLOCKSIZE_TO_READ = 1024 * 1000
def create_tarball(path)
tar_filename = Pathname.new(path).realpath.to_path + '.tar'
File.open(tar_filename, 'wb') do |tarfile|
Gem::Package::TarWriter.new(tarfile) do |tar|
Dir[File.join(path, '**/*')].each do |file|
mode = File.stat(file).mode
relative_file = file.sub(/^# Regexp.escape(path) \/?/, '')
if File.directory?(file)
tar.mkdir(relative_file, mode)
else
tar.add_file(relative_file, mode) do |tf|
File.open(file, 'rb') do |f|
while buffer = f.read(BLOCKSIZE_TO_READ)
tf.write buffer
end
end
end
end
end
end
end
tar_filename
end
BLOCKSIZE_TO_READ
应该在文件的顶部,因为它是一个常量并且是“可调整的” - 比代码主体更可能被更改。
该方法返回 tarball 的路径,而不是像原始代码那样的 IO 句柄。使用IO.open
的块形式会自动关闭输出,这将导致任何后续open
自动rewind
。我更喜欢传递路径字符串而不是文件的 IO 句柄。
我还将一些方法参数包裹在括号中。虽然 Ruby 中的方法参数不需要括号,而且有些人避开了括号,但我认为它们通过分隔参数的开始和结束位置使代码更易于维护。当您将参数和块传递给方法时,它们还可以避免混淆 Ruby——这是众所周知的错误原因。
【讨论】:
作为一般说明,对使用read
而不是逐行IO 或“分块”read
的代码要非常怀疑。我们一次又一次地看到人们填满他们的 RAM 并让他们的机器跪下的问题,因为他们在生产中遇到了一个大文件。逐行 IO 速度极快,简化了很多文本文件处理问题,所以先抓住它。如果它是二进制的,请先进行块读取。【参考方案2】:
minitar 看起来像是写入流,所以我认为内存不会成为问题。以下是 pack
方法的注释和定义(截至 2013 年 5 月 21 日):
# A convenience method to pack files specified by +src+ into +dest+. If
# +src+ is an Array, then each file detailed therein will be packed into
# the resulting Archive::Tar::Minitar::Output stream; if +recurse_dirs+
# is true, then directories will be recursed.
#
# If +src+ is an Array, it will be treated as the argument to Find.find;
# all files matching will be packed.
def pack(src, dest, recurse_dirs = true, &block)
Output.open(dest) do |outp|
if src.kind_of?(Array)
src.each do |entry|
pack_file(entry, outp, &block)
if dir?(entry) and recurse_dirs
Dir["#entry/**/**"].each do |ee|
pack_file(ee, outp, &block)
end
end
end
else
Find.find(src) do |entry|
pack_file(entry, outp, &block)
end
end
end
end
自述文件中编写 tar 的示例:
# Packs everything that matches Find.find('tests')
File.open('test.tar', 'wb') |tar| Minitar.pack('tests', tar)
自述文件中编写 gzip tar 的示例:
tgz = Zlib::GzipWriter.new(File.open('test.tgz', 'wb'))
# Warning: tgz will be closed!
Minitar.pack('tests', tgz)
【讨论】:
我见过 minitar,但我不敢使用它,因为它似乎没有得到维护。 是的,它看起来好像有一段时间没有被碰过。但从好的方面来说,没有未解决的问题,整个库只是一个文件。如果归根结底,源代码看起来不会太难分叉和维护自己的分支。 我对 ruby 还很陌生,因此认为现在不是这样做的合适时机。以上是关于Ruby 分块创建 tar 球以避免内存不足错误的主要内容,如果未能解决你的问题,请参考以下文章
使用大型数据结构时,避免 Java(eclipse) 中的“内存不足错误”?