Apache再起動後の初回リクエスト前からOPCacheキャッシュウォーミングを試みた話

@boscoworks です。
この記事は [2016年 Livesense Advent Calendar](http://qiita.com/advent-calendar/2016/livesense_1) 第16日目です。
きのうは @takemotto の「[AWSの請求情報をembulkを使用してTreasure Dataに転送し、Re:dashで可視化](http://qiita.com/takemotto/items/358bae5818365c1d67c3)」でした。
PHP5.5.0以降にバンドルされるようになったOPCacheで色々試行錯誤した話をします。


# うわっ…私のサイト、遅すぎ…？
~~このネタ古くて若い人は知らない人いるんじゃないだろうか~~
ウェブサイトを運営している人は、きっとレスポンス速度について日々思いを馳せていることと思います。
レスポンス速度が0.1秒遅くなるだけで売上が1%落ちるという逸話は古くから伝わる話です。
速さは命。速さは正義。 ~~じゃあそもそもPHPやめろよっていうのはナシだ~~

## なぜウェブサイトは遅くなるのか
色々理由はあると思うのですが、だいたいが

* データベースにスロークエリを投げている
* データベースにクエリ投げすぎ
* サーバ間 OR インターネットとの通信が遅すぎ
* サーバ間 OR インターネットとの通信しすぎ
* ファイルを読み書きしすぎ
* サーバのマシンスペックが低すぎ

に帰結すると思っています。まとめると

* データベースのクエリチューニング・インデックスチューニング・クエリキャッシュ
* 通信回数の抑制
* ファイルI/Oの抑制
* 適切なハードウェア・ミドルウェアに調整

をすることで、ほとんどが解決するんじゃないかと思います。
「塵も積もれば」的に遅くなったウェブサイトは、「塵も積もる」ような努力で高速化するのが一番よい、と。

## じゃあなぜ今回バイトコードキャッシュか
結論から言うと「PHPに対するこだわり」です。
だってレスポンス速度遅延の原因って上で書いたとおりPHPそのものでゴリ押しで速度向上なんてそうそうしないもの。
というわけで、もっともらしい前口上を台無しにしつつ、OPCacheについてアレコレやってみようと思います。
今更ですが、ここでは「バイトコードキャッシュってなに？」「OPCacheってなに？」については触れません。何年も前から良い記事がたくさんありますしね。パクリ記事よくない。


# OPCache を見える化する
まずは、

* どのファイルのキャッシュがあるのか
* どれくらいのヒット率なのか

を見える化します。
GUIについては既にいくつも出ているのですが、見た目の好みと導入のしやすさから「[amnuts/opcache-gui](https://github.com/amnuts/opcache-gui)」を選びました。


## amnuts/opcache-gui　の導入
ディレクトリ名などはお好みで調整してくださいね。

### git clone

```
cd /var/www/html
git clone https://github.com/amnuts/opcache-gui.git
```

### apache の設定追加
nginx の人は適当に読み替えていただければ。

```
vim /etc/httpd/conf.d/opcache-gui.conf
```

以下の感じで追記：
ServerName も適当に設定してください。

```
<VirtualHost *:80>
   ServerName opcache-gui.boscoworks:80
   DocumentRoot "/var/www/html/opcache-gui/"
</VirtualHost>
```

### apache リロード
nginx の人は適当に読み替えt(ry

```
service httpd configtest
service httpd reload
```

### アクセスしてみる

上のサンプルだと `http://opcache-gui.boscoworks/` とかになりますね。
もちろんサンプルなので 404 になります。
アクセスしてみましょう。

https://github.com/amnuts/opcache-gui#overview

こんな画面になるはず！

apache からのリクエストでPHPが実行される場合、 OPCache は apache の再起動のたびにリセットされちゃうので、初回アクセス時は全然ヒットしないですね。

![first_access.png](https://qiita-image-store.s3.amazonaws.com/0/62697/4b65fc43-0b39-f40f-94e7-52bac1f3c618.png "first_access.png")

ページをリロードしたりすると、OPCache に乗ったPHPコードが増えるので、 hit rate が増えるのがわかります。

![second_access.png](https://qiita-image-store.s3.amazonaws.com/0/62697/f286c3f6-d197-abeb-e2e5-250179c56f5c.png "second_access.png")


# 最初からバイトコードキャッシュをポッカポカにしておきたい
アプリケーションのデプロイや apache の再起動なんてのは、高速な開発を行っていれば頻繁に起きると思います。起きるよね？
上でも書いたとおり、 初回リクエスト時は当然キャッシュがきいてません。

それじゃあ自動的にバイトコードキャッシュを生成しておけば、常にキャッシュがポッカポカだね！ということで試してみました。

* http://php.net/manual/ja/function.opcache-is-script-cached.php
* http://php.net/manual/ja/function.opcache-compile-file.php


```cache_warmer.php
<?php

ini_set('memory_limit', -1); // 大量にコンパイルをすると一時的ではあれどものすごくメモリを食うらしい。だらしないけど横着した

// いつも思うけど glob は RECURSIVE にファイルを探しに行ければ最強よね
function phpFiles($dir) {
    $files = array();
    // log, test, config の名前の付いたディレクトリはキャッシュさせない。お好みで。
    if (preg_match('/log|test|config/', $dir)) {
        return $files;
    }
    foreach (glob($dir . '*/', GLOB_ONLYDIR) as $childDir) {
        $files = array_merge($files, phpFiles($childDir));
    }
    foreach (glob($dir . '*.php', GLOB_BRACE) as $file) {
        $files[] = $file;
    }
    return $files;
}

// キャッシュさせたいプログラムのあるディレクトリ
$dirs = array(
    '<プロダクトコードのディレクトリパス>',
    '/usr/lib/php/pear/'
);

$successNum = 0;
$failureNum = 0;
$noChangeNum = 0;
$totalNum = 0;
foreach ($dirs as $dir) {
    foreach(phpFiles($dir) as $file) {
        if (!opcache_is_script_cached($file)) {
            opcache_compile_file($file) ? $successNum++ : $failureNum++;
        }
        else {
            $noChangeNum++;
        }
    }
}
$totalNum = $successNum + $failureNum + $noChangeNum;

echo "success: ${successNum} \tfailure: ${failureNum} \tno_change: ${noChangeNum} \ttotal: ${totalNum}\n";
```

apache からアクセスしてあげないといけないので、 `cache_warmer.php` を `/var/www/html/opcache-gui/` とかに置いてあげて、

```
curl -X GET "http://opcache-gui.boscoworks/cache_warmer.php"
```

を実行します。

## 本当にポッカポカになったのか
手っ取り早く自分のところのプロダクトコードをgit cloneして、Webからアクセスしてみます。
そのあと opcache-gui にてグラフの変化を観察してみます。

![cache_warmed.png](https://qiita-image-store.s3.amazonaws.com/0/62697/5ff2418e-0924-1c99-058b-22e823c0d207.png "cache_warmed.png")

**hit rate: 3%**
嘘でしょ、と思ったのですが、どうも自分は hit rate を根本的に勘違いしていたようです。

![cache_detail.png](https://qiita-image-store.s3.amazonaws.com/0/62697/f0ca7b99-a107-e710-6211-aada984ab0df.png "cache_detail.png")

```
hit rate = (number of hits / number of cached files) * 100
```

ですね。そりゃ最初から大量のファイルがキャッシュされていれば率は必然的に低いわけです。
number of hits は apache がリクエストを受け付けてからの累積ヒット数のように見え（違ったらPHPのすごい人がきっとマサカリをなげてくれるだろう）、何ページか見てみると、number of cached files の増分 (フレームワークによってはテンプレートキャッシュとかを php プログラムで出力していたりしますよね。今回はそれが該当してるみたいです) に比して number of hits が大きくなり、結果 hit rate 上昇につながるわけです。

## requests per sec で本当に効き目があるのか検証
今回は**初回のレスポンス速度をあげたい話**なので、abコマンドは使いつつ、1回目のリクエストだけを比較するということをしました。

```ab_first.sh
#! /bin/bash

for count in {1..5}; do # 5回試行する。お好きな数だけ。
    echo trial: ${count} >> /tmp/ab.log
    ### OPCache が全くきいてない状態
    service httpd restart
    ab -n 1 -c 1 http://product.boscoworks/ | grep "Requests per second" | awk '{print $4}' >> /tmp/ab.log
    
    service httpd restart
    ### OPCache で予めコードを総舐めした状態
    curl -X GET "http://opcache-gui.boscoworks/cache_warmer.php"
    ab -n 1 -c 1 http://product.boscoworks/ | grep "Requests per second" | awk '{print $4}' >> /tmp/ab.log
done;
```

PHPプログラムをファイルキャッシュとして生成するフレームワークを使っている場合は、このスクリプトに適宜キャッシュクリアを入れましょう。意図しない結果になります。

| 試行 | 改善率 |
|:---|:---|
|1回目|125.93%↑|
|2回目|121.43%↑|
|3回目|117.86%↑|
|4回目|117.86%↑|
|5回目|113.79%↑|

若干ばらついてますが **10~20%は早くなっている** わけですね。


# まとめ
OPCacheは確かにウェブサイトを高速化していた！速さは正義！！


明日は @roana0229 です。お楽しみに！