🐘

Re: WebサーバーアーキテクチャとPHP実行方式の理解から始めるphp-fpmとはなにか?

2024/02/24に公開
2

この記事のモチベーション

「php-fpmとはなにか?」を知るため、PHPのドキュメントを見ました。

https://www.php.net/manual/ja/install.fpm.php

しかし、ここに書いていることはまあそうなのですがあまりに焦点が絞られ過ぎてて「php-fpmとはなにか?」に対する答えとしては少し不十分な気がしていました。

例えるなら数学の問題に答えるにあたって、途中式を飛ばしたり証明の過程を飛ばしたりというような感じ。

不十分というのは、それを理解するための段階をすっ飛ばして答えだけが書かれている状態のことを指しています。

その不十分なところを自分も曖昧にしか理解できていない気がしており、いい機会なので整理しておこうというのがこの記事のモチベーションです。

そのためこの記事は、「php-fpmとはなにか?」をプロセス→Webサーバー→実行方式と順を追って説明していく構成になっています。

「細けぇこたぁいいんだ、おらぁ今すぐ答えだけ知りてぇンダ」という方は先に貼ったリンクを見るか、結論の章を見ればよいかと思います。

一応補足で記事タイトルの元ネタはリゼロです。

http://re-zero-anime.jp/tv/

PHPを動かす方法

まず最初に定義ですが、PHPを動かす方法について前提を確認しておきます。

  • PHPを動かす=CPUがメモリ上にあるバイト列を読み込んで演算する=プロセスを作る
  • プロセスを作る=phpのランタイムが用意されたマシンのプログラムを実行する
  • プログラムを実行する手段=PHPランタイムが用意されたマシンに直接入って実行するor通信により実行する
  • 通信で実行する=TCP/IP or Unixソケットなどを利用する

で、ここではその手段としてhttp/httpsを採用するものとします。
(http/httpsはいちいち長いので、ここから先はWebと称することにします)

で何のために通信をしているのかというと、当然情報を取得するためですよね。
情報を取得する=こちらの依頼(リクエスト)に対して、その依頼を実行した結果を返す(レスポンス)
ということ。

結果はもちろん紙ではなく、通常はHTMLやJSONとったデータで返ってきます。

では「こちらの依頼(リクエスト)に対して、その依頼を実行した結果を返す(レスポンス)」をやっているのはPHPか?と聞かれると、答えは「No」ですよね。

PHPはリクエストを受けた上でそのものの実行と、レスポンスとなる結果を生成しているに過ぎないので。

ではそれをやっているのは誰か?と言われると、もちろんWebサーバーなわけです。

Webサーバーの必要性

ここでWebサーバーの必要性を理解しておきたいのですが、本当の意味でこれを理解するにはLinuxの知識が必要不可欠になってきます。

ここではその必要性を『ふつうのLinuxプログラミング』の説明を借りながら説明することにします。

https://amzn.to/3wnn9VU

  • データとは=バイト列の集まり
  • データを送受信する=バイト列を送り合う
  • バイト列を送り合う=ストリーム

というこの書籍の定義を拝借すると、バイト列を送り合うというのは、aaa > hoge.txtであろうがhttp://example.com/?message=aaaであろうがストリームを作る必要があります。
なぜならWebのリクエスト/レスポンスはバイト列の送り合いであり、バイト列を送り合っている=ストリームだからです。つまり、マシン内だろうがネットワーク越しだろうが結局相手にしているのはストリームなのです。

ではストリームををどうやって手に入れるのか?ですが、当然データの送受信なので、ストリームの入力と出力の橋渡しをする役が必要になります。

同じマシン内であればパスを指定すればおしまいですが、ネットワークの場合はサーバー側で通信を待ち受けているプロセスが存在しています。

これがファイルに対応する実体で、サーバー側で通信を待ち受けているプロセスに入力をつなぐことでバイト列をやりとりしています。

この「サーバー側で通信を待ち受けているプロセス」がWebサーバーであり、Webサーバーはバイト列のやりとりのために必要なのです。

その際必然的にプロセスが起動することになるわけですが、Webサーバーは「プロセスをいかに安全かつ効率的に運用するか?」という問題に対するアプローチによって駆動方式が変わってきます。

