PHPサーバーサイド速度改善のアプローチ

こんにちは、17新卒エンジニアのジャンハンです。  
現在はDMM電子書籍サービスの開発部門で改修、保守の業務を担当しています。  
サーバーサイド（PHP）がメインで、フロントの実装も度々あったりしまして、たまーーーにデプロイツールの実装や環境周りの業務も担当しています。

本日は[DMM.com #2 Advent Calendar 2017](https://qiita.com/advent-calendar/2017/dmm2)の6日目を担当させていただき、新米エンジニアが電子書籍サービスの速度改善タスクを担当した時のアプローチについて文章にまとめました。

カレンダーのURLはコチラ
[DMM.com #1 Advent Calendar 2017](https://qiita.com/advent-calendar/2017/dmm)
[DMM.com #2 Advent Calendar 2017](https://qiita.com/advent-calendar/2017/dmm2)

## 速度改善の指標
webサイトのアクセス速度に影響する要素は、css、jsの圧縮、画像の圧縮、初期レンダリング情報量、画像サーバーの返答速度等がありますが、今回の担当はメインサーバーの返答速度なので、サーバーサイドの処理時間を改善指標とします。サーバーサイドの処理時間はTTFB（Time To First Byte:以後TTFB）の値を指標にしました。

### TTFBとは
サーバーがリクエストを受け取ってからデータを返し始めるまでの時間のことです。

### TTFBの確認方法
chrome、safari等のブラウザの開発ツールを使用して確認できます。下図はchromeで確認した例です。「newwork」というタブを選択して、リスト一覧の最上部にある要素にカーソルをあわせるとTTFBを確認できます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/113339/8e43832f-f02b-2dae-378a-6602de8ce4a8.png)

## サーバーサイド応答速度改善
Googleが推奨するTTFB時間は200ms以下（きびしいー）でした。そこまでは道が遠いですが、ひとまずサーバーサイドの応答速度を影響する要因を考えてみました。主に以下3つが考えられます。

- 速度の遅いアプリケーションロジック
    - ロジックが最適化されてない、重複処理が多い
    - `array_push`、`array_search`、`is_null`等の処理速度の遅いメソッドが多い

> 処理速度の遅いPHPメソッドについては[こちら](https://thk.kanzae.net/net/itc/t2379/)を参考してください

- 速度の遅いクエリ
    - 必要のないデータを取得している(`selcet*`を多用)
    - indexが適用されていないクエリがある
    - データ件数分のクエリが実行されている（N+1問題）

- キャッシュの利用が不十分

しかし、実際どれが原因なのかを考える時ボトルネックを探す必要があります。腕のいいエンジニアはソースコードをちらっと見みたらボトルネックが分かるかもしれませんが、自分はそんな超能力を持ってないのでボトルネックを調査するため、blackfireというプロファイリングツールを使用しました。

### blackfireとは
blackfireはPHPフレームワークのSymfonyの開発元となるSensioLabsが開発した、PHPに特化したプロファイラーです。サーバーに必要なツールをインストールするだけで、リクエストを受けた際、どのメソッドを通り、各メソッドがどれくらい時間がかかり、どのぐらいメモリを消費している等を視覚化してくます。洗練されたユーザーインターフェイスが実装されていますので、スムーズにボトルネックを探せます。

[有料プラン](https://blackfire.io/pricing)が提供されていますが、ボトルネック調査だけなら無料プランでも十分使えます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/113339/ec861144-ff5c-931d-b55a-1fb01b565336.png)

### blackfireの初期設定
blackfireを使用するにあたて、まずは[blackfire.io](https://blackfire.io/)にて登録する必要があります。
以下は公式ページからCentOSで利用する場合の初期設定を抜粋したものです。他のOSは[公式ページ](https://blackfire.io/docs/introduction)を参考してください。

#### 1. pygpgmeパッケージとblackfireリポジトリーのインストール

```
sudo yum install pygpgme
wget -O - "http://packages.blackfire.io/fedora/blackfire.repo" | sudo tee /etc/yum.repos.d/blackfire.repo
```  

#### 2. エージェントのインストール

```
sudo yum install blackfire-agent
sudo blackfire-agent -register
```

これらのコマンドを実行すると、ServerIDやServerToken等の情報が聞かれると思いますが、これらの情報は[アカウント情報](https://blackfire.io/account)で確認できます。
その後、以下コマンドでエージェントを再起動します。

```
sudo /etc/init.d/blackfire-agent restart
```

#### 3. blackfire CLI toolのインストールと起動

```
sudo yum install blackfire-agent
blackfire config
```

ここでまたClient IDとClient Tokenが聞かれます。これらの情報も[アカウント情報](https://blackfire.io/account)で確認してください

#### 4. PHP Probeをインストール

```
sudo yum install blackfire-php
```

ここで、サーバーサイドのインストール系が完了します。webサーバーを再起動してからblackfireが利用可能になります。  
※ PHP ProbeはXHProf、IonCube、Pinba、Suhosinと競合するため、Probeを有効にするにはそれらのツールを無効化するか、アンインストールする必要があります。

#### 5. Google Chromeの拡張機能をインストールする  
以上でコマンドラインでプロファイリングができるようになりましたが、blackfireの素晴らしいインタフェースを使用するため、Chromeの拡張機能のをインストールする必要があります。インストールのリンクは[こちら](https://chrome.google.com/webstore/detail/blackfire-companion/miefikpgahefdbcgoiicnmpbeeomffld)になります。  

以上揃えるとblackfireを使用して、ボトルネック調査ができるようになります。

### blackfireの使い方と見方

blackfireの使い方はとても簡単です。初期設定を完了したサイトにアクセスして、下図のようにChromeの右上の拡張一覧からblackfireのアイコンをクリックし、「Profile！」をクリックするだけです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/113339/c6e4aa3d-72f3-fe03-ca4d-6c2093fa259b.png)

プロファイル完了したら、blackfire.ioコンソールページで該当計測結果をアクセスすると、以下のようなインタフェースが表示されます。
右側のグラフで、ボトルネックとなるメソッドが一目瞭然になります。左側のリストをクリックすることで、メソッドの処理時間の詳細、メモリ使用量等も確認できます。

また、最も左側のタブ「Recommendations」を選択すると、速度改善のレコメンドもあります。これでボトルネックとなるメソッドを探し、そのメソッドの中でさっきほど述べた「サーバーサイドの応答速度を影響する要因」を探し、一つずつ解決すればいいと思います。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/113339/da4c6242-57a0-3314-992b-0206362eb1fb.png)

## まとめ
まとめとして、サーバーサイドが遅い原因は以下が考えられます。

- 速度の遅いアプリケーションロジック
    - ロジックが最適化されてない、重複処理が多い
    - `array_push`、`array_search`、`is_null`等の処理速度の遅いメッソドが多い

- 速度の遅いクエリ
    - 必要のないデータを取得している(`selcet*`を多用)
    - indexが適用されていないクエリがある
    - データ件数分のクエリが実行されている（N+1問題）

- キャッシュの利用が不十分

blackfireを使用してボトルネックとなるメソッドを探し、上記の「サーバーサイドの応答速度を影響する要因」を一つ一つ解決してみました。結果として、対象ページのサーバーサイドの応答速度を30%程度削減できました。

phpサーバーサイドの速度改善を考えている人には、個人的にこのアプローチがおすすめです。よかったら試してみてください。
ここまで見て頂きありがとうございました。明日のDMM.com Advent Calendarは[@wshino](https://qiita.com/wshino)さんが担当します。


## 参考した記事
[PHP 高速化に関するメモ書き](https://thk.kanzae.net/net/itc/t2379/)
[サイトを37倍に高速化した7つの手法](http://allabout-tech.hatenablog.com/entry/2016/09/28/094600)
[あなたのサイトは大丈夫？簡単手軽にサーバーのパフォーマンスを計測する方法](http://www.aiship.jp/knowhow/archives/20507)
[PHPプロファイラーのblackfireを使う](https://qiita.com/bezeklik/items/926f73cccc5ddd452a06)
[高速なWebサーバアプリケーションを構築するための6つの経験則](http://postd.cc/6-rules-of-thumb-to-build-blazing-fast-web-server-applications/)