Passenger から Puma への移行で気を付けたいポイント

Posted on

freee では Ruby のアプリケーションサーバに Passenger を使っていることが多いです。最近、Passenger から Puma への移行を検討する機会があったので、そのときに検証したポイントをまとめておきます。

先に断っておくと、いろいろ検証した上で Puma への移行はやめました(その理由も書いています)。また、特段目新しいことは書いていないです。

Puma を使いたかった理由

Puma を使いたかった一番の理由は、デプロイ時間を短縮するために Puma の phased restart を採用したかったからです。いわゆるゼロダウンタイムデプロイという仕組みで、ELB などのロードバランサから切り離すことなくデプロイが行えます。

Passenger Enterprise には Rolling Restarts という同等の機能がありますが、freee のインフラ規模だとかなりの金額になってしまうので Puma が候補に挙がりました。

なお、この記事で Passenger と書いたときは OSS 版の Passenger を指しています。

実装モデルの違い

Passenger から Puma への移行で最も重要なポイントは実装モデルの違いです。

Passenger はマルチプロセスモデルなのに対して、Puma はマルチスレッドモデルです。この違いが意味することは、Puma を採用するならスレッドセーフなコードでなければならないということです。

実装モデルの違いについて詳しく知りたい方は「2015 年 Web サーバアーキテクチャ序論」がおすすめです。 2015 年に書かれた記事ですが、非常によくまとまっています。

GVL とスレッド

MRI (CRuby) は Giant VM lock という仕組みを持っており、マルチスレッドモデルであっても同時に動くスレッドは常にひとつです。ただし、IO 関連の処理中はこのロックが解放されます。

現在の実装では Ruby VM は Giant VM lock (GVL) を有しており、同時に実行されるネイティブスレッドは常にひとつです。ただし、IO 関連のブロックする可能性があるシステムコールを行う場合には GVL を解放します。その場合にはスレッドは同時に実行され得ます。

GVL の動きは簡単なコードで実証できます。たとえば、次のコードの結果は常に 1 になります。

counter = 0
flag = true

threads = 10.times.map do
  Thread.new do
    if flag
      counter += 1
      flag = false
    end
  end
end
threads.each(&:join)

puts counter

ただし、IO 処理を挟むと GVL が解放されるので結果が変わります。次のコードはスレッドの処理に puts の行を追加しただけですが、実行するたびに結果が変わります。

counter = 0
flag = true

threads = 10.times.map do
  Thread.new do
    if flag
      puts 'not thread safe'
      counter += 1
      flag = false
    end
  end
end
threads.each(&:join)

puts counter

GVL が解放されて複数のスレッドが同時に flag の変数を変更してしまうため結果が一意になりません。スレッドセーフでないコードを Puma で動かすと、これと同じようなことが起きかねないのです。

Puma への移行をやめた理由

Rails はスレッドセーフなコードなので、The Rails Way に則っていれば何も問題はありません。また、Rails 5.0 から Puma がデフォルトのアプリケーションサーバになっています。

それを踏まえた上でも Puma への移行をやめたのは、次のような理由があったからです。

  • 数十万行もあるすべてのコードがスレッドセーフであることを保証できない
    • RuboCop::ThreadSafety などを使って機械的に見つけることはできるが、それでも 100% 防げるわけではない
  • 仮にスレッドセーフでないコードでバグが発生しても、レースコンディションを再現させるのが難しい
  • freee のプロダクトが扱うデータは 1 件でも情報漏洩すると事業継続できない可能性がある
    • ここでいう情報漏洩は、A ユーザーのデータが B ユーザーに見えてしまうという意味

まとめ

マルチプロセスモデルで動いているコードをマルチスレッドモデルに変更するのはそれなりにリスクを伴います。目先の機能だけに捕らわれることなく、しっかりと仕組みを理解することが重要ですね。