ここまで整理して前提を確認すると…

  • PHPを動かす方法としてWebが選択されることが主流(PHPランタイムがあるマシンにプログラムを配布して実行させる方法ももちろんできないことはない)
  • WebでPHPを動かすためにはWebサーバーが絶対に必要

WebにおけるPHPランタイム

PHPを動かすためにWebサーバーが必要不可欠なことはわかりました。

そしてさらに言えば、WebにおけるPHPランタイムを考えるとはすなわち、Webサーバー×実行方式の組み合わせを考えることとほぼ同義です。(厳密にはOSなどもあるがそこを考え出すと話が大きくなりすぎるので今は考えない)

この章ではそのあたりの考えを整理したいと思います。

  • Webサーバー
    • Apache
    • Nginx
    • LiteSpeed
  • 実行方式
    • Webサーバーの拡張モジュール
    • CGI(FastCGI)
    • SAPI(LiteSpeed SAPI)

ただしパターンとしては上に挙げたWebサーバー×実行方式=9パターン採用できるわけではありません。

例えばApacheにはphpモジュール版とCGI版の両方が存在するためどちらの実行方式を取ることも可能ですが、Nginxの場合はPHPの拡張モジュールが存在しないためCGI方式で実行することになります。

これについては本家ドキュメントを見るとよいでしょう。

https://www.php.net/manual/ja/install.php

https://www.php.net/manual/ja/install.unix.apache2.php

https://www.php.net/manual/ja/install.unix.nginx.php

https://www.php.net/manual/ja/install.unix.litespeed.php

よくある勘違いとして「モジュールで動かすならApacheを使い、CGIならNginx」というのがありますが、これは間違いでApacheでもCGIを利用することはできます。(つまり、もちろんApacheでもphp-fpmの利用ができる)

この勘違いが生まれる理由はおそらく、PHPをインストールすると自動的にPHPモジュールもインストールされApacheはデフォルトでそのPHPモジュールを読み込みにいくからではないかと推察します。

話が少しそれましたが、つまり 「PHPの実行環境を構築する」とは「どのWebサーバーを使うか?」と「どの実行方式でPHPを動かしたいか?」によって決まるものであり、「どのWebサーバーを使うか?」だけでは決まらない のです。

ここでようやく記事テーマの「php-fpmとはなにか?」について表面的に答えられるのですが、php-fpmとはPHPの実行方式であり、PHPにおけるFastCGI実装のことである

つまりphp-fpmとは「どの実行方式でPHPを動かしたいか?」に対する答えということです。

文脈的にはこの後すぐにPHPの実行方式であるWebサーバーの拡張モジュールとCGIについて説明したいところですが、その前にもう少し寄り道してWebサーバーとプロセスについて話しておく必要があります。

結局WebにおけるPHPランタイムはWebサーバー×実行方式によって決まるため、PHPの実行方式だけを説明してもそれは「半分わかった」くらいの理解度になってしまうからです。

またFastCGIが生まれた経緯を理解するためにもプロセスは避けて通れない知識なので、どちらにせよ知っておく必要があります。

Webサーバーのアーキテクチャを理解する

Webサーバー×実行方式ごとの特徴について本質的な理解を得るために、まずWebサーバーというものを理解する必要があります。

そしてWebサーバーというものを理解するということは、すなわちプロセスをどう扱うかを理解することです。

なのでまずは前提知識となるプロセスについて見ていきます。

ただしプロセスの具体的な内容については触れないので、興味がある人は以下の本を読むかその本の読書メモを見てください。

https://amzn.to/3T6z7fG

https://zenn.dev/yskn_sid25/scraps/1fee3c04edc786

この記事はphp-fpmとは何かに視点をおいているためこのセクションの説明は表層的ですが、Webサーバーとプロセスについてより詳しく知りたい方は以下の記事が非常に参考になります。この記事を書くにあたっても参考にさせていただきました。

https://qiita.com/kamihork/items/296ee689a8d48c2bebcd

それと完全に余談ですが自分が「Webサーバーとプロセス知っておかないと」と思ったのは統合開発環境のアニキが書かれたこちらの記事です。

https://blog.ojisan.io/server-architecture-2023/

