RestClientのような内部でNet::HTTP
を使っているような場合も起こります。
タイムアウトされないコード
pry(main)> require "benchmark"
=> true
pry(main)> uri = URI.parse(NOT_TIMEOUT_URL)
=> #<URI::HTTP http://023sos.com>
pry(main)> time = Benchmark.measure do
pry(main)* Net::HTTP.start(uri.host, uri.port) do |http|
pry(main)* http.open_timeout = 1
pry(main)* http.read_timeout = 1
pry(main)* http.get(uri.request_uri)
pry(main)* end
pry(main)* end
=> #<Benchmark::Tms:0x00000002655958 @cstime=0.0, @cutime=0.0, @label="", @real=5.746352325000316, @stime=0.0, @total=0.0, @utime=0.0>
[20] pry(main)> p time.real
5.746352325000316
=> 5.746352325000316
5.746352325000316
...タイムアウトを1秒にしているはずですが、6秒弱掛かっているではありませんか。タイムアウトがうまく機能していないようです。
色々と調べた結果、名前解決に時間がかかっている場合は、タイムアウトされないことが発覚。
timeout による割り込みは Thread によって実現されています。 C 言語レベルで実装され、 Ruby のスレッドが割り込めない処理に対して timeout は無力です。 そのようなものは実用レベルでは少ないのですが、 Socket などは DNSの名前解決に時間がかかった場合割り込めません (resolv-replace を使用する必要があります)。 その処理を Ruby で実装しなおすか C 側で Ruby のスレッドを意識してあげる必要があります。
resolv-replaceを使ってみる
pry(main)> require "benchmark"
=> true
pry(main)> require "resolv-replace"
=> true
pry(main)> uri = URI.parse(NOT_TIMEOUT_URL))
=> #<URI::HTTP http://023sos.com>
pry(main)> time = Benchmark.measure do
pry(main)* timeout(1) do
pry(main)* Net::HTTP.start(uri.host, uri.port) do |http|
pry(main)* http.open_timeout = 1
pry(main)* http.read_timeout = 1
pry(main)* http.get(uri.request_uri)
pry(main)* end
pry(main)* end
pry(main)* end
=> => #<Benchmark::Tms:0x00000002416160 @cstime=0.0, @cutime=0.0, @label="", @real=0.8125761109995437, @stime=0.0, @total=0.0, @utime=0.0>
[20] pry(main)> p time.real
0.8125761109995437
=> 0.8125761109995437
なぜか早くなった影響でタイムアウトされず、、、
再度、同じように実行する。
Timeout::Error: execution expired
無事タイムアウトされました!!
resolv-replaceとは
resolv-replaceを使うと、libcのresolverをRubyで実装しているResolveクラスに置き換えることができます。
resolverとは
リゾルバとは、IPアドレスとドメイン名を結びつけるDNSにおいて、ネームサーバにホスト名を通知してIPアドレスの検索を依頼したり、その逆を依頼したりするクライアント側のプログラム。
アプリケーションソフトがIPアドレスやホスト名を必要とする場合には、通常リゾルバを介して名前解決が行われる。
リゾルバは一つ以上のネームサーバのアドレスを知っており、そのネームサーバに問い合わせを行い、返ってきた答えをアプリケーションに渡す。
なぜ早くなったのか
詳しく調べたわけではないので、憶測です。
Resolve::DNSは初期設定のままでは、/etc/resolv.conf
もしくはプラットフォーム固有のDNS設定を利用します。
私のlocal環境ではGoogleのDNS(8.8.8.8, 8.8.4.4)を使うようになっており、このDNSを利用したため早くなったと思われます。
そこで、stagingやproductionの環境とlocalの環境とで利用するDNSが変わってしまう可能性があり、注意が必要です。
考察
- Crawlerを作るときは、DNSを変更したり、タイムアウトができたりするので、resolv-replaceを使うと幸せになれる。
- 環境の差異によって、DNSが変わってしまう恐れがあるので、resolv-replaceを使うときは、初期設定のまま使うことは避けた方が良いと思われる。
参考
Ruby の Net::HTTP のタイムアウトにハマって、結局 Timeout について調べることになった件
Ruby 2.1.0 リファレンスマニュアル Timeoutモジュール
Ruby 2.1.0 リファレンスマニュアル Resolv::DNSクラス
http://morizyun.github.io/blog/open-uri-timeout-ruby/
[Ruby] 例えば、DNS Resolver をすげかえる
リゾルバ