Chapter 12

[Rails基礎] 複数リクエスト間で残存する変数

igaiga
igaiga
2022.08.18に更新

ワーカープロセスの寿命と複数リクエストをまたいで残存する変数

Pumaやunicornなどのアプリケーションサーバでは、ワーカーと呼ばれるプロセスがリクエストを処理します。1つのリクエストをワーカーが処理したあと、プロセスは残存して複数回リクエストを処理します。

プロセスが残存して複数回リクエストを処理するので、前回のリクエスト時の値が変数などに残る場合は、動作を把握しておかないとバグになります。これは気づきづらい問題であり、また個人情報が入っているとセュリティ上の問題にもなり得ます。

ここでは、変数の種類によって複数リクエスト間でプロセスに残存するかを調べます。

実際の運用では、複数回のリクエストを処理したあとで定期的にワーカープロセスをkillして再起動するケースが多いです。リクエスト1回ずつ再起動するというよりは、複数回のリクエストを処理したあとで再起動されるケースが多いようです。この場合、プロセスが複数回のリクエストを処理するため、プロセスに残存する変数を意識することは重要になります。

また、複数リクエスト間で残存する変数は、すなわち複数のリクエストで共有される可能性がある変数ということです。したがってレースコンディションにも気をつける必要があります。たとえばリクエスト回数を記録するようなケースで 変数 = 変数+1 と書いたとしても、リクエストの回数だけ必ず加算されるとは限りません。たとえば、同一プロセス内に複数ワーカーがスレッドで動いているときには、スレッドが動作するタイミングによって結果が変わることがあります。並行に操作される可能性があるときは排他制御にも気を配り、加えてRDBなど安全に並行操作できる外部ストレージをつかうことも選択肢になります。

結論

いずれもBooksControllerクラスに以下の変数をつくって、ワーカープロセスへ複数回リクエストして残存するかどうかを確かめました。アプリケーションサーバはRAILS_ENV=production rackup config.ru でpumaを起動しています。Rails 7.0.3.1, ruby 3.1.2, puma 5.6.4, Rack 1.3 (Release: 2.2.4) でテストしています。

  • グローバル変数 $foo: 複数リクエスト間で残存する
  • クラス変数 @@foo: 複数リクエスト間で残存する
  • クラスインスタンス変数 @foo(コントローラのクラス直下): 複数リクエスト間で残存する
  • インスタンス変数 @foo(コントローラのインスタンスメソッド内): リクエストごとに消える
  • スレッドローカル変数 Thread.current[:foo]: 複数リクエスト間で残存する(単調増加ではなく不規則)

グローバル変数、クラス変数、クラスインスタンス変数、スレッドローカル変数が複数リクエスト間で残存しています。インスタンス変数は複数リクエスト間で消去されています。スレッドローカル変数は単調増加するのではなく、不規則に値が変わっています。pumaのワーカーがスレッドで動作しているのが理由だと考えています。脱線しますが、スレッドローカル変数はFiberローカル変数でもあります。別々のFiberであれば別の変数になります

複数リクエスト間で残存する変数にアクセスしたユーザーに依存する情報を代入することは、セキュリティ上の問題が発生するリスクを高めるので、設計を再検討して保存しないようにするのが無難です。

テスト方法

次のようなRailsアプリをつくって、アプリケーションサーバを起動します。サーバの起動は RAILS_ENV=production rackup config.ru でpumaを起動しました。rails s -e production でも同様の結果になります。production環境で実行するため、事前に rails assets:precompile してあります。

テストコード

コード全体はこちらのリポジトリに置いてあります。

https://github.com/igaiga/remaining_variables_check_app

各種変数

以下のコードの「変数」部分をテスト対象の変数にして、pで表示して確認。

class FooController < ApplicationController
  def index
    変数 ||= 0
    変数 += 1
    p 変数
  end
...

クラスインスタンス変数のテストコード

class FooController < ApplicationController
  @foo ||= 0
  def index
    self.class.instance_variable_set(:@foo, self.class.instance_variable_get(:@foo)+1)
    p self.class.instance_variable_get(:@foo)
  end

Thread.currentのテストコード

class FooController < ApplicationController
  def index
    Thread.current[:foo] ||= 0
    Thread.current[:foo] += 1
    p Thread.current[:foo]
  end

テスト結果

$ RAILS_ENV=production rackup config.ru
Puma starting in single mode...
* Puma version: 5.6.4 (ruby 3.1.2-p20) ("Birdie's Version")
*  Min threads: 5
*  Max threads: 5
*  Environment: development
*          PID: 22279
* Listening on http://127.0.0.1:9292
* Listening on http://[::1]:9292
Use Ctrl-C to stop

::1 - - [16/Aug/2022:09:58:35 +0900] "GET /404 HTTP/1.1" 404 1722 0.1150
"グローバル変数: 1"
"クラス変数: 1"
"クラスインスタンス変数: 1"
"インスタンス変数: 1"
"スレッドローカル変数: 1"

::1 - - [16/Aug/2022:09:58:39 +0900] "GET /foo/index HTTP/1.1" 200 2269 0.0450
"グローバル変数: 2"
"クラス変数: 2"
"クラスインスタンス変数: 2"
"インスタンス変数: 1"
"スレッドローカル変数: 1"

::1 - - [16/Aug/2022:09:58:40 +0900] "GET /foo/index HTTP/1.1" 200 2269 0.0415
"グローバル変数: 3"
"クラス変数: 3"
"クラスインスタンス変数: 3"
"インスタンス変数: 1"
"スレッドローカル変数: 2"

::1 - - [16/Aug/2022:09:58:41 +0900] "GET /foo/index HTTP/1.1" 200 2269 0.0348
"グローバル変数: 4"
"クラス変数: 4"
"クラスインスタンス変数: 4"
"インスタンス変数: 1"
"スレッドローカル変数: 1"

::1 - - [16/Aug/2022:09:58:41 +0900] "GET /foo/index HTTP/1.1" 200 2269 0.0361
"グローバル変数: 5"
"クラス変数: 5"
"クラスインスタンス変数: 5"
"インスタンス変数: 1"
"スレッドローカル変数: 3"