プロセスとスレッド

「ほぼ同じなんじゃない?」という方もいらっしゃるかもしれませんがコンピューターのお気持ちになると実は違うものだというのがわかるはずです。

重要なのはコンピューター(というかメモリ)側の視点に立った時に、メモリ空間を共有できるか・できないかの違い。

プロセス

  • プログラムの実行単位
    • MACのモニタリングやWinのタスクマネージャー、Linuxだとpsコマンドなどで見れるアレ
  • プロセスを実行するとは、CPUがメモリを確保して演算するということ
  • プロセス同士はメモリ空間を共有できない

つまりプロセスとはプログラムの実行単位=CPUとメモリを使って演算する仕事の単位とも言える。

スレッド

  • プロセスの中で動くさらに細分化された実行単位
    • =CPUを利用するための実行単位で最小処理単位の概念
    • 1つのプロセスが1つのスレッドしか持っていない場合、この時はスレッド=プロセスとも言える
  • スレッド同士はメモリ空間を共有できる

並列と並行

並列と並行を理解しておく必要があるのは、この後に出てくるシングル or マルチ × プロセス(スレッド)を理解するために必要だからです。

並行

  • ある任意時間で一つの仕事しか行わないが、複数の仕事を切り替えることによって同時に実行していること
    • こっちの仕事をして"待ち"になったら別の仕事に切り替えて、また"待ち"になったら更にべつの仕事をする
    • 人間のやっているマルチタスクは一つの脳みそを使って作業を切り替えているが、それと同じで脳みそ=CPU, プロセス=作業という感じ

並列

  • ある任意時間時で複数の仕事を同時に実行すること
  • 単に複数の仕事を同時に実行できていれば良い
  • 並列は並行を包含している

図で表すとこういうイメージ。


出典: https://qiita.com/yukiyamamuro/items/de06878d6772ee2a1e76

T1とT2, T3とT4の関係は並行です。一方でT1とT3、T2とT4の関係は並行であり並列でもあります。

コンテキストスイッチ

コンピューターが現在の実行中のプロセスやスレッドから別のプロセスやスレッドに制御を移す操作のことを指します。

コンテキストスイッチは、次のような場面で発生します。

  1. プロセスやスレッドの実行中に、タイムスライス(一定時間の実行時間)が終了した場合。
  2. 割り込みが発生し、新しいタスクが実行される必要がある場合(例:デバイスからの入出力が完了した際など)。
  3. プロセスやスレッドが自発的に待機状態に入り、他のプロセスやスレッドが実行可能になった場合。

コンテキストスイッチはオーバーヘッドが大きい処理なので、頻発すると処理速度が低下します。

C10K問題

くらいあんとてんけー問題。

ここまでの説明でプロセスやスレッドを駆使してCPUのやりくりを工夫していることが分かったかと思うのですが、それを踏まえてこの問題を簡単に説明すると「リクエストをうまく捌けるのはリクエストが一定以下の場合の話でクライアント数が1万を超えると限界が来るよね?」という問題です。

この問題にどうアプローチしているか?もWebサーバーの稼働方式の違いに繋がりますがそのアプローチだけでWebサーバーの違いを須く語ることはできません。それは現代ではなくひと昔前の時代における前提となります。

詳しくはこちらの記事をご参照ください。

https://blog.inductor.me/entry/2022/05/31/150707

そのことを念頭におきつつ、Webサーバーの稼働方式の違いは次の節で説明します。

シングル or マルチ × プロセス(スレッド)

ここまででプロセス・スレッド・並行/並列・コンテキストスイッチ・C10K問題について把握できたかと思います。

最後はそれらの知識を組み合わせることで理解可能となる、シングル or マルチ × プロセス(スレッド)について説明します。

マルチプロセス

  • 複数のプロセスを並列処理すること
  • クライアントからの接続ごとにプロセスを複製(fork)して処理する
  • 子プロセスをたくさん作ることで並列処理をするため、処理がメモリ空間で相互影響しない
  • 裏を返せばプロセスごとにメモリを食っていくため、大量にプロセスができるとメモリの観点からパフォーマンスが悪化する


出典: https://milestone-of-se.nesuke.com/sv-basic/linux-basic/apache-mpm-prefork-worker-event/

