LoginSignup
21
21

More than 5 years have passed since last update.

Ruby HTTPClientでギガクラスの大きいファイルをHTTPダウンロードする時の注意点

Posted at

HTTPClientで大きいファイルをダウンロードする時の作法

いけないコード

RubyのHTTPClientモジュールを使ってOpenStackオブジェクトストレージクライアントを作成していた時に
こういうゴミコードを書いていた。

client1.rb
http_client = HTTPClient.new
open(dest_file, 'wb') do |file|
 file.write  http_client.get_content(URI.parse(URI.encode(url)), query, auth_header) 
end

このコードでもある程度は動くことは動くが、メモリ8GBを積んでいるマシンで、7GBのファイルをダウンロードしようとすると以下のようなエラーが起きた。

[root@10-134-25-12 rabbit_swift]# bundle exec ruby -I./lib bin/get_object.rb -t /nico_archive/anime_c123_2015_02_19PLIT1G.zip -d ./private/ -c ../Chino/conf/conf.json
Content-Length = 7548785363
Etag = "3772025e360d2d99fb813d151ba7d875"
Accept-Ranges = bytes
Last-Modified = Sat, 21 Feb 2015 09:23:37 GMT
X-Object-Manifest = nico_archive/anime_c123_2015_02_19_SPLIT1G.zip_
X-Timestamp = 1424510616.48291
X-Static-Large-Object = true
Content-Type = application/json
X-Trans-Id = tx17d36543427f47ac915f3-0054e85060
Date = Sat, 21 Feb 2015 09:31:12 GMT
/root/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/httpclient-2.6.0.1/lib/httpclient.rb:1201:in `block in do_get_ock': failed to allocate memory (NoMemoryError)
from /root/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/httpclient-2.6.0.1/lib/httpclient/session.rb:960: `read_body_length'
from /root/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/httpclient-2.6.0.1/lib/httpclient/session.rb:698: `get_body'
from /root/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/httpclient-2.6.0.1/lib/httpclient.rb:1196:in `do_t_block'
from /root/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/httpclient-2.6.0.1/lib/httpclient.rb:974:in `blocin do_request'
from /root/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/httpclient-2.6.0.1/lib/httpclient.rb:1082:in `proct_keep_alive_disconnected'
from /root/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/httpclient-2.6.0.1/lib/httpclient.rb:969:in `do_ruest'
from /root/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/httpclient-2.6.0.1/lib/httpclient.rb:1053:in `folw_redirect'
from /root/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/httpclient-2.6.0.1/lib/httpclient.rb:625:in `get_ntent'
from /mnt/code/rabbit_swift/lib/rabbit_swift/client.rb:79:in `block in get_object'
from /root/.rbenv/versions/2.1.5/lib/ruby/2.1.0/open-uri.rb:36:in `open'
from /root/.rbenv/versions/2.1.5/lib/ruby/2.1.0/open-uri.rb:36:in `open'
from /mnt/code/rabbit_swift/lib/rabbit_swift/client.rb:78:in `get_object'
from bin/get_object.rb:48:in `<main>' 

NoMemoryErrorである。

何が起きているのか

C言語などでSocketプログラムをゴリゴリ書いた経験があると予想はつくのだが、file.write http_client.get_content(URI.parse(URI.encode(url)), query, auth_header)
のコードだと、HTTPでダウンロードしたファイルをメモリに一時的に全部格納した後に、一気にファイルを書き込むというフローになってしまう。
なので動いているマシンのメモリ限界値に近いファイルをダウンロードしようとすると上記のエラーが発生してしまう。
通常のSocketやHTTPプログラムであれば基本的にHTTPで受け取ったデータをファイルに書き込む場合はForでくるくる小回ししながらファイルに定期的に書き込むのが作法となっている。
私はてっきりRubyはその部分を open(dest_file, 'wb') do |file|ブロックがやってくれてると思っていたがそんなことはなかったぜ・・・。

 いけてるコード

client2.rb
http_client = HTTPClient.new
open(dest_file, 'wb') do |file|
  http_client.get_content(URI.parse(URI.encode(url)), query, auth_header) do |chunk|
    file.write chunk
  end
end

http_client.get_contentをブロックにすることでchunk(チャンク)単位で大きいデータを小刻みにファイルに書き込むコードに修正する。
これでHTTPストリームを小刻みにファイルに書き込むため保持するメモリはほとんど要らなくなる。
それこそ100Gだろうが大きいファイルもちゃんとダウンロードできる。

もう少しちゃんとしたコード

client3.rb
http_client = HTTPClient.new
http_client.receive_timeout = 60 * 120
open(dest_file, 'wb') do |file|
  http_client.get_content(URI.parse(URI.encode(url)), query, auth_header) do |chunk|
    file.write chunk
  end
end

大きいファイルの場合HTTPClientのデフォルトのタイムアウトを超えてしまうため、いい感じにreceive_timeoutを設定するのがいいだろう。

21
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
21