MAGAZINE

ルーターマガジン

Ruby

Rails7.1の非同期メソッドを試してみた

2024.01.10
Pocket

ビッグデータを速く見せる

明けましておめでとうございます。 アドクロールクラウド開発チームの増田です。

弊社ではインターネット広告クリエイティブ収集サービス「アドクロール」を提供しています。2023年12月時点で総取得件数6.7億件を超え、今もなお増え続けており、ビッグデータをリアルタイムかつ高速に扱うことの難しさを感じています。

アドクロールは

  • ボリューム → クローラー
  • リアルタイム → バッチ処理
  • データ操作性 → Webアプリ(Rails)

の棲み分けで運用されています。DBからデータを取得し、画面に表示する責務を持つWebアプリを開発する身として、パフォーマンスの追求は腕の見せどころです。 今回はビックデータを扱う上でのRails便利メソッドを紹介します。

Rails7以降で追加された非同期メソッド

Rails7.0からload_asyncメソッドが追加されました。

また、Rails7.1からはActiveRecord::Relationを返さない、集計系のクエリ(sum, count)でも非同期メソッドがサポートされました。

Active Record APIに重要な改善が導入され、非同期クエリのサポートが拡張されました(#44446)。この拡張により、特に集計メソッド(count、sumなど)や、(Relationでない)単一レコードを返すメソッドなど、あまり速くないクエリをより効率的に処理するニーズに応えます。 新しいAPIには、以下の非同期メソッドが含まれます。 async_count async_sum async_minimum async_maximum async_average async_pluck async_pick async_ids async_find_by_sql async_count_by_sql

参考:Ruby on Rails 7.1 リリースノート - Railsガイド

これらメソッドの追加により、重くなりがちなLimit Offset Paginationのページ数計算などもコンテンツの取得と計算が並列に実行できてパフォーマンスが向上しそうだなと思ってます。

# 同期的なカウント
published_count = Post.where(published: true).count # => 10

# 非同期なカウント
promise = Post.where(published: true).async_count # => #<ActiveRecord::Promise status=pending>
promise.value # => 10

本記事では非同期メソッドの1つであるasync_countを用いて、同期的にSQLを流した場合との比較をしていきます!

パフォーマンス検証

今回検証するにあたってサンプルアプリを作成しました。titleを検索して部分一致するBookの一覧と、各カラムのユニーク件数を表示するシンプルなアプリです。検索にはransackを使用しています。

ソースコードは弊社GitHubにアップロードしました。詳細はそちらをご覧ください。

MariaDB [async_test_development]> desc books;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| title      | varchar(255) | YES  | MUL | NULL    |                |
| author     | varchar(255) | YES  |     | NULL    |                |
| publisher  | varchar(255) | YES  |     | NULL    |                |
| created_at | datetime(6)  | NO   |     | NULL    |                |
| updated_at | datetime(6)  | NO   |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+

TL;DR

  • 20%ほどパフォーマンスが向上した
  • 並列上限数はサーバーリソースと要相談

開発環境

❯ ruby -v
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [x86_64-linux]

❯ rails -v
Rails 7.1.2

❯ mariadb --version
mariadb  Ver 15.1 Distrib 10.6.12-MariaDB, for debian-linux-gnu (x86_64) using  EditLine wrapper

テストデータの作成

faker-rubyを使用して、サンプルデータを100万件作成しました。

10.times do
  books = []
  100_000.times do
    books << {
      title: Faker::Book.title,
      author: Faker::Book.author,
      publisher: Faker::Book.publisher
    }
  end
  Book.insert_all(books)
end
MariaDB [async_test_development]> SELECT COUNT(1) FROM books;
+----------+
| COUNT(1) |
+----------+
|  1000000 |
+----------+
1 row in set (0.622 sec)

主要コード抜粋

非同期処理を行う場合は以下のように記述します。

class BooksController < ApplicationController
  # GET /books
  def index
    @q = Book.ransack(params[:q])
    @q.sorts = 'title asc'
    @books = @q.result
    title_count_promise = @books.distinct.async_count(:title)
    author_count_promise = @books.distinct.async_count(:author)
    publisher_count_promise = @books.distinct.async_count(:publisher)
    @title_count = title_count_promise.value
    @author_count = author_count_promise.value
    @publisher_count = publisher_count_promise.value
  end
end

非同期メソッドを使うときの注意点

以下の記述をdevelopment.rbproduction.rbに追加しないと、非同期にクエリが流れません。デフォルトでは4並列が上限のようですが、サーバーリソースと要相談かと思います。

config.active_record.async_query_executor = :global_thread_pool

参考:Rails アプリケーションの設定項目 - Railsガイド

非同期でSQLが流れると、Railsのログに吐かれるSQLの頭にASYNCが付くようになります。

レスポンスタイム比較

以下の条件で10回のレスポンスタイムを計測し平均を同期/非同期で比較しました。

  • titleBeyondを含む(title like '%Beyond%'
  • title ASCでORDER BY

非同期処理をした方が200msほどレスポンスが速くなりました。

単位:ms 同期 非同期
平均値 950 747

終わりに

最後までお読みいただき、ありがとうございました。Rails最新(2024月1月時点)の機能を使って実験してみることで、分かることもたくさんあるかと思います。Rails7.1で言うと、Trilogyなんかもmysql2と比較して記事にしたいなと考えてます。Rails楽しい!!

Pocket

CONTACT

お問い合わせ・ご依頼はこちらから