シングルプロセス

  • 1つのプロセスでのプログラム実行を示す
  • プロセスは一つなので、並行処理となる

マルチスレッド

  • 複数のスレッドを並列処理すること
  • クライアントからの接続毎にスレッドを生成して処理する。
  • メモリ空間が各スレッドの間で共有できるため、メモリの消費量を抑えたりコンテキストスイッチのオーバーヘッドがマルチプロセスに比べて小さくなる
  • 裏を返せばメモリ空間を共有しているため、他のスレッドに依存する場合がある


出典: https://milestone-of-se.nesuke.com/sv-basic/linux-basic/apache-mpm-prefork-worker-event/

シングルスレッド

  • 単一のスレッドでのプログラム実行
  • 1つの処理が完了するまで次の処理が開始されない
  • Node.jsはシングルスレッド
    • ん?非同期処理できるのにシングルスレッド?
    • ノンブロッキングI/Oとイベントループという仕組みを使って擬似的に並列処理を作っている。詳しくはこちら


出典: https://qiita.com/klme_u6/items/ea155f82cbe44d6f5d88

Webサーバーの稼働方式の違い

Webサーバーの稼働方式の違いを理解するためにはシングル or マルチ × プロセス(スレッド)を理解する必要があり、そのためにはプロセス・スレッド・並行/並列・コンテキストスイッチ・C10K問題を知っておく必要があります。

そのために前節で時間をかけて説明を重ねてきました。

ここまではプロセスとスレッドという世界の中だけで説明が進んできましたが、ここからは視野を少し広げて「Webサーバーはプロセスとスレッドをどのように扱っているのか?」というお話になってきます。

なぜならWebサーバーの稼働方式の違いとは、「プロセスとスレッドをどのように扱うか?」の違いだからです。その違いが、コンテキストスイッチやC10Kといった処理速度低下につながる問題をWebサーバー単体で解決しようとしたときの違いとして現れます。

そしてその違いこそがWebサーバーの稼働方式の違いであり、ApacheやNginxなどWebサーバーそれぞれの違いにもつながります。

ただしそれはあくまで稼働方式の違いから見た観点なだけで、時代やアーキテクチャの変遷による使い方の違いについては脇においておきます。その辺りのことは注釈の記事を読んでいただけると良いかと思います。[1]

そのWebサーバーの稼働方式には以下の3つがあります。

  • prefork
  • worker
  • event driven

以降の項で違いを話します。

prefork

  • マルチプロセスで動いている
  • Apacheのデフォルトアーキテクチャ
  • あらかじめプロセスをいくつか立ち上げておき、リクエストを受けられるようにしている(だからpre+fork)
  • リクエストとプロセスが1対1で紐づく
    • つまり一つのプロセスには一つのスレッドしか存在しないため、preforkはシングルスレッドでもある
  • 一つのプロセスが停止しても他のプロセスには影響を及ぼすことがないため、通信を継続できる
  • リクエストが増えると子プロセスが増える。つまりリクエスト数に比例してメモリやCPUを消費するため、C10K問題につながる


出典: https://milestone-of-se.nesuke.com/sv-basic/linux-basic/apache-mpm-prefork-worker-event/

worker

  • マルチプロセスで動いている
  • Apacheで選択可能な稼働方式
  • リクエストとスレッドが1対1で紐づく
    • つまり一つのプロセスには複数のスレッドが存在するため、workerはマルチスレッドでもある
    • いわばハイブリッド方式
    • いくつかのプロセスやその中にいるスレッドを労働者に見立ててworkerと呼んでいる
  • 子プロセスを減らすことができるため、メモリの消費量をpreforkに比べて抑えることができる
  • 一つのプロセスが停止した場合、複数のスレッドも同時に影響を受けるためpreforkに比べると通信が不安定になりやすい
  • リクエストが増えるとスレッドが増える。つまりリクエスト数に比例して結局メモリやCPUを消費するため、やはりC10K問題につながる


出典: https://milestone-of-se.nesuke.com/sv-basic/linux-basic/apache-mpm-prefork-worker-event/

event driven

