こんにちは。技術戦略室の安田です。今回自社サイトの外形監視も兼ねてRailsでWebページの監視システムを作りました。
その際に考えなければいけなかったこととおすすめのライブラリを紹介します。
CyberOwlではaukanaという動画配信サービス(VOD)の比較サイトを運営しています。
aukanaはHuluやNetflixなどで配信中の映画やドラマが検索できるサイトで、DBに登録されている作品の数だけページ(URL)が増えていく仕様になっています。
そのページ数は10万件にも及び、何も工夫を凝らさず全ページを閲覧しようものなら、1リクエストに対して事後処理を含めて0.5秒だとすると13時間かかる計算になります。
バッチサーバではそれ以外の仕事もさせたいので、改善しなくてはいけません。
Railsはマルチスレッドなので、比較的簡単にキューとスレッドを用いた並行リクエストをスクラッチで実装が可能です。
が、今回はこの辺のリクエストを良い感じに捌いてくれる `Typhoeus`というライブラリを使用しました。
これの強みは並行リクエストのみでなく、RailsデフォルトのHTTPリクエストモジュールの`open-uri`の苦手な日本語URL(マルチバイトURL)もエラー無しでリクエストできる点です。
require 'typhoeus'
module Fetcher
# 複数のHTTPリクエストを同時に行う
# @params [Hash] queries URLなどの情報
# @option queries [String] :url リクエストするURL
# @option queries [Symbol] :req_method :get,:post,:headなど
# @option queries [Integer] :timeout タイムアウト
# @params [Integer] max_concurrency 並行処理の数
# @params [Block] oncomplete リクエスト完了した時のコールバック
# @yieldparam [Typhoeus::Response] Httpレスポンスをコールバックに渡す
def fetch_queries(queries:, max_concurrency:, &oncomplete)
# キューみたいなもの。最大10個並行に動かす
hydra = ::Typhoeus::Hydra.new(max_concurrency: [max_concurrency, 10].min)
queries.each {|q|
url = q[:url]
req_method = q[:req_method] || :get
timeout = q[:timeout] || 60
opts = {
method: req_method,
timeout: timeout,
connecttimeout: timeout,
followlocation: true,
ssl_verifypeer: false,
ssl_verifyhost: 0,
cache: false,
}
# リクエストの作成
req = ::Typhoeus::Request.new(url, opts)
# リクエストが完了した時のコールバック(DB保存等の処理)
req.on_complete {|res|
oncomplete.yield(res)
}
# キューに登録
hydra.queue(req)
}
# 実行
hydra.run
end
end
この実装で、1回の処理で10個の並行リクエストが可能になりました。
ここまでの実装でも13時間の処理が2時間程度まで下がったと思われます。
これ以上の高速化を目指すなら、ジョブキューサーバーと複数台のHTTPリクエスト処理を実行するバッチサーバーを建てる方法があります。
今回のシステムではActive Job(Sidekiq)を利用したジョブキューサーバーと、バッチサーバーを何台か建てて高速化しました。
# HTTPリクエストを複数のジョブに小分けするジョブ
# cronから定時処理としてperformを呼び出す
class HttpRequestQueueJob < ActiveJob::Base
queue_as :default
# HTTPリクエストのジョブを登録
# @params [Array] args 不要
def perform(*args)
# 大量の行が取れるので find_in_batches を利用する。
WebPage.where(host: 'doga.hikakujoho.com').find_in_batches{|webpages|
HttpRequestJob.perform_later(ids: webpages.ids)
}
end
end
# HTTPリクエストを実際に行うジョブ
class HttpRequestJob < ActiveJob::Base
queue_as :http_request
# HTTPリクエストの結果をDBに保存
# @params [Hash] args リクエストを行うWebPageのIDが格納されている
# @option args [Array<Integer>] :ids WebPageのID
# @note 例なのでこの中で処理を書いているが、実際はWebPageモデル内で行うのが望ましい
def perform(**args)
ids = args[:ids]
queries = WebPage.where(id: ids).map {|webpage|
{ url: webpage.url, req_method: :get, timeout: 60 }
}
::Fetcher.fetch_queries(queries: queries, max_concurrency: 10) {|res|
# コールバック。DB保存などの処理
}
end
end
あとはジョブを走らせるためにSidekiqの起動スクリプトをDockerなどで走らせて完了です。
下の例では環境変数`SIDEKIQ_ENV`が`parallel`のサーバーを複数台走らせることになります。
if [ "$SIDEKIQ_ENV" = "default" ]; then
sidekiq -q default 2>&1 &
elif [ "$SIDEKIQ_ENV" = "parallel" ]; then
sidekiq -q http_request,20 -q other_pipeline,15 2>&1 &
fi
今回はサーバーの並列と、並行HTTPリクエストライブラリを使って高速Web監視システムの構築を行いました。このシステムはWeb監視以外にもWebクローラーなどにも応用ができるかと思います。
ただし、これを運用する上で注意点が二つあります。
1つ目は、並列バッチ処理サーバーを作りすぎて同じDBテーブルへの更新時にデッドロックが起きないように保存処理に気をつけること。
2つ目は、外部サイトのクローラーに転用する場合は同時接続数を管理して相手のサーバーに負荷をかけないようにすることです。
両方とも自社、もしくは他人に迷惑をかける脅威になりかねないのでモニタリングや通知のシステムを併用するのが望ましいです。
ぜひ大量のHTTPリクエストを捌かなければいけない時に参考にしてみてください。
※2020年7月31日時点