Job Queue を使った Rails の大量データ登録の負荷分散

## はじめに
Rails アプリケーションで大量のデータを登録する際、処理が一度に集中してしまいデータベースに負荷がかかりパフォーマンスの低下で障害となった経験のある方はたくさんいらっしゃると思います。

このような問題に対処された経験のある方は解決法がすぐに思い浮かぶでしょうが経験の浅い方はすぐには思いつかないかもしれません。

そこで今回は様々な現場で解決法としてよく使われる Job Queue を活用してデータ登録の負荷を分散する方法について解説します。


## 課題

よくあるケースとして、大量のデータを一括で登録しようとすると、以下のような問題が発生することがよくあります。

* データベースの負荷が急激に増加
* リクエストがタイムアウトする
* メモリ消費が増大し、パフォーマンスが低下
* アプリケーション全体の応答速度が低下

これらの問題を解決するために、Sidekiq を用いた非同期処理を利用して負荷を分散します。

## 解決策: Sidekiq を活用した負荷分散

### 1. Sidekiq の導入

まずは Sidekiq を Rails アプリケーションに導入します。

```ruby:Gemfile
gem 'sidekiq'
```

```
bundle install
```

### 2. ワーカーの作成

大量データの登録を行うための Sidekiq ワーカーを作成します。

```ruby:bulk_insert_worker.rb
class BulkInsertWorker
  include Sidekiq::Worker
  sidekiq_options queue: :default, retry: 3

  def perform(attributes)
    # 配列で渡されたデータを一括登録
    # Modelは適宜実際のモデルのクラスに置き換えてください
    Model.insert_all(attributes, record_timestamps: true)
  end
end
```

このワーカーを使うことで、insert_all を使った一括登録が可能になります。

### 3. バッチ処理でデータ登録を行う
前項で insert_all を使っているように大量のデータの登録/更新ではbulk insert/updateを行うことが望ましくなります
例えば1回の INSERT文の実行が 0.1 (ms) だった場合でもデータ量が多ければ必要な時間は 0.1 × N (ms) となってしまいDBがボトルネックとなります。

大量のデータを処理する際、すべてのデータを一度にワーカーへ投げるのではなく、適切な件数に区切ってバッチ処理することが重要になります。


実際には何件ごとに区切る必要があるかは想定するデータの件数に対しての処理速度、リソース負荷を測定しながら決めていくことになると思います。

この方法でデータを分割し、バッチごとにワーカーに処理を任せることで、データベースへの負荷を分散できます。

## Sidekiq のスケーリング

大量のデータを並列処理するために、Sidekiq のスレッド数やキューの設定を適切に調整します。

### 1. Sidekiq の設定

`config/sidekiq.yml` にスレッド数やキューの設定を追加します。

```yml:sidekiq.yml
:concurrency: 10
:queues:
  - default
```

concurrency（同時実行数）はデフォルト値が10になっています。

### 2. 複数のワーカーを立ち上げる

Sidekiq を複数プロセスで実行すると並列処理の効率が向上します。


```
bundle exec sidekiq -C config/sidekiq.yml
```

上記以外にもワーカーで重い処理を実行するなどでジョブが貯まっていってしまう場合は、ワーカーを複数のマシンで立ち上げることで 1台ごとの負荷を軽減することもできます。

実際にどういった方法で負荷を分散していくかはプロジェクトごとに変わってくるため、都度測定しテストをしながらベストな状態を模索することになります。




## まとめ

本記事では、Sidekiq を活用した大量データ登録の負荷分散について解説しました。

大量データ登録の課題 → 負荷集中、DB 負荷、メモリ消費増加

Sidekiq による解決策 → バッチ処理、非同期実行、スケーリング

適切な設定と並列実行 → ワーカーのスケール調整、キュー設定

Rails で大量のデータを扱う際は、Sidekiq を活用することでパフォーマンスを向上させ、アプリケーションの安定性を保つことができます。


## おまけ: 実際にsidekiqを使った場合と使わなかった場合
この項目は40万件ほどのデータをCSVから読み取りDBに登録するだけのシンプルな処理をWSLのローカル環境で試した結果のまとめになります。

### 1件ずつ登録
以降と差分を分かりやすくするため敢えて bulk insert せずに1件ずつ登録しています。

グラフはデータの登録処理を動かし始めてから終わるまでのメモリ使用量の増減と時間の折れ線グラフになります。


#### sidekiq未使用時
![画像1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4005878/f2bb81fd-bf2d-4643-9aec-8f8c06bc2adc.png)


sidekiqを使わない場合は単純に登録していっているだけなので単純な右肩上がりの傾きになっています。

#### sidekiq使用時
![画像2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4005878/7c5f2552-1b50-421f-8d65-2ab5b1096f8e.png)


sidekiqを使う場合は初めにジョブの登録後に実行していくため初めにメモリ使用量が一気に上がっています。

メモリの使用状況がギリギリの状態では通常の処理にプラスしてジョブ登録のコストがかかることは注意する必要があります。

最終的にどれ程の件数の処理まで問題ないのかは実際に動かしてみて測定して見極めることが重要になります。


### 10件ずつ登録

1件ずつの時と同じデータの取り込みで10件ずつまとめて処理するようにした場合は以下のようになります。

![画像3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4005878/a428b2e5-1e5c-46b8-ba00-26723d2a9318.png)


処理を分割することで完了までの時間がかなり短くなっていることが分かります。
測定がデータの取得頻度と比べて早く終わりすぎたため綺麗にグラフが取得できていませんがそこは気にしないでください。

10件ずつにすることでジョブの登録のコストが 1/10 、DB登録のコストが単純に 1/10 という訳ではありませんがかなり早くなった結果です。

### まとめ
この結果から適切な件数に区切って Job Quere を活用することの効果を感じていただけたと思います。

処理速度やリソース負荷に悩んだ時は是非試してみてください。