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を使用した低レベルな受信を行っていることが分かります。
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