エンジニアインターンの田中です! 株式会社CyberOwlで画像ホスティングシステムの実装を担当させていただきました。
その過程で、Ruby on Railsサーバーが大量の画像編集を同時に行った場合に、メモリーオーバーフローとなることが判明し、セマフォを利用してその対策を行いました。ここではその対策に加え、そのために必要な知識であった、プロセス、スレッド、セマフォ、ミューテックス、そしてRailsのウェブサーバーpumaがどのように複数リクエストを処理しているのかをまとめました。
プロセスは実行するプログラムのインスタンスであり、独立したリソースを割り当てられます。このリソースにはメモリ空間も含まれており、複数プロセスを作成した場合、プロセス内にあるプログラムは別プロセスのメモリなどにアクセスすることができません。
スレッドはプロセス内で実行される軽量の実行単位です。スレッドは同じプロセス内の別スレッドと並行して実行され、この時、そのプロセスのメモリ空間などのリソースを共有します。
pumaを使用してウェブサーバーを作成する場合、プロセスを一つとするシングルモードと複数プロセスを利用するクラスターモードを選択することができます。
シングルモードでは一つのプロセス内でスレッドプールを作成します。スレッド数の上限と下限を設定することができ、トラフィックに応じてその数をスケールさせることが出来ます。
クラスターモードでは複数のプロセスを使用し、それを可能とするためにマスタープロセスとワーカープロセスを作成します。
マスタープロセスはワーカープロセスの元になるプロセスです。マスタープロセスはリクエストに対する処理はしませんが、ワーカープロセスがこれをフォークし、その処理を行います。そして、ワーカープロセスが作成された後は、それらを監視する役割を持ちます。
マスタープロセスからフォークされて作られたプロセスです。puma.rbを使用したRuby on Railsでは、マスタープロセスが一つ、ワーカープロセスが複数作成されます。このワーカープロセスがアプリケーションを実行しておりそれぞれが複数スレッドを持っています。
今回、実装するサービスはAWSのECS(Elastic Container Service)で実行されます。ECSなどの環境では複数個のコンテナを作成し、ロードバランシングすることが可能なため、Railsアプリケーションでワーカプロセスを二つ以上作成する必要がありません。このため、シングルモードを使用し、リクエストはスレッドプールで処理します。
Pumaのシングルモードで、Railsが複数のリクエストを受け取った場合、複数のスレッドを使ってリクエストを処理します。Pumaはスレッドプールを保持しており、リクエストが送られる度に、その中から空いているスレッドを特定し、そのスレッドに処理を割り当てます。一つのリクエストに対する処理中に他のリクエストあった場合は、他の空いているスレッドを使って同時に処理を行います。スレッドはアプリケーションのコードを実行するために必要な環境を持っているため、並列に複数のリクエストを処理することができます。
セマフォは排他処理の方法の一つです。いくつのプログラムが同時に共有リソース(メモリーなど)を使用して良いかを制限するもので、セマフォを利用した場合以下のような実装になります。
1. セマフォを定義し、同時利用の上限を設定する。
2. 共有リソースを使用する直前にセマフォから-1する。
3. 共有リソースを使用し終わった段階でセマフォ+1とする。
4. セマフォが0の時(共有リソースが他のプログラムから使用さるべきでないとき)は他のプログラムがセマフォ+1をして、セマフォの数字が1以上になるまで待つ。
これによって、同時リソースアクセス数に上限を儲けることができます。
ミューテックスはセマフォと同じように排他処理の方法の一つです。一つのタスクが特定の共有リソースを使用中、他のタスクはそのリソースにアクセスできなくするための技術です。つまり、セマフォの数字が0か1しかない時と同じような挙動になります。
メモリーを消費する処理の一例として画像編集があげられます。画像編集を行うサーバーに複数リクエストが同時にあった場合に、サーバーは同時に複数画像を処理します。この時、多くのメモリーを消費し、メモリーオーバーフローを引き起こしてしまう可能性があります。そこで、セマフォを使用して画像編集の同時実行数に上限を設けました。
同じプロセス内でセマフォを使用する場合はConcurrent::Semaphoreを利用し、仮に、画像処理の同時実行数を4としたい場合、セマフォの数を4とします。
config/puma.rb
$semaphore = Concurrent::Semaphore.new(4)
app/controllers/images_controller.rb
def image_editing
$semaphore.acquire #セマフォから-1、セマフォの数が0のとき、ここで待機
#------------------------------
#
#画像編集ロジック
#
#------------------------------
$semaphore.release #セマフォに+1
end
ミューテックスやセマフォはRailsのapplication.rb, モデル, コントローラなどでも定義できますが、すべてのスレッドからのアクセスを制限する必要がある時、サーバーが立てられる前(puma.rbなど)で定義しなければなりません。これはapplication.rb, モデル, コントローラーなどでセマフォを定義した場合、リクエストごとにそのインスタンスが作成されるので、プロセス内の全てのスレッドで使用されるグローバル変数を定義するには不適当なためです。
また、ミューテックスやセマフォを使用する場合、プロセスやスレッドがお互いにお互いが占有するリソースの取得を待っている状態で進行が止まってしまうデッドロックを起こす可能性があるので、二つ以上の共有リソースを制御する場合は要求順序などを注意しなければなりません。
その他、仮に複数のプロセスを使用する必要がある場合には、Concurrent::Sepaphoreによるスレッド間の共有リソースアクセスの制限だけでは十分でない可能性があります。その場合には、全てのプロセスのアプリケーションからの共有リソースアクセスを制限するため、redisを導入してそこにセマフォを作成する必要があります。
最後まで読んでいただきありがとうございます。
※2023年6月2日時点