MAGAZINE
ルーターマガジン
Rails7.1の非同期メソッドを試してみた
ビッグデータを速く見せる
明けましておめでとうございます。 アドクロールクラウド開発チームの増田です。
弊社ではインターネット広告クリエイティブ収集サービス「アドクロール」を提供しています。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.rb
やproduction.rb
に追加しないと、非同期にクエリが流れません。デフォルトでは4並列が上限のようですが、サーバーリソースと要相談かと思います。
config.active_record.async_query_executor = :global_thread_pool
参考:Rails アプリケーションの設定項目 - Railsガイド
非同期でSQLが流れると、Railsのログに吐かれるSQLの頭にASYNC
が付くようになります。
レスポンスタイム比較
以下の条件で10回のレスポンスタイムを計測し平均を同期/非同期で比較しました。
title
にBeyond
を含む(title like '%Beyond%'
)title ASC
でORDER BY
非同期処理をした方が200msほどレスポンスが速くなりました。
単位:ms | 同期 | 非同期 |
---|---|---|
平均値 | 950 | 747 |
終わりに
最後までお読みいただき、ありがとうございました。Rails最新(2024月1月時点)の機能を使って実験してみることで、分かることもたくさんあるかと思います。Rails7.1で言うと、Trilogyなんかもmysql2と比較して記事にしたいなと考えてます。Rails楽しい!!
CONTACT
お問い合わせ・ご依頼はこちらから