Apache event MPMとPHP-FPMのメモリ設定を最適化【PHP Fatal error: Out Of Memory】

ステージング環境をAWSで運用していましたが、頻繁にサーバーが落ちるという問題がありました。
そこでエラーログを見てみたところ、

```
PHP Fatal error:  Out of memory (allocated 54525952) (tried to allocate 86016 bytes) in Unknown on line 0
```  

というエラーが。
メモリ使用量がいっぱいになってしまいPHPのプログラムが動かなくなっていたので、ApacheとPHP-FPMの設定を見直しました。

同じようなエラーが出たという方の参考になるかもしれませんので備忘録として残します。

## まずはサーバーのスペック確認
メモリ数とコア数によって設定値を調整する必要があるので確認します。

メモリ 3757MB・コア数 1となっていました。

```bash:メモリ使用量を確認
$ free -m
              total       used       free     shared    buffers     cached
Mem:          3757        1247       2510         39        141        276
```  

```bash:コア数を確認
$ grep "cpu cores" /proc/cpuinfo
cpu cores       : 1
```  

次に、現在のApache MPMの設定を確認してみます。

```bash
$ httpd -V | grep MPM
Server MPM:     event
```  

event MPMを使っているようです。
eventはスレッドを使用している分、preforkと比べてメモリ使用量は抑えられると思いますが、
とはいえ設定ファイルがデフォルトの数値のままだったので、メモリ3757MBでも稼働するように調整が必要です。

もともとの設定は下記になっていましたので、こちらを改善していきます。

```conf:/etc/httpd/conf.modules.d/10-mpmevent.conf
<IfModule mpm_event_module>
        StartServers             10
        MinSpareThreads          100
        MaxSpareThreads          125
        ThreadLimit              110
        ThreadsPerChild          100
        MaxRequestWorkers        600
        MaxConnectionsPerChild   0
</IfModule>
```  

## 設定の見直しとチューニング
メモリ使用量を抑えるためには、

- サーバー起動時のプロセス数を設定する`StartServers`
- プロセス数の上限である`ServerLimit`
- 全プロセスにおけるスレッド合計を設定する`MaxRequestWorkers`

が特に大切かと思います。

また、`MaxConnectionsPerChild`を0にするとリクエスト数が増えた時にプロセスが肥大化してしまうので、一定の数値に設定する必要があります。

### Apacheの設定
[こちらの記事](https://medium.com/@sbuckpesch/apache2-and-php-fpm-performance-optimization-step-by-step-guide-1bfecf161534)を参考に設定していきます。

#### メモリ使用量の確認
設定値を計算するためには「プロセスあたりのメモリ使用量」を把握する必要があります。

下記のツールを用いることで物理メモリを実行ユーザーごとにまとめて表示してくれます。

```bash
$ cd ~
$ wget https://raw.githubusercontent.com/pixelb/ps_mem/master/ps_mem.py
$ chmod a+x ps_mem.py
$ sudo python ps_mem.py
...
 11.1 MiB +   8.8 MiB =  20.3 MiB       php-fpm-7.0 (5)
461.4 MiB + 777.5 KiB = 462.1 MiB       mysqld
  2.4 GiB +  42.5 MiB =   2.5 GiB       httpd (4)
---------------------------------
                          3.0 GiB
=================================
```

今回の場合、Apacheは2.5GBメモリを使用していて、1プロセスあたり625MB使用していることが分かりました。
また、PHP-FPMは1プロセスあたり4MBでした。

#### 実際の設定
`StartServers`、`ServerLimit`、`MaxRequestWorkers`にどのくらいの値を設定するかですが、
mediumのサイトを参考に、

> ServerLimit = (Total RAM - Memory used for Linux, DB, etc.) / process size
> MaxRequestWorkers = (Total RAM - Memory used for Linux, DB, etc.) / process size
> StartServers          (Number of Cores)

となっていて、

「Total RAM」は`$ free -m`で確認したメモリ容量、
「Memory used for Linux, DB, etc」はApache以外のメモリ使用量を考慮するためのもので、今回は全体のメモリの15%を取って500MBとします。
「process size」は先ほど確認したApacheの1プロセスあたりメモリ使用量、
「Number of Cores」はサーバーのコア数です。

なので今回の場合は、

```
ServerLimit = (3757MB - 500MB) / 625MB ≒ 5
MaxRequestWorkers = (3757MB - 500MB) / 625MB ≒ 5
StartServers = 1
```

で設定します。

##### スレッドの設定について

スレッドの設定 `MinSpareThreads` `MaxSpareThreads` `ThreadLimit` `ThreadsPerChild`の値に関しては、
今回は詳しい説明は省きますが、
スレッド自体は親プロセスのメモリを利用するので、スレッドがメモリ使用量に与える影響は小さいです。

なのでメモリ使用量を抑えるという点では、これらの設定値はそこまで重要ではないです。

ただし最低限、
`MinSpareThreads`は`MaxSpareThreads`より小さな値を設定する必要があり、
ドキュメントには「`ThreadLimit`は`ThreadsPerChild`の上限を超える値を設定してはいけない」と書かれていますので、同じ値にしておくのが安全でしょう。

---

最終的には、下記のような設定になりました。

```conf:/etc/httpd/conf.modules.d/10-mpmevent.conf
<IfModule mpm_event_module>
        ServerLimit              5
        StartServers             1
        MinSpareThreads          5
        MaxSpareThreads          75
        ThreadLimit              5
        ThreadsPerChild          5
        MaxRequestWorkers        5
        MaxConnectionsPerChild   300
</IfModule>
```  

### PHP-FPMの`max-children`を計算
PHP-FPMもプロセスを生成していますので、こちらの設定も修正する必要があります。

こちらの設定も先ほどのmediumのサイトを参考に設定します。

`pm.max_children`が一番重要で、プロセスの上限を設定するものです。

> pm.max_children = (total RAM - (DB etc) / process size)

今回は、

```
pm.max_children = (3757MB - 500MB) / 4MB ≒ 814
```

で設定をします。

また、`pm.min_spare_servers`はコア数×2、
`pm.start_servers`、`pm.max_spare_servers`はコア数×4、
`pm.max_requests`はApacheの`MaxConnectionsPerChild`と同様で、同じ値になるように設定します。

---

最終的に、PHP-FPMは下記の設定となりました。

```conf:/etc/php-fpm.d/www.conf
pm.max_children = 814
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 4
pm.max_requests = 300
```

### 設定を反映
再起動して設定を反映します。

```bash
$ service httpd restart
Stopping httpd:                                      [  OK  ]
Starting httpd:                                      [  OK  ]
$ service php-fpm-7.0 restart
Stopping php-fpm-7.0:                                [  OK  ]
Starting php-fpm-7.0:                                [  OK  ]
```

## 改善後のメモリ使用量を確認
上記の設定でしばらく稼働させ、`htop`コマンドでメモリ使用量を確認してみます。

![Image201908271805.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245411/8096fd7d-a4e8-923b-c228-5d818914d8c7.png)

改善前は2～3時間でメモリがいっぱいになって落ちてしまっていましたが、
5日経ってもメモリ使用量は30%以下をキープするようになりました！

---

今回はメモリに限ったチューニングでしたが、アプリケーション側のパフォーマンスが良くないのでそちらの改善を今後やっていきたいです。

サーバーのメモリが限られていてOut Of Memoryになってしまう方がいましたら、
簡単な計算で算出できますので、是非やってみてください。