PHP5.6 + CakePHP3 + Apache2.2のECサービスからAMP並のTTFBを実現するまで


TL;DR

  • 可能であればPHPは今すぐ7以降にバージョンアップしろ
  • ControllerばかりではなくTemplateのレンダリングコストも考える
  • AWS推奨がパフォーマンスとして最適とは限らない
  • インスタンスタイプはt2やm4だけではないという話
  • 俗に言うインタプリタ言語にコンパイルがないと思ってはいけない

速度遅くね?という指摘からすべてが始まった

弊社サービスあなたのマイスターは以下のようなアーキテクチャで構成されています。

今までも商品検索にクソDBで20秒かかっていたのをElasticsearchに切り替えてなんとかしのいだり、デフォルトのクソキャッシュエンジンをRedisに切り替えたりと順当な改善を行ってなんとか速度改善を行ってきました。

― が、2018年3月 ―

「ページ速度遅いよね!」

という恐れていた指摘が皆様(代表、役員、技術顧問、お手伝いしてくれている皆様、etc...)からどストレートに来るようになりました。

  • ちなみに改善前の速度はこんな感じ
    ページ TTFB(ms)
    トップページ 300ms
    検索結果画面 600ms
    商品詳細画面 1400ms

まあ...あれだ...入社から早1年、ずっとCakePHP+Apacheのアーキテクチャを捨てようと言ってきたんですよ。
※CakePHP+Apacheが悪いというのではなく、エンジニアに開発ノウハウがあまり無く、チューニングの勘所がわからないため

とはいえエンジニアが足りず、なんとかボトルネックになりがちなDBアクセス部分とかを必死にElasticsearchやRedisに移したりしてしのいでいたんですが...

もう逃げられない

まずはPHPのバージョンを5.6->7.2へ上げてみる

兎にも角にもバージョン上げると速くなる、と。
これはテストをやりながらもバージョン上げるっきゃ無い!ということでチャレンジしてみました。
基本的には深刻なエラーとかは出なかったものの、以下のパターンでちょいハマりました。

発生した問題

  • 変数呼び出しを動的にやっているところでエラーが出る
$this->loadModel('Users');
$modelName = 'Users';
$this->$modelName->find(); // ここでエラー

まあ、あんまりいいコードではないんですが、こんな感じで解決できました。

$this->loadModel('Users');
$modelName = 'Users';
$this->{$modelName}->find(); // {}でくくればOK
  • AWSのインスタンス上でPHP7.2がyumからインストールできない
    • AWSのEC2ではyumでインストールする場合にAmazonのリポジトリを参照するため、epelやremiを参照するようにインストール実行時にオプションを指定する必要がありました。
    • yum -y install --disablerepo=amzn-main --enablerepo=remi,remi-php72,epel

実施結果

  • 処理時間が全体的に10~30%短縮されました。

商品詳細ページを黒魔術で高速化する

商品詳細ページについては、以下のような理由からなるべくリアルタイムでDBから情報を取得するようにしていました。

  • 決済の前提となる情報(価格やスケジュールなど)が多いため、リアルタイム性が求められる
  • かなり多くのテーブルから情報を取得している上に、ページ数が多い割にヒット率が低いためキャッシュに格納しづらい

とは言うものの...1400msは遅すぎる...

DBアクセスも減らせないし、Indexとかそういう問題じゃないレベルで遅い...

しかもTemplateがえっらい巨大なので、そのレンダリングも結構遅い...

黒魔術でどうにかする

  • Contrrollerの処理+レンダリングの結果を全てRedisにキャッシュする
  • デバイスによる処理の切り分けも対応する
  • リアルタイム性の必要なものはAjax化してあとから描画
  • 以下の感じでgzipで圧縮してからRedisに格納
$httpResponse = $this->render('hoge');
$this->redis->set('cache', gzcompress($value, 6));
  • レンダリングするときは手動でHttpStatusやContent-typeを手動で設定した後にob_start()してヒットしたキャッシュをechoしてあげればOK

※実際にはもう少しいろいろな工夫をしているのでぜひオフィスに遊びに来てください。詳しくお伝えします。

実施結果

  • 処理時間が1400ms->90msとこの時点でAMP超えのTTFBを叩き出しました

EC2のアベイラビリティ―ゾーン(AZ)見直し

詳しくは以下の記事に書きましたが、Multi-AZからSingle-AZに変更しました。
WSでアベイラビリティゾーンをまたいでのアクセスが想像以上に遅かった話

実施結果

  • 平均の応答時間が30%短縮されました。

インスタンスタイプの最適化

チューニング前は2台のアプリケーションサーバは m4.large を使用していました。
オウンドメディアへのアクセスがかなり多いこと、記事に関連する事柄がTVで紹介されたりするとアクセスがかなり増えるため、CPUクレジットの枯渇に怯えないようにm4を選択していました。

が、メモリはスカスカなのでこれ以上インスタンスサイズを上げても処理速度が上がる見込みがない...


メモリの使用率的にC4でもいける!!!クロック数的に20%くらい性能アップするはず!!

お値段もほぼ変わらない(むしろc4のほうがほんのちょびっと安い)のでサクッとインスタンスタイプを変更しました。

実施結果

  • 処理時間が10%くらい短縮されました

OPCache導入

PHPはインタープリタ言語だからぁー、コンパイルとか無いしぃ―
そう思っていた時期が私にも有りました。
OPcache の最適化器の今
↑のスライドなどで詳しく解説されていますが、実際には処理をするたびに都度コンパイルしているわけです。
そこをキャッシュさせてあげることで、処理時間を減らす仕組みですね。

懸念点としては
コンパイル結果がキャッシュされ、キャッシュヒット時にはそのコンパイル結果を実行するため、ソースコードのデプロイがキャッシュの寿命によって即時反映されない場合がある
というものです。

こちらはデプロイ時に

  • Webサーバを再起動する
  • opcache_reset()を実行する

などで回避することができます。さくっとデプロイスクリプトに組み込んでしまいましょう。

実施結果

  • 全体的に処理時間が40%くらい短縮されました

まとめ

  • 改善結果
    ページ 改善前TTFB(ms) 改善後TTFB(ms)
    トップページ 300ms 80ms
    検索結果画面 600ms 160ms
    商品詳細画面 1400ms 50ms

沢山のトライアンドエラーを経て、かなり速度が上がりました。
概ねGoogleが推奨する200ms以下に収まっています。

フロントエンドは未だにjQueryが使用されている部分が多いため、現在弊社のフロントエンドエンジニアが鋭意改善中です。

今まではあまりインフラ周りにチャレンジしてきませんでしたが、かなり勉強になりました。

CakePHP+Apacheで巨大なDBがあっても工夫次第で実用充分な速度は出ます!
今後もユーザの満足度のためにがんばります!