Ruby on Rails バッチ処理概要|Offers Tech Blog
はじめに
こんにちは。
プロダクト開発人材の副業転職プラットフォーム Offers を運営する株式会社 overflow のエンジニアばばです。 2023 年 1 月に中途入社としてジョインいたしました。
前職では不動産テックのエンジニアとして、データの各種エンジニアリング(クローリング、取り込み、集計処理)や機械学習ロジックのサービスへの組み込みを行っておりました。
overflow では、先日オープンβ版を発表いたしましたプロダクト開発支援組織の生産性最大化を支援するサービスである「Offers MGR」の開発を担当しております。
一見まったく別のプロダクトに見えますが、やっていることは案外共通点があり興味深く毎日開発をしています。
ただ、いまだジョインしたて業務に直結したことを書くネタはないので、Ruby on Rails(以下 Rails)のバッチ処理の概要をまとめてようとおもいます。
そもそもバッチ処理とは?
(Tech Blog の読者の皆様には説明不要かと思いますが)バッチ処理とはどういうものか振り返ることにしましょう。
バッチ処理は、スケジューラなどの定時に起動するシステムをトリガーとしてプログラムを実行して処理します。
例えば次のような処理ですね。
- メールや Push 通知を定期的に配信したい
- クローリングや外部サービスの API からデータを取得する処理
- 月次、日次などの集計レポート作成する処理
- データベースのバックアップやエクスポート
集計などの処理は、時間やシステムリソースを使うため、比較的利用者が少ない夜間に行うことでリソースの有効利用しています。
このようにリアルタイム性は求められないが、時間がかかる処理をバッチ処理として実装します。
Ruby on Railsでのバッチ処理実行方法
バッチ処理自体は Rails で簡単に実装ができます。一方書き方はいろいろな流儀があります。
Rails 自体は Web Framework であり、エンドポイントを楽に定義するか、どうやってビューをレンダリングするかの機能は充実している一方、バッチ処理を書く厳密な仕組みは、後述する Rails Runner の機能程度しか用意されていません。
Rails の体系的なガイド集である「Ruby on Rails ガイド」にも、バッチ処理を同記述するかの記載はなく実装者に委ねられています。
なので、開発者やチームにもよりますが、一般的には次のとおりに実装するケースが多くみられました。
-
rails
コマンドの機能であるrunner
コマンドを経由して Ruby スクリプトを書く - タスク実行のライブラリである
rake
を呼び出す - バックグラウンドジョブサーバ(Sidekiq/Resque/DelayedJob など)に委ねる
RubyスクリプトをRails Runner経由で実行する
一番よく見かけるパターンです。
Rails の環境をセットアップしてから実行してくれるので、
バッチ処理を書くための前処理を独自に定義する、など行わずにクラスやメソッドを呼び出すことができます。
スクリプト自体は基本的にどこに置いても OK ですが、使い捨てや移行時に使う処理以外だと、 app
や lib
などアプリケーションに組み込んでしまいます。
大半は models
のクラスメソッドとして定義することが多いですが、 Fat Model を避ける風潮や責務を分割するために app/services
や app/batches
のようなレイヤーとして配置するパターンもあります。(ただし、initializer による PATH 追加が必要となります)
定時実行は OS の Cron デーモンや、AWS ECS などクラウドプロバイダーのタスクスケジューリング機能を使います。
注意しなければいけないのはエラー時の再実行です。エラーが発生した場合のハンドリング処理を独自に実装し、さらにその後にどのタイミングで再処理を行うかも自身で実装する必要があります。
エラー時の再実行については後述します。
rake で実行する
rake
は、 Rakefile
と呼ばれるタスク定義ファイルを記述します。
もともと Unix 系の OS でよく使うビルドツール make
の Ruby 実装という位置づけのため、機能は少なく非常にシンプルです。
- Ruby スクリプトと同じ文法の DSL で定義ファイルを実装する
- 複数のタスクを依存関係つけて呼び出す
- ファイルタスクと呼ばれる、入力となるファイルと、出力となるファイルのタイムスタンプを比較して新しいときのみ実行する
などの機能があります。
Rakefile
に複雑な処理を記述すると(パーシャルに定義する xxxx.rake
ファイルも可能ですが)巨大な手続き処理になるため負債化しやすくなります。
そのため、 models
クラスなどにメソッドを定義して、そこから呼び出すなどが多くなります。
結果、 Rails Runner
経由で呼び出すパターンとの優位性がみられないため、わざわざ Rake タスクとして実装するケースは少ないようです(自分が関わった案件では皆無でした)
バックグラウンドのバックエンドサーバ(Sidekiq/Resque/DelayedJobなど)に委ねる
Rails には、バックグラウンドで実行するジョブとして Active job というインタフェースがありますが、そのジョブバックエンドに処理を委ねるというパターンです。
バックグラウンドジョブのバックエンドには、
- Sidekiq[1]
- Resque
- Delayed Job
などがあります。
これらのスケジューラ機能または Rails Runner 経由でエンキューする処理を組み合わせることで実装できます。
この方法のメリットは「バックグラウンドジョブの処理との共用できる」「エラー時のリトライ処理や遅延実行の実装がバックエンド側に実装されているので不要」「
Rails 初期化時間が短縮される」などがあります。ただし、バックエンドサーバの常駐が必要、バックエンドサーバを Web サーバ側と共用する場合はバッチ処理がキューを埋め尽くさないなどの注意深い実装が必要です。
バッチ処理の周辺技術・テクニック
バッチ処理はシンプルですが気をつけなければいけない罠も多く潜んでいます。
それらを防ぐテクニックなども多く存在しています。
以下はこれらをご紹介していきます。
大量のデータの読み込み・削除
大規模なデータの入出力が発生することに注意します。
- CSV ファイルを全行読み込む (いうまでもがな。オブジェクト化したらさらに悲惨)
- 大量のデータを一気にインスタンス化しない (
each
でなくfind_each
と言われるもの ) - リレーションのデータを
include
などしないで読み込む (N+1
問題はバッチ処理でも甚大な影響を与えます) - 大量のデータを 1 レコードずつ
find_or_create_by
する (一概にはいえませんがactiverecord-import
使いましょう)
多重実行防止
通知を行うバッチなど複数回バッチを起動してはならないケースでは排他制御をいれることがあります。
- ロックファイルを作成
- Redis/Memcache などによる制御
- DB にテーブルやカラムを用意し日付などで管理
- 多重実行されても問題起きない作りにする
定期実行
crontab
があまりにも有名ですが Unix 系 OS の機能であるため、サーバレス環境では使うケースは減ってきました[2]。
-
sidekiq
などジョブサーバのスケジューラ機能 - AWS などクラウドプロバイダーのスケジューラ機能
- Workflow Engine のスケジューラ機能
エラー時の再実行
たとえば、アクセス制限を突破してエラーが発生したがすぐに回復の見込みはないためしばらくたってから再実行したい場合、など。
- リトライを監視するスレッド/プロセスを起動しておく
- (DB などに状態を保存しておき)次の実行タイミングでエラーが起きた処理に実行する
- 再実行は行わず次のタイミングに行われることを信じる
など様々なパターン(最後のは諦め?)がありますが、いずれもエラーハンドリングのための実装が必要になります。
そのため、バックグラウンドジョブと組み合わせ実行するケースも多いです(バッチ処理内でも ActiveJob で行う)。
複雑なバッチ処理と戦うには
データ量が増えてくるとシンプルであったバッチ処理も様々な問題がでてきます。
- 処理時間がかかるので並列実行したい(リソース制御)
- 複数の処理を組み合わせて 1 つのジョブとして実行したい(いわゆるパイプライン実行)
- エラー時の通知処理を共通化させたい
このような要件を満たすサービスとして Workflow Engine と呼ばれるものがあります。
オープンソースの Workflow Engine については以下のリポジトリで紹介されています。
関連記事
副業転職の Offers 開発チームがお送りするテックブログです。【エンジニア積極採用中】カジュアル面談、副業からのトライアル etc 承っております💪 jobs.overflow.co.jp
Discussion