ISUCON7予選敗者が挑むWordPressパフォーマンス・チューニング！

## はじめに
[GMOペパボ Advent Calendar 2017](https://qiita.com/advent-calendar/2017/pepabo)の9日目の記事です。

2017/10/21(土) ISUCON 2017 予選に参加しました。[結果は大敗・・・](http://blog.hypermkt.jp/isucon7/)。前回よりも点数が下がり精神的ダメージは大きかったです。今回はインフラ要素が強い課題となっており、Nginx + PHP-FPMの知識不足で力になれず悔しい思いをしました。

そこで今回はその雪辱を晴らすべく、Nginx + PHP-FPM 7.1 + MySQL 5.7で構築したサーバーで[自分のブログ](http://blog.hypermkt.jp/)をパフォーマンス・チューニングし、どこまで高速化できるか試したいと思います。

## お題
* [個人のWordPress](http://blog.hypermkt.jp/)を高速化する。目安としてレスポンスタイムとリクエスト毎秒を改善する

## ルール
* ISUCONのような特定計算ツールはないので、Gatlingを利用して負荷測定を行う
* 低スペックサーバーを1台を使用。Conohaの１コア/512MB 630円/月プラン。
* Nginx + PHP-FPM 7.1 + MySQL 5.7をインストールし、設定ファイルは基本未修正とするが、最低限WordPressが表示できるようにNginxの調整をする
* 個人の話なので好きにやってよし。

### インフラ環境

* サーバー: Conoha 東京リージョン 512Mプラン x 1
* OS: Ubuntu 16.04.3 LTS (Xenial Xerus)
* Nginx 1.10.3 インストール済み＆初期設定状態
* PHP-FPM 7.1 インストール済み＆初期設定状態
* MySQL Community Server インストール済み＆初期設定状態

ということで、やったるぞー！

## 初期状態で計測

平均リクエスト毎秒は9req/sで、平均レスポンスタイムは244msという結果になった。

### 5req/s(30s)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29755/13f46921-9d65-7902-3023-97cc9429d701.png)

### 13req/s(30s)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29755/feeb2835-aadf-9e6c-a363-6bcef365a182.png)

## Let's パフォーマンス・チューニング！
パフォーマンス・チューニングして、秒間リクエスト数・平均応答時間をどこまで改善できるか試しましょう。チューニングに関しては思いつきで実施しているので順番がご容赦ください。

### PHP OPCache を有効
#### OPCache とは
[OPCache](http://php.net/manual/ja/book.opcache.php) とは、コンパイル済みのバイトコードを共有メモリに保存し、PHPがリクエストのたびにスクリプトを読み込み、パースをするのを省くことでパフォーンスを向上させます。

#### 設定方法
PHP-FPMで有効にするには、`/etc/php/7.1/fpm/conf.d/10-opcache.ini`に以下を追記することで有効になります。

```shell-session:/etc/php/7.1/fpm/conf.d/10-opcache.ini
opcache.enable_cli=1
```

#### 結果
期待するほど大きな改善にはなりませんでしたが、平均レスポンスタイムが10ms軽減されました。いいですね。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29755/3fab05a3-34e6-3ec9-b64f-b370ec0aeb45.png)

### Nginx で静的ファイルのキャッシュ

[PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights/)にブラウザキャッシュを活用してくださいと指示を受けたので、Nginxによる静的ファイルのキャッシュを有効にしました。

#### 設定方法
`/etc/nginx/sites-available/default` のserverディレクティブ内に以下を追加します。

```shell-session:/etc/nginx/sites-available/default
    location ~ .*\.(html?|jpe?g|gif|png|css|js|ico|swf|inc) {
        expires 7d;  # キャッシュ期間は7日
        access_log off;
    }
```

#### 結果
PageSpeed Insights の点数は上がりましたが、Gatlingの最小・平均レスポンスタイムは悪化・・・。うーん、なんでだろう・・・。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29755/fcadd87a-8535-b747-fd33-ae5d00bb2a4d.png)


### gzip圧縮の有効
サーバーとブラウザ間のネットワーク帯域を減らすことで高速化を図ります。今回はサーバー側でgzip圧縮をする方式をとるので、CPU負荷があがる欠点もあります。

#### 設定方法
`/etc/nginx/nginx.conf` の `http` ディレクティブで以下を設定します。

```shell-session:/etc/nginx/nginx.conf
    ##
    # Gzip Settings
    ##

    # gzip 圧縮を有効にする
    gzip on;
    # Internet Explorer6ではgzip圧縮が正常動作しないので除外する
    gzip_disable "msie6";

    # ヘッダー情報にVary情報を追記する
    gzip_vary on;
    # Proxy経由のリクエストでもgzip圧縮を有効にする
    gzip_proxied any;
    # １〜9段階ある圧縮レベルの6番目を使用
    gzip_comp_level 6;
    # gzipで使用するバッファーのサイズを設定
    gzip_buffers 16 8k;
    # gzipを使用するときのHTTPバージョンを設定
    gzip_http_version 1.1;
    # gzipの対象となるMIMEを設定
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
```

#### 結果

全体的に応答速度が改善した。地味に効果あり。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29755/3afc8230-6cb6-6b1c-197a-8890354451ae.png)


### Nginx FastCGIキャッシュの有効

#### FastCGIとは

ユーザーから要求がある度に、プロセスの生成と破棄が行われます。大量の要求があればその分だけプロセスの生成と破棄が実施され、このことがパフォーマンスの悪化に繋がっていました。

FastCGIとは、プロセスをメモリ上に永続的に起動させたままとすることで、その起動と終了にかかる時間を削減し、結果としてプログラム動作速度の向上およびサーバ負荷の低下が可能となります。

#### Nginx FastCGI とは

Nginxには Proxy Cache と FastCGI Cacheの２通りがあります。違うについては以下サイトで図解付きで分かりやすく説明されているのでご覧ください。

[Nginxを使ったもう一歩進んだWordPressチューニング \| cloudrop](http://cloudrop.jp/wordpress/more_tuning_on_nginx)

FactCGIキャッシュとはPHP-FPMの動的コンテンツをNginxでキャッシュし、PHP-FPMへのアクセスを減らすことでパフォーマンス改善を可能にします。

#### 設定方法

まずは `nginx.conf` にNginx全体のキャッシュ設定をします。

```shell-session:/etc/nginx/nginx.conf
    ##
    # Cache
    ##

    # キャッシング時のキーの設定
    fastcgi_cache_key "$scheme://$host$request_uri";

    # /var/cache/nginx キャッシュを保存するパス
    # keys_zone=ゾーン名とゾーンサイズ
    # levels=キャッシュディレクトリ配下の文字数(1階層目は1文字、2階層目は2文字という意味)
    # inactive=7日後にキャッシュが破棄される
    # max_size=キャッシュの最大サイズを設定。これを超えると使用頻度が低いデータから削除される
    fastcgi_cache_path /var/cache/nginx keys_zone=czone:10m levels=1:2 inactive=7d max_size=1000m;
```

変更点が多いので `diff` 表示としました。変更点のポイントとしては

* WordPress管理画面、RSSフィード、サイトマップなどはキャッシュしない。
* それ以外の処理はキャッシュする

```diff:/etc/nginx/sites-available/default
+    set $do_not_cache 0;
+    if ($request_method !~ ^(GET)$) {
+        set $do_not_cache 1;
+    }
+    if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml") {
+        set $do_not_cache 1;
+    }
+    if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
+        set $do_not_cache 1;
+    }

    location ~ .*\.(html?|jpe?g|gif|png|css|js|ico|swf|inc) {
        expires 7d;  # キャッシュ期間は7日
        access_log off;
    }

    location / {
        try_files $uri $uri/ @wordpress;
    }

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    location ~ \.php$ {
        #include snippets/fastcgi-php.conf;
        root           /var/www/html;

        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_script_name;
        include        fastcgi_params;
        fastcgi_pass unix:/run/php/php7.1-fpm.sock;

+        fastcgi_cache_bypass $do_not_cache;
+        fastcgi_no_cache $do_not_cache;
+        fastcgi_cache czone;
+        fastcgi_cache_valid 200 302 7d;
    }

    location @wordpress {
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  /var/www/html/index.php;
        fastcgi_param PATH_INFO $fastcgi_script_name;
        include        fastcgi_params;
        fastcgi_pass unix:/run/php/php7.1-fpm.sock;

+        fastcgi_cache_bypass $do_not_cache;
+        fastcgi_no_cache $do_not_cache;
+        fastcgi_cache czone;
+        fastcgi_cache_valid 200 302 7d;
    }
```

#### 結果

劇的に改善しました！平均レスポンスタイムも244msから81msと30%になりました。体感的にも早くなったことが感じられます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29755/aa398ac0-0ba2-0e3b-edb1-ed04ce91e9f4.png)

### PHP-FPM max_children数調整

### max_children とは

> pm.max_children - the maximum number of children that can be alive at the same time.

「同時に起動することができる子プロセスの最大値」という意味になります。またこの値は、pm の設定によって変化します。pm = dynamicの場合は、作成される子プロセス数になり、staticの場合は子プロセス数となる、パフォーマンスを上げるため固定値でプロセス数を20にしました。

#### 設定方法

```diff:/etc/php/7.1/fpm/pool.d/www.conf
-pm = dynamic
+pm = static

-pm.max_children = 5
+pm.max_children = 20
```

#### 結果

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29755/05dc098f-ae71-58c7-afac-cabc7b8dfae9.png)

### MySQL InnoDBバッファープール

NginxのFastCGIキャッシュに乗ってしまえば、DBアクセスはほぼないので効果は薄いかもしれませんが、念のため設定したいと思います。

#### InnoDBバッファープール とは

[MySQLのリファレンス](https://dev.mysql.com/doc/refman/5.6/ja/innodb-buffer-pool.html)には

> InnoDB は、データとインデックスをメモリーにキャッシュするためのバッファープールと呼ばれるストレージ領域を維持しています。InnoDB バッファープールの仕組みを知り、頻繁にアクセスされるデータをメモリーに維持するためにそれを利用することは、MySQL チューニングの重要な側面です。

とのことで、MySQLのパフォーマンスチューニングの重要な設定項目の一つで、メモリーの80%を割り当てるのが良いとされています。

#### 設定方法
512Mのメモリーのサーバーなので、 `/etc/mysql/mysql.conf.d/mysqld.cnf` に以下を設定しました。

```shell-session
innodb_buffer_pool_size=400MB
```

#### 結果

予想通り変化なし。これは今回のチューニングでは効果が現れないというだけで、基本的には設定すべきです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29755/3c25f8c7-4770-fee9-6c58-254571920663.png)

### PHP7.1 > 7.2 アップグレード

日本では2017/12/01 にPHP7.2がリリースされました。7.1から7.2にアップグレードすることで約10%ほどパフォーマンス改善した内容の[記事](https://symfony.fi/entry/php-7-1-vs-7-2-benchmarks-with-docker-and-symfony-flex)もありました。基本的にはアップグレードして悪化することはあまりないので、特別な理由がない限りアップグレードすべきです。

#### 結果

前回より応答速度が46%になりました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29755/dba719b3-8ea9-e38b-ce1a-1686f48ebc95.png)

### パフォーマンス・チューニングの結果

以下の結果になった。平均リクエスト毎秒は21倍に、平均レスポンスタイムは25%と高速化かつ高負荷耐性となった。

-|チューニング前|チューニング後
---|---|---
平均リクエスト毎秒|9req/s|192req/s
平均レスポンスタイム|244ms|61ms

#### 150 req/s(30s)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29755/3079cb5d-0873-63ec-4701-bc40240e1403.png)

#### 200 req/s(30s)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29755/4bcc1243-5a01-9da9-dd16-f9367705103e.png)

## おわりに

更新頻度の高くない個人ブログだったので、NginxのFastCGIキャッシュを活用することで大幅なパフォーマンス改善ができました。結果的にとても満足しています。しかしこれが更新頻度の高いアプリケーションだとこうはいかなかったでしょう。

さて、これで来年のISUCONに望めるか？というとまだまだです。ISUCON7の課題であったCDNの利用と複数台構成まではできませんでした。次回はそれら課題へ挑戦し、パフォーマンス・チューニング力を磨きたいと思います。

## 参考
* [hypermkt blog \| 平日はとあるホームページ作成サービス、週末はとあるレビューサイトの開発・運営。](http://blog.hypermkt.jp/)
* [サイトの最適化 ～ Vary: Accept\-Encoding ヘッダーを設定する \- 海外ホスティング入門](http://kaigai-hosting.com/opt_site-specify-vary-header.php)
* [nginxによるgzip圧縮について【技術メモ】 \- ふわっとしたやつ](http://t-suzuki.hatenablog.jp/entry/2017/03/24/190945)
* [Nginxでリバースプロキシとしてfastcgi cacheを使う](https://runble1.com/nginx-fastcgicache-setting/)
* [Nginxが重い？　NginxにおけるPHP\-FPMの設定 \| WordPress入門倶楽部](http://wp.nyumon.club/1468.html)
* [MySQLの「innodb\_buffer\_pool\_size」と「innodb\_log\_file\_size」の設定 – FlatLabs](http://blog.flatlabs.net/20100727_212649/)