#HTTPClientで大きいファイルをダウンロードする時の作法
いけないコード
RubyのHTTPClientモジュールを使ってOpenStackオブジェクトストレージクライアントを作成していた時に
こういうゴミコードを書いていた。
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|
ブロックがやってくれてると思っていたがそんなことはなかったぜ・・・。
## いけてるコード
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だろうが大きいファイルもちゃんとダウンロードできる。
もう少しちゃんとしたコード
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を設定するのがいいだろう。