一応ここではNginxのevent drivenという前提でお話ししておきます。

  • マルチプロセスで動いている
    • シングルスレッドで動き、CPUの数だけプロセスを用意している
    • シングルスレッドのためメモリ空間の共有が可能でコンテキストスイッチが発生しない
  • Node.jsと同様、イベントループ方式(シングルスレッドでループ処理をまわし、キューに溜まったイベントを処理していく処理方式)が利用されている
    • が、定義されたイベントをもとにプロセスやスレッドを増減させ一定の数に保てることで、C10K問題を解決している

Webサーバーの稼働方式から見たApacheとNginxとLiteSpeedの違い

以上のWebサーバーの稼働方式の違いという観点から、この節ではPHPのドキュメントに掲載されている3種のWebサーバー=Apache, Nginx, LiteSpeedの違いを簡単にまとめます。

特にApacheとNginxの比較はこちらの記事を参考にさせていただきました。

https://qiita.com/kamihork/items/49e2a363da7d840a4149

https://blog.inductor.me/entry/2022/05/31/150707

またLiteSpeedは自分も利用したことがないためあまり明るくなく、NginxとLiteSpeedの性能比較についてはこちらの記事を参考にさせていただいています。

https://qiita.com/BRSF/items/ed838139a97e5d702d09

シェアについては以下の通りで、NginxがややApacheより上で、その1/3程度がLiteSpeedという現状のうようでした。


出典: https://w3techs.com/technologies/history_overview/web_server/ms/y

Apache

Nginx

  • event driven方式
  • リバースプロキシにできたりなど、使い方に幅がある
  • ただしWebサーバーとしてPHPランタイムを構築しようとすると、Apacheモジュール版に比べると手間がかかる

LiteSpeed

  • event deriven方式
  • Apacheと互換性がある
  • Wordpressをロードした時の表示速度はNginxの最大12倍、Apacheの84倍と圧倒的に速い[2]
  • ただしApache,Nginxに比べてシェアが少ない

プロセス(プログラム=PHP)の実行方式

ここまででWebサーバーがリクエストにより起動したプロセスをどのように捌いているのかが理解できたと思います。

ではここからは、先ほどから出てきているプロセスに再び焦点を当てたいと思います。

ここまででプロセスはプログラムの実行単位ということはお分かりかと思いますが、その実態とは具体的になんでしょうか?

…それはもちろん、PHPのプログラムのことです。

では、PHPのプログラムを実行している(=プロセスを起動している)のは一体誰なのでしょうか?

それを知るためには、WebサーバーにおけるPHPの実行方式について理解する必要があります。

結論から言うと、WebサーバーにおけるPHPの実行方式は2つに大別されます。

モジュール方式

先に挙げたWebサーバーのなかだとApacheで利用可能な方式です。

これはWebサーバーに組み込まれているモジュールがPHPコードを引き渡したPHPを外部実行して、その実行結果にHTTPヘッダなどを付け加えてレスポンスを返します。

この方式ではWebサーバーの実行権限でPHPが動作するため、ホスティング(例えばレンタルサーバーやIaaSのマルチテナント)などサーバーを複数のユーザーで利用する形態では各ユーザーに分けた実行権限制御ができません。

また、この方式ではPHPを実行しているのはWebサーバー(の拡張モジュール)ということになります。


出典: https://www.fumi.org/neta/201205sv.html

CGI方式

PHPをWebサーバーで実行する際、最近多くの場合で採用されているのがこの方式です。

これはWebサーバーが実行形式のPHPコードの外部実行や,PHPコードや引数を引き渡したphp-cgiを外部実行し,その出力を応答(response)する方式です。

モジュール形式と異なるのは、PHPコードの実行をWebサーバーのプロセスとは完全に別個で実行する点です。

その場合PHPコードごとの権限で実行できるので、モジュール版のように誤って他ユーザーに干渉するといった危険がありません。

またこの方式では、PHPを実行しているのはアプリケーションサーバー(PHPインタプリタ)ということになります。

この記事の表題に登場するphp-fpmとは、CGI方式の中でも特にFastCGIという方式のことを指します。

またCGI方式はphp-cgi方式とScript方式の2つに大別することができます。

php-cgi

WebサーバーがPHPコードを php-cgiへ環境変数・引数付きで投げ、その実行結果をクライアントへ応答する方式です。


