Preforking Servers and Reforking

Unicorn に代表される Prefork 型の HTTP サーバーは、起動直後に子プロセスを fork(2) する。プロセスごとにクライアントのリクエストを処理することで、パフォーマンスを向上させることが目的だ。Apache (mod_prefork) や Gunicorn (Python) も同様の Prefork 型のサーバーだ。

Pitchfork fork(2)

この時 Copy on Write (CoW) という仕組みを導入することで、実際に必要なメモリ量を可能な限り抑えようとする。CoW は、書き込みが生じて必要になるまでメモリを共有しておく。不要なメモリコピーを避けることができる。理論上、メモリの書き換えが生じなければ、二つのプロセスを起動するのに必要なメモリ量は、一つのプロセスを起動するのに必要なメモリ量に限りなく近い。

Pitchfork CoW

しかし、Ruby プロセスを prefork する場合、多くのコードは遅延初期化される点に注意しておく必要がある。起動直後にプロセスを fork しても、遅延初期化されるコードはまだメモリに載っていないので、CoW による効率化の恩恵を受けることができない。

  • メモ化 (Memoization)
    • コードパスが実行されて初めてオブジェクトがメモリに載る。逆にいうと起動直後はメモリにまだ載っていない
  • インラインキャッシュ (Inline Caching)
    • Virtual Machine の最適化手法であるメソッド呼び出しのキャッシュも、起動直後は存在しない
  • JIT コード (Just-in-time compiled)
    • コードパスが実行されるに従ってコンパイルされる JIT も遅延初期化されてからバイトコードが生成されるので、Prefork の恩恵を受けることができない

Pitchfork Lazy Initialized

この Ruby 特有の問題に対して、Reforking を導入することで克服しようとしたのが、Shopify で開発されている Pitchfork である。アプリケーションを起動させてから任意のタイミング(実際は、リクエストを N 回処理した後)で子プロセスを「再フォーク (Refork)」させる。この時コピーされるメモリには、何度かリクエストを処理した後でメモリに載っているはずなので、Prefork されたプロセスよりも CoW の恩恵をより受けることができる。

Pitchfork Reforking

Pitchfork を利用する場合、アプリケーションの Refork Safety に気をつける必要がある。Linux がプロセスを fork する時 File descriptor tables はコピーされるが、実際にオープンな V-Node へのコネクションは共有される。従って、意図しないリソースの競合(同時アクセスやリソースリーク)を避けるために、fork の前後でコネクションを閉じる必要がある。これはライブラリに依存する。

Pitchfork Fork Safety

2024-03-05