1718637755
2024-06-17 11:13:08
非同期プログラミングにより、アプリの速度を上げることができます。Ruby on Rails で非同期を使用してアプリを高速化する方法を紹介します。Ruby の例もありますが、原則はどの言語にも当てはまります。
例を 2 つの基本原則に分類します。最初の原則は次のとおりです。
物事をできるだけ遅らせましょう。怠けることは必ずしも悪いことではありません。実際には、それはいくつかのことを意味します。
次のようなメソッドを使用する場合は注意してください。 _nowこれらは非同期で実行できる処理の有力候補です。一般的な例としては、電子メールの送信があります。ユーザーが登録した後に電子メールを送信する Rails コントローラーを想像してください。
class RegistrationsController
def create
@registration = Registration.new(params)
if @registration.save
RegistrationMailer
.welcome_email(@registration)
.deliver_now
redirect_to @registration
else
# ...
end
end
end
リクエストはメールが送信されるのを待つ必要はありません。
deliver_later ここでリクエストを高速化できます。他の種類のジョブにも同じことが当てはまります。統計を保存したり、データを処理したり、今すぐに行う必要がない他の作業を行う場合は、 perform_later。
アクティブストレージファイルを非同期で削除することもできます purge_later:
class User ApplicationRecord
has_one_attached :avatar
end
User.first.avatar.purge_later # enqueue a job to delete the file
また、Rails 6.1以降では、依存関係を非同期で削除できるようになりました。 dependent: :destroy_async:
class Team ApplicationRecord
has_many :players, dependent: :destroy_async
end
class Player ApplicationRecord
belongs_to :team
end
Team.destroy_by(name: "Flamengo")
# Enqueued ActiveRecord::DestroyAssociationAsyncJob
あなた 設定できる バックグラウンドジョブで破棄されるレコードの最大数 dependent: :destroy_async 関連付けオプション。
いいですね、これが最初の原則です。しかし、2 番目の原則が来ます。
怠けるのはいいことですが、何もせずに待つことはできません。次の例を考えてみましょう。
puts(
Benchmark.realtime do
5.times do
Net::HTTP.get(URI.parse("https://httpbin.org/delay/2"))
end
end
)
リクエストは同期的であるため、合計時間は約 10 秒 (+ ネットワーク オーバーヘッド) になります。問題なのは、コードがあまり実行しないことです。主にリクエストが完了するのを待機しています。視覚的には、次のように実行されます。
先手を打って、前のリクエストが完了するのを待つ間に、さらにリクエストを出すこともできます。 の async
宝石:
puts(
Benchmark.realtime do
Sync do
5.times.map do
Async do
Net::HTTP.get(URI.parse("https://httpbin.org/delay/2"))
end
end.map(&:wait)
end
end
)
大きな変更はありませんが、完了するまでに約 2 秒しかかかりません。できるだけ早く別のリクエストを発行します。つまり、基本的には、それらが並行して完了するのを待機していることになります。
何が起こっているかを視覚的に表すと次のようになります。

