はじめに
こんにちは、レコチョクの星野です。
最近は murket というプロダクトのパフォーマンスチューニング等に従事しています。
コードの修正ももちろんやるのですが、 php-fpm の設定を考えることも増えてきました。
php-fpm の設定プラクティスを調べていると、めちゃくちゃ大量の言説があって混乱します。
かくいう私も公式ドキュメントを読んだ程度の素人なので、どれがいいのか判別することもできません。
そこで今回は、php-fpm について調べて murket のパフォーマンスチューニングに役立てることにしました~
今回の目的
やりたいこと
- murket の php-fpm の設定を最適化したい
- ECSコンテナのCPU使用率を最大化しつつ、リクエスト処理速度を落としたくない
- 受け付けられるリクエスト数を最大化したい
論点
- プロセス管理は
staticかdynamicか - プロセス数(今回は
max_children)はどう設定するべきか listen.backlogはどう設定するべきか
調べたいこと
- プロセス管理はいつ、どのように行われるのか
- 子プロセスはどのように作成されるのか
- 子プロセスはどこに制約を受けるのか(=子プロセス数の限界はあるのか)
- php-fpm のリクエスト受付数はどこに制約を受けるのか (=リクエスト受付数の限界はあるのか)
想定読者
- php-fpm をある程度知っている人
- php-fpm の動きを知りたい人
- php-fpm と Linux との関わりを知りたい人
前提・予習
前提1: 今回のソースコード
php-fpm や php のソースコードは php-src にあります。
今回は2026/02/24時点の最新リリースである php-8.5.3 を参照します。
php-fpm のソースは sapi/fpm/fpm 配下にあります。
C言語で書かれているので、興味ある方は読んでみてください。
前提2: 今回のOS
Linux (Amazon Linux や RHEL 系 OS) を想定して話を進めます。
予習1: php とは?
php はプログラミング言語です。
今回の記事を読むにあたり理解しておいて欲しいのは php は基本的にシングルプロセス・シングルスレッド(= 1プロセスあたりCPUを1コアまでしか使わない)であることです。
予習2: php-fpm とは?
php-fpm は php の FastCGI です。 php-fpm がプロセスを管理して数を調整してくれたり、再起動してくれたりします。 (いつもありがとう)
予習3: ファイルディスクリプタ
今回 php-fpm のソースを読む際に ファイルディスクリプタ というのが大量に出てくるので初めに補足です。
ファイルディスクリプタはファイルやネットワーク通信などに割り当てられる整理番号です。
ファイルディスクリプタは Linux 側にある I/O を管理するための構造体と紐づきます。
予習4: ソケット
ソケットは Linux における(プロセス間の)通信端点で、その実体はファイルディスクリプタです。
ソケットに関する関数は以下の通りです。
socket(): ソケット(のファイルディスクリプタ(FD))の作成bind(): ソケットとIPアドレス/ポートの対応付けlisten(): ソケットの接続準備accept(): ソケット接続待機recv(): データ受信send(): データ送信close(): ソケット切断
UNIXには
Everything is a file.(プロセス以外はファイル) という考え方が存在するらしく、ファイル I/O もネットワーク I/O も同じように扱っている背景があるようです。
全体の流れをつかむ
さて、長くなりましたが本題です。
main() を読む
最初に実行される関数は main() です。
コイツをざっくり読みます。
int main(int argc, char *argv[])
{
int exit_status = FPM_EXIT_OK;
int c, use_extended_info = 0;
/* (中略) 設定うにゃうにゃ */
enum fpm_init_return_status ret = fpm_init(argc, argv, fpm_config ? fpm_config : CGIG(fpm_config), fpm_prefix, fpm_pid, test_conf, php_allow_to_run_as_root, force_daemon, force_stderr);
/* (中略) fpm_init エラー処理とか */
fcgi_fd = fpm_run(&max_requests);
/* (中略) 環境変数とか */
request = fpm_init_request(fcgi_fd);
zend_first_try {
/* (中略) リクエスト処理 */
} zend_catch {
exit_status = FPM_EXIT_SOFTWARE;
} zend_end_try();
out:
/* (中略) 後始末の処理 */
}
https://github.com/php/php-src/blob/php-8.5.3/sapi/fpm/fpm/fpm_main.c
main() は大体こんな構成になっています。
- 初期設定 (環境変数とか設定の読み込み)
- php-fpm の初期化 (
fpm_initの実行) - php-fpm の実行 (
fpm_runの実行) - リクエスト処理
この時点でちょっと様子がおかしいです。
main() にリクエストを受け付けるコードがあるの?
(この辺は後でフォローしますので今はちょっと我慢してください)
ちょっと我慢しつつ先に進みます。
fpm_init() を読む
fpm_init() は fpm.c に書いてあります。
enum fpm_init_return_status fpm_init(int argc, char **argv, char *config, char *prefix, char *pid, int test_conf, int run_as_root, int force_daemon, int force_stderr) /* {{{ */
{
/* (中略) 設定うにゃうにゃ */
if (0 > fpm_php_init_main() ||
0 > fpm_stdio_init_main() ||
0 > fpm_conf_init_main(test_conf, force_daemon) ||
0 > fpm_unix_init_main() ||
0 > fpm_scoreboard_init_main() ||
0 > fpm_pctl_init_main() ||
0 > fpm_env_init_main() ||
0 > fpm_signals_init_main() ||
0 > fpm_children_init_main() ||
0 > fpm_sockets_init_main() ||
0 > fpm_worker_pool_init_main() ||
0 > fpm_event_init_main()) {
if (fpm_globals.test_successful) {
return FPM_INIT_EXIT_OK;
} else {
zlog(ZLOG_ERROR, "FPM initialization failed");
return FPM_INIT_ERROR;
}
}
/* (中略) エラー処理とか */
return FPM_INIT_CONTINUE;
}
https://github.com/php/php-src/blob/f665c20219d0861fcbdd1f663fc4ac061f245a3c/sapi/fpm/fpm/fpm.c#L44
ここで大量の初期化を行っていますね。
今回は読みませんが、大まかな役割はこんな感じ(ChatGPTまとめ)。
| 関数 | 主な役割 | メモ |
|---|---|---|
fpm_php_init_main() |
PHPコアの初期化 | Zend Engine や内部関数のセットアップ |
fpm_stdio_init_main() |
標準入出力の準備 | 標準ファイルディスクリプタのリダイレクトやバッファ設定 |
fpm_conf_init_main(test_conf, force_daemon) |
設定のロード | php-fpm.conf の読み込み、コマンドライン引数優先 |
fpm_unix_init_main() |
UNIX固有初期化 | UID/GID の切替や chroot(必要なら)、unixソケット準備 |
fpm_scoreboard_init_main() |
スコアボード初期化 | master ↔ worker 状態共有用メモリ(TSRM) |
fpm_pctl_init_main() |
プロセスマネージャ初期化 | pm = static/dynamic/ondemand の設定 |
fpm_env_init_main() |
環境変数初期化 | PATH, HOME, LANG など FPM 用環境変数 |
fpm_signals_init_main() |
シグナルハンドラ設定 | SIGTERM, SIGCHLD, SIGUSR1 など |
fpm_children_init_main() |
子プロセス関連構造初期化 | 子プロセス情報テーブル、pid ファイル関連 |
fpm_sockets_init_main() |
ソケット作成 | TCP/UNIXソケット listen、backlog 設定 |
fpm_worker_pool_init_main() |
ワーカープロセスプール設定 | プールごとの子プロセス数や制限を設定 |
fpm_event_init_main() |
イベント管理初期化 | epoll/select などイベントループの準備 |
ここで重要なのは fpm_children_init_main() や fpm_worker_pool_init_main() ではまだ子プロセスを作成しないことです。
fpm_sockets_init_main() を読む
今回はリクエスト受付数について知りたいので、関係ありそうな fpm_sockets_init_main() は読んでおきます。
int fpm_sockets_init_main(void)
{
/* (中略) 設定うにゃうにゃ */
/* import inherited sockets */
for (i = 0; i next) {
switch (wp->listen_address_domain) {
case FPM_AF_INET :
wp->listening_socket = fpm_socket_af_inet_listening_socket(wp);
break;
case FPM_AF_UNIX :
if (0 > fpm_unix_resolve_socket_permissions(wp)) {
return -1;
}
wp->listening_socket = fpm_socket_af_unix_listening_socket(wp);
break;
}
/* (中略) 後始末とか */
}
ここでワーカープール(子プロセスが入るためのプール)ごとにソケットを作成しています。
つまり、同じワーカープール内の子プロセスは同じソケットを参照するということです。
ソケットを作成する処理は fpm_socket_af_inet_listening_socket or fpm_socket_af_unix_listening_socket -> fpm_sockets_get_listening_socket() -> fpm_sockets_new_listening_socket() が担っています。
static int fpm_sockets_new_listening_socket(struct fpm_worker_pool_s *wp, struct sockaddr *sa, int socklen) /* {{{ */
{
/* (中略) 設定うにゃうにゃ */
sock = socket(sa->sa_family, SOCK_STREAM, 0);
/* (中略) エラー処理とか */
if (0 > bind(sock, sa, socklen)) {
/* (中略) エラー処理とか */
}
/* (中略) エラー処理とか */
if (0 > listen(sock, wp->config->listen_backlog)) {
/* (中略) エラー処理とか */
}
/* (中略) エラー処理とか */
return sock;
}
ここで socket() と bind() 、 listen() を実行しています。
まずここが通信するためのソケットを作成していました。
ここで重要なのは、 listen.backlog が listen() に直接渡されているところです。
なるほど、「バックログ」 ってのは php-fpm の機能じゃなくて、ソケットに備え付けられた機能だったのね…
fpm_run() を読む
さて、先ほども言及した通り、 fpm_init() は子プロセスを作成していませんでした。
なので、たぶんこっちで子プロセスを作成しているんでしょう。
読み進めていきましょう。
/* children: return listening socket
parent: never return */
int fpm_run(int *max_requests) /* {{{ */
{
struct fpm_worker_pool_s *wp;
/* create initial children in all pools */
for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
int is_parent;
is_parent = fpm_children_create_initial(wp);
if (!is_parent) {
goto run_child;
}
/* handle error */
if (is_parent == 2) {
fpm_pctl(FPM_PCTL_STATE_TERMINATING, FPM_PCTL_ACTION_SET);
fpm_event_loop(1);
}
}
/* run event loop forever */
fpm_event_loop(0);
run_child: /* only workers reach this point */
fpm_cleanups_run(FPM_CLEANUP_CHILD);
*max_requests = fpm_globals.max_requests;
return fpm_globals.listening_socket;
}
https://github.com/php/php-src/blob/f665c20219d0861fcbdd1f663fc4ac061f245a3c/sapi/fpm/fpm/fpm.c#L91
またここでもあんまりよく分からないですね。
親プロセスが実行しそうな fpm_event_loop() と 子プロセスが実行しそうな run_child が混在している…
コメントにもあるように、 fpm_children_create_initial() で子プロセスを作成しているようなので、そこから読み進めましょう。
int fpm_children_create_initial(struct fpm_worker_pool_s *wp) /* {{{ */
{
if (wp->config->pm == PM_STYLE_ONDEMAND) {
wp->ondemand_event = (struct fpm_event_s *)malloc(sizeof(struct fpm_event_s));
if (!wp->ondemand_event) {
zlog(ZLOG_ERROR, "[pool %s] unable to malloc the ondemand socket event", wp->config->name);
// FIXME handle crash
return 1;
}
memset(wp->ondemand_event, 0, sizeof(struct fpm_event_s));
fpm_event_set(wp->ondemand_event, wp->listening_socket, FPM_EV_READ | FPM_EV_EDGE, fpm_pctl_on_socket_accept, wp);
wp->socket_event_set = 1;
fpm_event_add(wp->ondemand_event, 0);
return 1;
}
return fpm_children_make(wp, 0 /* not in event loop yet */, 0, 1);
}
子プロセスの実体を作っているのは fpm_children_make() っぽいですね。
int fpm_children_make(struct fpm_worker_pool_s *wp, int in_event_loop, int nb_to_spawn, int is_debug) /* {{{ */
{
/* (中略) 設定うにゃうにゃ */
if (wp->config->pm == PM_STYLE_DYNAMIC) {
if (!in_event_loop) { /* starting */
max = wp->config->pm_start_servers;
} else {
max = wp->running_children + nb_to_spawn;
}
} else if (wp->config->pm == PM_STYLE_ONDEMAND) {
if (!in_event_loop) { /* starting */
max = 0; /* do not create any child at startup */
} else {
max = wp->running_children + nb_to_spawn;
}
} else { /* PM_STYLE_STATIC */
max = wp->config->pm_max_children;
}
/*
* fork children while:
* - fpm_pctl_can_spawn_children : FPM is running in a NORMAL state (aka not restart, stop or reload)
* - wp->running_children < max : there is less than the max process for the current pool
* - (fpm_global_config.process_max < 1 || fpm_globals.running_children running_children < max && (fpm_global_config.process_max < 1 || fpm_globals.running_children pid = pid;
fpm_clock_get(&child->started);
fpm_parent_resources_use(child);
zlog(is_debug ? ZLOG_DEBUG : ZLOG_NOTICE, "[pool %s] child %d started", wp->config->name, (int) pid);
}
}
/* (中略) ログ出力とか */
return 1; /* we are done */
}
ここでは要するに作成するべき子プロセスの数だけ fork() をして、その結果をもとに処理をしているようです。
作成すべき子プロセスの数は wp->config->pm つまり www.conf の pm によって動作が変わっていますね。
ここが明らかに子プロセスを作成していそうなので、注意して読んでみます。
fork() とは
fork() について、こちらでは以下のような説明がされています。
fork()は親プロセスから子プロセスを分岐させて、並行に複数の処理を行うことができる関数です。 コピーはfork()が実行されたタイミングから発生します。つまり、必ずしも親プロセスを「最初から」コピーするわけではないということは頭に入れておきましょう。
つまり、fork() 以降のコードは親プロセスも生成された子プロセスも実行されるということです。
fork() で作成された子プロセスの戻り値は 0 になり、 fork() 呼び出し元の親プロセスは親プロセスのプロセスIDを返す仕様になっているので、以降の switch (pid) で親プロセスと子プロセスごとの処理があるわけですね。
これらをまとめると、 fpm_children_make() はここで子プロセスを作成し、子プロセスが作成できたら 0 を、 子プロセスを生成した親プロセスは 1 を返却するという仕様ですね。
fpm_run() をもう一度読む
/* children: return listening socket
parent: never return */
int fpm_run(int *max_requests) /* {{{ */
{
struct fpm_worker_pool_s *wp;
/* create initial children in all pools */
for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
int is_parent;
is_parent = fpm_children_create_initial(wp);
if (!is_parent) {
goto run_child;
}
/* handle error */
if (is_parent == 2) {
fpm_pctl(FPM_PCTL_STATE_TERMINATING, FPM_PCTL_ACTION_SET);
fpm_event_loop(1);
}
}
/* run event loop forever */
fpm_event_loop(0);
run_child: /* only workers reach this point */
fpm_cleanups_run(FPM_CLEANUP_CHILD);
*max_requests = fpm_globals.max_requests;
return fpm_globals.listening_socket;
}
https://github.com/php/php-src/blob/f665c20219d0861fcbdd1f663fc4ac061f245a3c/sapi/fpm/fpm/fpm.c#L91
fpm_children_create_initial() は fpm_children_make() (の戻り値)を返していたことに注意すると、 fpm_run() もまた親プロセスと子プロセスが実行することになって、
- 親プロセス (
is_parentが1) の場合はfpm_event_loop()を実行する。 - 子プロセス (
is_parentが0) の場合はrun_childまで移動する。
親プロセスは fpm_event_loop() を処理し続け (= return しない)、
一方子プロセスは main() に listen しているソケットのファイルディスクリプタ(FD)を返却することになります。
main() をもう一度読む
ここまで来ると、何となく全体がつかめて来ました。
最初に疑問だった main() をもう一度読んでみます。
int main(int argc, char *argv[])
{
int exit_status = FPM_EXIT_OK;
int c, use_extended_info = 0;
/* (中略) 設定うにゃうにゃ */
enum fpm_init_return_status ret = fpm_init(argc, argv, fpm_config ? fpm_config : CGIG(fpm_config), fpm_prefix, fpm_pid, test_conf, php_allow_to_run_as_root, force_daemon, force_stderr);
/* (中略) fpm_init エラー処理とか */
fcgi_fd = fpm_run(&max_requests);
/* (中略) 環境変数とか */
request = fpm_init_request(fcgi_fd);
zend_first_try {
/* (中略) リクエスト処理 */
} zend_catch {
exit_status = FPM_EXIT_SOFTWARE;
} zend_end_try();
out:
/* (中略) 後始末の処理 */
}
この処理の fcgi_fd = fpm_run(&max_requests); 以降の処理は fork()で作成された子プロセスだけが実行するというわけです。
何となく全貌が見えてきましたね。長かった~~~
これまでのまとめ
これらの流れをまとめるとこんな感じです。

