読者です 読者をやめる 読者になる 読者になる

RubyでHTTPリクエストを並列化する

この記事は、「Speee Advent Calendar 2016」の13日目です。 12日目は、_miyachikより、「HAProxyを使用した汎用的ABテスト基盤への挑戦」です。

RubyでHTTPリクエストを並列化したくないですか?

例えば、リクエストしてからレスポンスが返るまで、1リクエストにつき100msかかるとします。 それを順次実行で10回行うと1000msかかってしまいます。 その10回はほとんどCPUを使用してないのにもかかわらず、ロックされてしまうので、他からリソースが使われない限り、リソースが非常に無駄になりがちです。

上記ケースにて、HTTPリクエストを並列化すると、10回リクエストしても100msで全ての通信が完了します。

どんな時に並列化したくなる?

今回はクローラーで並列化したい前提とします。 例えば、クローラーを使って、ページ内のaタグのURLを全て取得したいとします。

直列での実行

require 'open-uri'
10.times.map do |num|
  open("http://example.com/articles/#{num}") do |file| # アクセスに100msかかる想定
    urls = Nokogiri::HTML.parse(html).xpath('//a/@href').map(&:value)
  end
end

上記のようなコードだと100msかかるページに10回アクセスするので、HTTPリクエスト時間だけで1000msは確実にかかってしまいます。

並列での実行

並列化する方法として様々な方法がありますが、 今回はTyphoeusというGemを使って、HTTPリクエストの並列化を実現しようと思います。

require 'typhoeus'
hydra = Typhoeus::Hydra.new
requests = 10.times.map do |num|
  request = Typhoeus::Request.new("http://example.com/articles/#{num}")
  hydra.queue request
  request
end
hydra.run
urls = requests.map { |request|
    urls = Nokogiri::HTML.parse(request.response.body).xpath('//a/@href').map(&:value)
}

Typhoeus を使うと上記のようなコードを書くだけで、HTTPリクエストの並列化が実現できます。

Typhoeus

あまり、Typhoeus についての記事がないので、 使い方について説明します。

※ほとんどはGitHubのREADMEを読めば理解できると思われる

基本的な考え方としてはTyphoeus::HydraオブジェクトにTyphoeus::Requestを突っ込む感じです。

簡単な使い方

単一のリクスエストもこのように実行することが可能です。

Typhoeus.get("www.example.com", followlocation: true)

並列化された実行もこのように実行できます。

hydra = Typhoeus::Hydra.new
10.times.map{ hydra.queue(Typhoeus::Request.new("www.example.com", followlocation: true)) }
hydra.run # runが実行された時点でURLに対してアクセスする

Requestの結果Responseを扱いたい場合

Request後の動作を定義することも可能です。 Request完了時の動作については以下のように定義します。

hydra = Typhoeus::Hydra.hydra
request = Typhoeus::Request.new("www.example.com", followlocation: true)
request.on_complete do |response|
  if response.success?
    # 成功した場合の処理
  elsif response.timed_out?
    # タイムアウトした時の処理
    log("got a time out")
  elsif response.code == 0
    # なにかしらの問題があり、HTTP Requestの結果を取得できなかった場合の処理
    log(response.return_message)
  else
    # その他の処理
    log("HTTP request failed: " + response.code.to_s)
  end
end
hydra.queue(request)
hydra.run

# 結果を後で参照することも可能
puts request.response.body
responseの値の参照方法
response = request.response
response.code
response.total_time
response.headers
response.body
最大並列数の設定
  • 並列数を設定することが可能
Typhoeus::Hydra.new(max_concurrency: 20)

※デフォルトは200

  • get以外でのアクセスも可能
Typhoeus.get("www.example.com")
Typhoeus.head("www.example.com")
Typhoeus.put("www.example.com/posts/1", body: "whoo, a body")
Typhoeus.patch("www.example.com/posts/1", body: "a new body")
Typhoeus.post("www.example.com/posts", body: { title: "test post", content: "this is my test"})
Typhoeus.delete("www.example.com/posts/1")
Typhoeus.options("www.example.com")

その他の並列化の方法

自分でスレッドを生成したり、ParallelなどのGemを使って、 複数スレッド、複数プロセスを立ち上げる方法がありますが、どちらにしても通信にしか使わないのにも関わらず、 スレッドやプロセスを立ち上げるのは非常にコストが高いです。 ※100並列をプロセスで実現しようとするとRubyプロセスが100個必要のため、Rubyプロセス100個分のメモリが必要

最後に

ドキュメントは以下のページにまとまっているので、大体見ればわかるものになってると思います。 Documentation for typhoeus/typhoeus (master)

また、今回の方法だと複数プロセスで実行できるわけではないので、CPUが複数個ある場合かつ CPUを使いたい場合はプロセスを立ち上げるなどの工夫が必要だと思います。

明日はkana_nakanoから「エンジニア採用を6ヶ月間やってみて(仮)」です お楽しみにー