ここでは HTTP リクエストを例として使用しましたが、この原則を他の種類の I/O バウンド操作に適用してみてください。ファイル操作、システム コール、データベース クエリは、この種の最適化に適しています。データベース クエリといえば…
非同期データベースクエリ
Rails 7以降では、
ActiveRecord::Relation#load_async
バックグラウンド スレッドでデータベース クエリを実行します。これは、リレーションをバックグラウンドでロードしたいが、すぐに結果が必要ない場合に便利です。
たとえば、ページをレンダリングするためにいくつかのクエリを実行するコントローラーがあるとします。
class ReportsController
def create
@new_authors = Author.recent
@new_books = Book.recent
@new_reviews = BookReview.recent
end
end
各クエリの実行に1秒かかるとすると、ここでの合計時間は3秒になります。しかし、 load_async 並行して実行するには:
class ReportsController
def create
@new_authors = Author.recent.load_async
@new_books = Book.recent.load_async
@new_reviews = BookReview.recent.load_async
end
end
すると、合計時間は約 1 秒になります。繰り返しますが、次のクエリを開始するために 1 つのクエリが完了するのを待つ必要はありません。Rails ログには次のように表示されます。
ASYNC Author Load (1010.2ms) (db time 1011.4ms)
ASYNC Book Load (2.2ms) (db time 1013.8ms)
ASYNC BookReview Load (0.2ms) (db time 1014.7ms)
最初の数値列には、フォアグラウンド スレッドでクエリを実行するのにかかった時間が表示され、2 番目の列には、データベースでクエリがかかった合計時間が表示されます。
パフォーマンス向上を約束するものには必ずトレードオフがあります。 load_async、1回のリクエストでより多くのリソース(データベース接続スレッド)を使用しています。負荷の高いアプリの一部でこれを使用している場合、1人または数人のユーザーが接続を使い果たし、他のユーザーが待機する(タイムアウトになる可能性もある)可能性があるため、問題が発生する可能性があります。そのため、 注意してください load_async!
ただし、これをうまく使用できるのは、HTTP リクエストとデータベース クエリを並行して実行できる場合です。
class BooksController
def show
@new_books = Book.recent.load_async
@external_books = HTTP.get("https://external.com/books")
end
end
非同期ビュー
Rails は部分を並列にレンダリングしないと思いますが、Turbo Frames を使用するとページの一部を並列に読み込むことができます。URL を指定するだけで、そのルートからコンテンツが読み込まれます。 遅延ロードフレーム ユーザー エクスペリエンスにとって重要ではない、または負荷が大きいページの部分に特に役立ちます。
ビューにターボ フレームを追加します。
id="best_sellers"
src="books/best_sellers"
loading="lazy"
>
フレームのコンテンツをレンダリングするコントローラー アクションを記述します。
class BooksController
def best_sellers
@books = Book.best_sellers
end
end
そして、コンテンツをレンダリングするビュー:
id="best_sellers">
Best Sellers
render @best_sellers %>
以上です。ページに複数のフレームがある場合は、それらは並行して読み込まれます。もちろん、これはサーバーへのリクエストが増えることを意味しますので、その点に注意してください。また、やりすぎには注意してください。ページが読み込まれてから毎回「コンテンツを再度読み込む」のは、ユーザーにとってイライラするものです (SPA の皆さん、ご存じのとおりです)。
レンダリングにコストがかかる要素や、すべてのユーザーが必要とするわけではない要素がある場合は、それを初期ビューポートの外部にプッシュし、ターボ フレームを使用して遅延ロードすることができます。
非同期アセット
この概念をさらに拡張すると、アセットでも同じことができます。たとえば、 の async 属性 スクリプトタグを並列に読み込むには、次のようにします。
"render" async src="async-script.js">
重要なCSSと重要でないCSSを分けることも役立ちます。 遅延読み込みフォント
と font-display: swapこれにより、カスタム フォントの読み込み中にフォールバック フォントでテキストがレンダリングされます。
画像の場合は、 loading="lazy" 属性これはTurbo Framesと同じように動作し、画像がビューポートに入る直前にのみ読み込みます。Railsには、 デフォルトで画像を遅延読み込みする。
これらすべてにより、空白の画面が長時間表示されるのではなく、ページの表示が速くなります。
インデックスを同時に追加する
最後に、開発や「舞台裏」のシナリオで使用できる非同期ツールをいくつか紹介します。1 つ目は、PostgreSQL を使用している場合です。
Postgresテーブルにインデックスを追加すると、テーブルへの書き込みがブロックされます。大きなテーブルがある場合、本番環境でのダウンタイムにつながる可能性があります。幸い、 concurrently オプション テーブルをブロックせずにインデックスを作成するには:
class AddIndexToUserRoles ActiveRecord::Migration
disable_ddl_transaction!
def change
add_index :users, :role, algorithm: :concurrently
end
end
ここでの注意点は、ドキュメントを引用すると、 この方法では、標準的なインデックス構築よりも多くの作業が必要となり、完了までにかなり長い時間がかかります。ただし、インデックスの構築中でも通常の操作を続行できるため、この方法は実稼働環境で新しいインデックスを追加する場合に便利です。もちろん、インデックス作成によって発生する余分な CPU 負荷と I/O 負荷によって、他の操作が遅くなる可能性があります。
テストを並行して実行する
Rails 6 では並列テストが導入されました。必要なのは、必要なワーカーの数を指定することだけです。
class ActiveSupport::TestCase
parallelize(workers: 2)
# or let it figure out looking at the number of CPUs
parallelize(workers: :number_of_processors)
end
計算は簡単です (単純化しているため)。ワーカーの数が増えるほど、テストの実行速度が速くなります。
| 労働者 | テストスイート時間 |
|---|---|
| 1 | 40分 |
| 2 | 20分 |
| 4 | 10分 |
残念ながら、 RSpecはRailsの並列テストをそのままではサポートしていないただし、いくつかのgemはRSpecでこの動作を実装しています。いくつかの例を挙げます。
parallel_tests そして flatware。
非同期によりアプリは高速化しますが、その過程でコードが複雑になる可能性もあります。実行方法の制御が難しくなり、エラーのデバッグが難しくなると感じるかもしれません。
つまり、非同期化する前に宿題をやるべきなのです。これらのテクニックを、一時的な対処法として使わないでください。 本物 パフォーマンスの問題です。つまり、基本的な問題です。 データベースの列にインデックスを追加する、修正 N+1クエリ、使用 低レベルキャッシュ そして ビューのキャッシュ 意味のあるところでは、一般的には RubyとRailsの良い実践。
これらの原則は賢明に使用してください。他の単純化と同様に、場合によっては間違っている可能性があります。 これらはルールではない! 「先を見越して」行動する方が「怠惰に」行動するよりも良い場合が数多くあります (たとえば、値の事前計算など)。しかし、これが async と Rails についてもう少し考え始めるための出発点として役立つことを願っています。
#非同期Ruby #Rails