親プロセスの流れをつかむ
先ほど確認したように、親プロセスは fpm_event_loop() を実行して終わりなので、これを読んでみましょう。
fpm_event_loop() を読む
fpm_event_loop() はこんな感じです。
void fpm_event_loop(int err) /* {{{ */
{
/* (中略) 設定とかエラー処理とか */
fpm_event_set(&signal_fd_event, fpm_signals_get_fd(), FPM_EV_READ, &fpm_got_signal, NULL);
fpm_event_add(&signal_fd_event, 0);
/* add timers */
if (fpm_globals.heartbeat > 0) {
fpm_pctl_heartbeat(NULL, 0, NULL);
}
if (!err) {
fpm_pctl_perform_idle_server_maintenance_heartbeat(NULL, 0, NULL);
/* (中略) ログ出力 */
#ifdef HAVE_SYSTEMD
fpm_systemd_heartbeat(NULL, 0, NULL);
#endif
}
while (1) {
/* (中略) 設定とかエラー処理とか */
/* search in the timeout queue for the next timer to trigger */
q = fpm_event_queue_timer;
while (q) {
if (!timerisset(&ms)) {
ms = q->ev->timeout;
} else {
if (timercmp(&q->ev->timeout, &ms, ev->timeout;
}
}
q = q->next;
}
/* (中略) 設定うにゃうにゃ */
ret = module->wait(fpm_event_queue_fd, timeout);
/* (中略) 子プロセス用の処理とか */
/* trigger timers */
q = fpm_event_queue_timer;
while (q) {
struct fpm_event_queue_s *next = q->next;
fpm_clock_get(&now);
if (q->ev) {
if (timercmp(&now, &q->ev->timeout, >) || timercmp(&now, &q->ev->timeout, ==)) {
/*(中略) イベントの処理とか*/
fpm_event_fire(ev);
/* sanity check */
if (fpm_globals.parent_pid != getpid()) {
return;
}
}
}
q = next;
}
}
}
fpm_event_loop() は大きく
- イベントの登録
- イベントループ
という構成になっています。
fpm_event_loop() で登録されるイベント
fpm_event_set(&signal_fd_event, fpm_signals_get_fd(), FPM_EV_READ, &fpm_got_signal, NULL);
fpm_event_add(&signal_fd_event, 0);
/* add timers */
if (fpm_globals.heartbeat > 0) {
fpm_pctl_heartbeat(NULL, 0, NULL);
}
if (!err) {
fpm_pctl_perform_idle_server_maintenance_heartbeat(NULL, 0, NULL);
/* (中略) ログ出力 */
#ifdef HAVE_SYSTEMD
fpm_systemd_heartbeat(NULL, 0, NULL);
#endif
}
このタイミングでいくつかのイベントが登録されています。
- シグナルイベント
- ハートビートイベント
- サーバメンテナンスイベント
(注) 別の場所でもいくつかのイベントを登録していますが今回は割愛します。
調べたところ今回の疑問: だれが子プロセスを管理しているのか はサーバメンテナンスイベントが関係していそうなので、そこを読み進めます。
サーバメンテナンスイベントは fpm_pctl_perform_idle_server_maintenance_heartbeat() が登録します。
void fpm_pctl_perform_idle_server_maintenance_heartbeat(struct fpm_event_s *ev, short which, void *arg) /* {{{ */
{
/* (中略) 設定とかエラー処理とか */
if (which == FPM_EV_TIMEOUT) {
fpm_clock_get(&now);
if (fpm_pctl_can_spawn_children()) {
fpm_pctl_perform_idle_server_maintenance(&now);
/* (中略) バリデーション */
}
return;
}
/* first call without setting which to initialize the timer */
fpm_event_set_timer(&heartbeat, FPM_EV_PERSIST, &fpm_pctl_perform_idle_server_maintenance_heartbeat, NULL);
fpm_event_add(&heartbeat, FPM_IDLE_SERVER_MAINTENANCE_HEARTBEAT);
}
この関数が呼び出す fpm_pctl_perform_idle_server_maintenance() が実際にプロセスを管理しています。
static void fpm_pctl_perform_idle_server_maintenance(struct timeval *now) /* {{{ */
{
struct fpm_worker_pool_s *wp;
for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
struct fpm_child_s *child;
struct fpm_child_s *last_idle_child = NULL;
int idle = 0;
int active = 0;
int children_to_fork;
unsigned cur_lq = 0;
if (wp->config == NULL) continue;
/* update status structure for all PMs */
if (wp->listen_address_domain == FPM_AF_INET) {
if (0 > fpm_socket_get_listening_queue(wp->listening_socket, &cur_lq, NULL)) {
cur_lq = 0;
#if 0
} else {
if (cur_lq > 0) {
if (!wp->warn_lq) {
zlog(ZLOG_WARNING, "[pool %s] listening queue is not empty, #%d requests are waiting to be served, consider raising pm.max_children setting (%d)", wp->config->name, cur_lq, wp->config->pm_max_children);
wp->warn_lq = 1;
}
} else {
wp->warn_lq = 0;
}
#endif
}
}
fpm_scoreboard_update_begin(wp->scoreboard);
for (child = wp->children; child; child = child->next) {
if (fpm_request_is_idle(child)) {
if (last_idle_child == NULL) {
last_idle_child = child;
} else {
if (timercmp(&child->started, &last_idle_child->started, scoreboard);
/* this is specific to PM_STYLE_ONDEMAND */
if (wp->config->pm == PM_STYLE_ONDEMAND) {
struct timeval last, now;
zlog(ZLOG_DEBUG, "[pool %s] currently %d active children, %d spare children", wp->config->name, active, idle);
if (!last_idle_child) continue;
fpm_request_last_activity(last_idle_child, &last);
fpm_clock_get(&now);
if (last.tv_sec config->pm_process_idle_timeout) {
fpm_pctl_kill_idle_child(last_idle_child);
}
continue;
}
/* the rest is only used by PM_STYLE_DYNAMIC */
if (wp->config->pm != PM_STYLE_DYNAMIC) continue;
zlog(ZLOG_DEBUG, "[pool %s] currently %d active children, %d spare children, %d running children. Spawning rate %d", wp->config->name, active, idle, wp->running_children, wp->idle_spawn_rate);
if (idle > wp->config->pm_max_spare_servers && last_idle_child) {
fpm_pctl_kill_idle_child(last_idle_child);
wp->idle_spawn_rate = 1;
continue;
}
if (idle config->pm_min_spare_servers) {
if (wp->running_children >= wp->config->pm_max_children) {
if (!wp->warn_max_children && !wp->shared) {
fpm_scoreboard_update(0, 0, 0, 0, 0, 1, 0, 0, FPM_SCOREBOARD_ACTION_INC, wp->scoreboard);
zlog(ZLOG_WARNING, "[pool %s] server reached pm.max_children setting (%d), consider raising it", wp->config->name, wp->config->pm_max_children);
wp->warn_max_children = 1;
}
wp->idle_spawn_rate = 1;
continue;
}
if (wp->idle_spawn_rate >= 8) {
zlog(ZLOG_WARNING, "[pool %s] seems busy (you may need to increase pm.start_servers, or pm.min/max_spare_servers), spawning %d children, there are %d idle, and %d total children", wp->config->name, wp->idle_spawn_rate, idle, wp->running_children);
}
/* compute the number of idle process to spawn */
children_to_fork = MIN(wp->idle_spawn_rate, wp->config->pm_min_spare_servers - idle);
/* get sure it won't exceed max_children */
children_to_fork = MIN(children_to_fork, wp->config->pm_max_children - wp->running_children);
if (children_to_fork warn_max_children && !wp->shared) {
fpm_scoreboard_update(0, 0, 0, 0, 0, 1, 0, 0, FPM_SCOREBOARD_ACTION_INC, wp->scoreboard);
zlog(ZLOG_WARNING, "[pool %s] server reached pm.max_children setting (%d), consider raising it", wp->config->name, wp->config->pm_max_children);
wp->warn_max_children = 1;
}
wp->idle_spawn_rate = 1;
continue;
}
wp->warn_max_children = 0;
fpm_children_make(wp, 1, children_to_fork, 1);
/* if it's a child, stop here without creating the next event
* this event is reserved to the master process
*/
if (fpm_globals.is_child) {
return;
}
zlog(ZLOG_DEBUG, "[pool %s] %d child(ren) have been created dynamically", wp->config->name, children_to_fork);
/* Double the spawn rate for the next iteration */
if (wp->idle_spawn_rate config->pm_max_spawn_rate) {
wp->idle_spawn_rate *= 2;
}
continue;
}
wp->idle_spawn_rate = 1;
}
}
ここは複雑ですが重要なところなのでそのまま載せています。
ただやっていることは要するに
- 現在アクティブな子プロセスと待機中の子プロセスの数をそれぞれ数える
- 多すぎる、少なすぎる場合は子プロセスを
fork()ないしkill()する
の 2 つだけですね。
イベントループの実体
さて、次にイベントループそのものです。
while (1) {
/* (中略) 設定とかエラー処理とか */
/* search in the timeout queue for the next timer to trigger */
q = fpm_event_queue_timer;
while (q) {
if (!timerisset(&ms)) {
ms = q->ev->timeout;
} else {
if (timercmp(&q->ev->timeout, &ms, ev->timeout;
}
}
q = q->next;
}
/* (中略) 設定うにゃうにゃ */
ret = module->wait(fpm_event_queue_fd, timeout);
/* (中略) 子プロセス用の処理とか */
/* trigger timers */
q = fpm_event_queue_timer;
while (q) {
struct fpm_event_queue_s *next = q->next;
fpm_clock_get(&now);
if (q->ev) {
if (timercmp(&now, &q->ev->timeout, >) || timercmp(&now, &q->ev->timeout, ==)) {
/*(中略) イベントの処理とか*/
fpm_event_fire(ev);
/* sanity check */
if (fpm_globals.parent_pid != getpid()) {
return;
}
}
}
q = next;
}
}
イベントループがやっていることは大きく
- epoll(= Linux が提供するファイルディスクリプタの I/O イベントを通知する API) から来るイベントの処理
- イベントキューにあるイベントの処理
の2つに大別できます。
(注) 環境によっては epoll とは限りませんが、今回は Linux を前提としているため epoll_wait の場合を考えます。
分かりやすくいえば、 Linux からくるイベントと php-fpm から来るイベントの処理をそれぞれ行うだけです。
それぞれの流れを見ていきます。
epoll からくるイベントの処理
epoll のイベントは
ret = module->wait(fpm_event_queue_fd, timeout);
で処理されます。
static int fpm_event_epoll_wait(struct fpm_event_queue_s *queue, unsigned long int timeout) /* {{{ */
{
int ret, i;
/* ensure we have a clean epoolfds before calling epoll_wait() */
memset(epollfds, 0, sizeof(struct epoll_event) * nepollfds);
/* wait for incoming event or timeout */
ret = epoll_wait(epollfd, epollfds, nepollfds, timeout);
/* (中略) エラー処理 */
/* events have been triggered, let's fire them */
for (i = 0; i next;
fpm_clock_get(&now);
if (q->ev) {
if (timercmp(&now, &q->ev->timeout, >) || timercmp(&now, &q->ev->timeout, ==)) {
/*(中略) イベントの処理とか*/
fpm_event_fire(ev);
/* sanity check */
if (fpm_globals.parent_pid != getpid()) {
return;
}
}
}
q = next;
}
大雑把に言えば
- 現在時刻 (
now) と イベントキューにあるイベントの発火時刻 (timeout) を比較する - イベント発火時刻を超過していればイベントを処理する
ことをやっています。
子プロセスの流れをつかむ
子プロセスの処理の流れについてもざっくり確認します。
fpm_run() を読む
子プロセスが実際にリクエストを受け取っているのは main() 部分です。
先ほども見た通り、 main() は fpm_run() を実行し、そこで子プロセスを fork() します。
fpm_run() は子プロセスのみ return します (= main() に戻ってくる)。
fpm_run() 以降のコードを読んでみます。
int main(int argc, char *argv[])
{
/* (中略) さっき話した場所 */
fcgi_fd = fpm_run(&max_requests);
parent = 0;
/* (中略) 設定うにゃうにゃ */
request = fpm_init_request(fcgi_fd);
zend_first_try {
while (EXPECTED(fcgi_accept_request(request) >= 0)) {
/* (中略) 設定うにゃうにゃ */
fpm_request_info();
/* (中略) エラー処理とか */
if (UNEXPECTED(php_fopen_primary_script(&file_handle) == FAILURE)) {
/* (中略) エラー処理とか */
} else {
fpm_request_executing();
/* Reset exit status from the previous execution */
EG(exit_status) = 0;
php_execute_script(&file_handle);
}
/* (中略) 諸々の処理 */
fastcgi_request_done:
/* (中略) 諸々の処理 */
fpm_request_end();
fpm_log_write(NULL);
/* (中略) 諸々の処理 */
php_request_shutdown((void *) 0);
fpm_stdio_flush_child();
requests++;
if (UNEXPECTED(max_requests && (requests == max_requests))) {
fcgi_request_set_keep(request, 0);
fcgi_finish_request(request, 0);
break;
}
/* end of fastcgi loop */
}
fcgi_destroy_request(request);
fcgi_shutdown();
if (cgi_sapi_module.php_ini_path_override) {
free(cgi_sapi_module.php_ini_path_override);
}
php_ini_builder_deinit(&ini_builder);
} zend_catch {
exit_status = FPM_EXIT_SOFTWARE;
} zend_end_try();
out:
/* (中略) シャットダウン用の処理とか */
#ifdef ZTS
tsrm_shutdown();
#endif
return exit_status;
}
ここでやっているのは
- リクエストの受け取り・初期化
- リクエストの処理
- リクエスト処理後の後始末
の3つです。
リクエストの受け取りは fcgi_accept_request() がやっています。
ここがソケットの accept や管理、リクエスト読み込みなどを行います(php-fpm まで話が行くと終わらないので今回は省略)。
リクエストの処理は php_execute_script() で行われます。
要するに index.php の実行だと思っておけばよいでしょう。
リクエストを処理したら後始末を行います。 後始末とは、たとえばメモリを解放したり処理したリクエスト数を更新したりすることです。
調査結果
プロセス管理はいつ、どのように行われるのか
プロセス管理は親プロセスが行い、その実態はイベントループでした。
最初に子プロセスを必要な分だけ作成して、あとはイベントが起きたタイミングで子プロセスを追加したり削除したりしていましたね。
子プロセスはどのように作成されるのか
子プロセスは fpm_run() または fpm_pctl_perform_idle_server_maintenance() で作成(= fork()) していました。
子プロセスはどこに制約を受けるのか(=子プロセス数の限界はあるのか)
php-fpm は(static または dynamic の場合は) pm.max_children の数以上は作成されません。
Linux 側の制約としては php-fpm で子プロセスを作成する fork() の限界と言えます。
fork できる数は Linux の最大プロセス数に依存します。
Linux のプロセス数上限は pid_max や threads-max などのカーネルパラメータのほか、メモリなどのリソースにも影響を受けます。
パラメータの数値的な限界とハードウェアの限界の双方が絡むので少し複雑ですね。
php-fpm のリクエスト受付数はどこに制約を受けるのか (=リクエスト受付数の限界はあるのか)
php-fpm のリクエスト受付数とは 「FastCGI からのリクエストを待っている php-fpm のソケットがどれくらいのリクエストを受け付けるか」と等価です。
先ほど確認した通り、 子プロセスが listen するソケットはワーカープールごとに作成される1つのソケットです。 1つのソケットが受け付けられる最大リクエスト数はソケットのバックログ数に依存します。
ただし、作成可能なソケット数はファイルディスクリプタの上限に依存することに注意してください。
ファイルディスクリプタはプロセスごとに RLIMIT_NOFILE パラメータの数まで発行可能です。
また、ソケットのバックログ数にも制約があります。somaxconn パラメータがバックログの上限値です。
本題
static か dynamic か ondemand か
常時それなりのリクエストが来ている murket では static が良いと判断します。
プロセス数(今回は max_children)はどう設定するべきか
プロセス数には Linux 上の大きな制約がなかったため、ソースコードだけ見て最適を見るのは難しいですね。 リクエストあたりの平均CPU使用率や平均リクエスト処理時間から最適なプロセス数を概算していきましょう。
APMとかいれて少しずつ監視・改善しましょうねぇ(投げやり)。
listen.backlog はどう設定するべきか
murket においてはカーネルパラメータも合わせてなるべく大きく設定します。
murket というサービスの特性上、スパイクアクセスが発生した場合に 大量のリクエストがコネクションを確立できずに破棄されるのを防ぐためです。
キューに入るため多少レスポンスタイムは伸びますが、スパイクアクセス時には許容してよいでしょう(落ちるよりマシ)。
おわりに
いかがだったでしょうか?
いつも使ってるOSSの中身を見るのもなかなか楽しいですね。
そして何より、どこがどこに束縛を受けているのかというのを知ると、パフォーマンスチューニングの際に考慮することをちゃんと把握できて嬉しいですね。
ただ、今は理屈を知っただけなので、これだけではまだ完全なチューニングは難しいです。
先にも話した通り、APMや実例ベースでの議論もしなければいけませんね。
参考文献
- https://hackers-high.com/linux/php-fpm-config/
- https://agaroot-itp.com/blog/1437/
- https://www.php.net/manual/ja/install.fpm.php
- https://github.com/php/php-src
- https://www.ren510.dev/blog/network-socket
- https://zenn.dev/uta_san1012/articles/4d5dba93d06605
- https://en.wikipedia.org/wiki/Everything_is_a_file
- https://qiita.com/Michinosuke/items/0778a5344bdf81488114
- https://speakerdeck.com/naoki85/php-fpm-womotutoli-jie-siyou
- https://qiita.com/kotarella1110/items/634f6fafeb33ae0f51dc
- https://docs.redhat.com/ja/documentation/red_hat_enterprise_linux/8/html/monitoring_and_managing_system_status_and_performance/tuning-applications-with-a-large-number-of-incoming-requests_tuning-the-network-performance
- https://qiita.com/takc923/items/238597a53a2328025b09
星野