HTTPClientを利用したダウンロードでファイルの欠損が出た話。
結果的にRubyもHTTPClientも関係無かったんですけど、最初全く原因がわかりませんでした。

症状

HTTPClientを使用してあるURLからファイルをダウンロードしつつ、進捗状況を確認していました。

require 'httpclient'

client = HTTPClient.new
open('temp', 'wb') do |file|
  client.get_content('<url>') do |chunk|
    # 進捗状況を確認するちょい重い処理
    file.write chunk
  end
end

処理が完了した時、処理自体は正常に終了したように見えましたが、ダウンロードされたファイルを見てみると1/10もダウンロードできていませんでした。また、「ちょい重い処理」を外して再度実施したところ、ファイルは欠損なくダウンロードできました。

正確に計測したわけではありませんが、「ちょい重い処理」の代わりにsleep 0.1を入れてもダメでした。少なくとも0.1秒の遅延はアウトのもよう。

原因

全く原因が分からず、藁にも縋る思いでWiresharkでキャプチャしていたら、TCP ZeroWindowという異常が多発しているのを発見。
TCP ZeroWindow自体はエラーではなくサーバーからのデータ送信に対しクライアントの受信処理が捌き切れない場合に発生するもので、サーバーが「受信遅すぎもう知らない!」とTCP RSTを送ってしまう現象。chunkのデフォルトサイズが16384バイトなのでその度に「ちょい重い処理」が走ってたらそら詰まるよねっていう状況だった。

処方箋

原因から分かるようにそもそも「ちょい重い処理」挟むなよっていう話なのですが、ある程度の「ちょい重い処理」ならば許容できるようにする方向で解決を図りたいと思います。

HTTPClientのget_contentメソッドのソースを追っていくと、socketを使用した低レベルな受信を行っていることが分かります。

lib/httpclient/session.rb
def read_body_chunked(&block)
  ... 省略
    @socket.read(@chunk_length, buf)
  ... 省略
end

一度のread処理で@chunk_length(=16384)分のデータをバッファから読み取り、そのたびに「ちょい重い処理」が走る状態です。
対策は単純に、バッファから一度に読み取る量を増やしてあげます。(「ちょい重い処理」が走る機会を減らす)
以下ではread_block_sizeを10倍にすることで、一度に処理するchunkサイズを増やしています。

require 'httpclient'

client = HTTPClient.new
client.read_block_size *= 10 # <- 追加

open('temp', 'wb') do |file|
  client.get_content('<url>') do |chunk|
    # 進捗状況を確認するちょい重い処理
    file.write chunk
  end
end

「ちょい重い処理」の代わりにsleep 0.5を入れても正常にダウンロードできるような状態にはなりました。

参考

https://qiita.com/AKB428/items/a0a7c269e0e910dc18d4
https://wiki.wireshark.org/TCP%20ZeroWindow
https://blog.goo.ne.jp/nobuoman/e/e2da59b317a98926cfa0398ebbe547e7

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.