出典: https://www.fumi.org/neta/201205sv.html

Script

PHPコードが実行形式となっており、WebサーバーがPHPコードを外部実行しその実行結果をクライアントへ応答する方式です。


出典: https://www.fumi.org/neta/201205sv.html

FastCGIとは

ではこの記事の表題であるphp-fpmで利用されているFastCGIとはなんでしょうか?

これは文字通り、Fast(速い) / CGI です。速さが違う要因には、やはりプロセスの扱い方に違いがあります。

CGIは各リクエストごとに新しいプロセスを生成しその後終了するのに対してFastCGIはプロセスが常駐し複数のリクエストを受け取ることができるため、オーバーヘッドが少なく済みます。なので、速い。

そのほかの特徴については以下をご覧ください。

https://www.php.net/manual/ja/install.fpm.php

結論: php-fpmとは?

PHPの実行方式の一つであり、FastCGIプロトコルを介してWebサーバー(通常はNginxやApacheなど)と通信し、PHPの処理を行うアプリケーションサーバー的な役割を果たすもの。

大量のアクセスがある想定で、かつセキュリティ的な配慮が求められる場合に利用するのが良い。

感想

WebにおけるPHP環境構築の際には、以上を把握した上で選択していく必要があるなと改めて思いました。

特にプロセスへのアプローチというのは非常に重要で、そもそもプロセスを理解していない以上適切な環境を作るのは不可能だと言う再認識を得ました。やはりプロセスとは何か?を知らないのにWebの環境構築もへったくれもないなという。

ランタイムの構築とは「いかにプロセスを扱うか?」であり、さらに突き詰めると「CPUとメモリをいかに安全かつ効率的に扱うか?」であることを確認できました。

あと、PHPは一度スクリプトを書いてしまえばWebサーバーが何だろうが実行方式がなんだろうが、JavaVMみたいなものがなくともだいたい動かせるんだろうなということも副次的に理解しました。

…と思ってたら今日自分が認識したことをめもりーさんtadsanのアニキが3年前にXで話してるのを見つけた。

https://twitter.com/m3m0r7/status/1337702467765583872

https://twitter.com/tadsan/status/1337944536262135809

流石っす👍

📢 Kobe.tsというTypeScriptコミュニティを主催しています

フロント・バックエンドに限らず、周辺知識も含めてTypeScriptの勉強会を主催しています。

毎朝オフラインでもくもくしたり、神戸を中心に関西でLTもしています。

盛り上がってる感を出していきたいので、良ければメンバーにだけでもなってください😣

https://kobets.connpass.com/


脚注
  1. そのあたりの詳しい話はこちらの記事が参考になります。 ↩︎

  2. https://webst8.com/blog/web-server-software/#st-toc-h-8 ↩︎

Discussion

takahashimtakahashim

こんにちは、Apache全盛の時代からApacheを使ってきた者です。
今のApacheはmpm_event + php-fpmを使うのであればC10K問題を意識しなくてもいいかと思います。というより、現代はWebサーバーが全てではないですし、C10Kとは別のボトルネックを気にした方が良いでしょう…といったことはinductorさんによる記事が詳しいので読まれると良いかと思います。

https://blog.inductor.me/entry/2022/05/31/150707

Apacheのworker MPMとevent MPMの違いはイベント駆動とはあまり関係がなく、worker MPMはkeep-alive時にもworkerを使用していたものが、event MPMではlistenerスレッドに渡すことで大量のコネクションを扱えるようにしているようでした。詳しくはApacheの公式ドキュメントやDataDogのブログ記事が参考になるかと思います(私もコードを読んでるわけではなくドキュメントで理解してるだけです。すみません)。

https://httpd.apache.org/docs/2.4/mod/event.html

https://www.datadoghq.com/ja/blog/monitoring-apache-web-server-performance/

なおこのような扱いができるようになったのは上記のApacheのドキュメントにもあるように、epollやkqueueといったOSの改良によるところもあります。この辺の時系列についてはkzysさんの記事が詳しいです。

https://8-p.info/ja/c10k.html

KanonKanon

とても詳しくありがとうございます!
勉強させていただきます🙏