12月15日、東京都大田区産業プラザPiOにて「PHPカンファレンス2018」が開催されました。本稿ではその模様をお届けしています。今回は、メルカリのDQNEOさんと、Cygamesの金山さんのセッションをレポートします。
DQNEOさん「大規模PHPプロジェクトでPHPUnitを3世代アップグレードするためにやったこと」
USメルカリアプリのバックエンドを担当するDQNEOさんは、メルカリの大規模なPHPプロジェクトでPHPUnitのバージョンをいかにしてアップデートしたか、その戦略と方法について話しました。
なぜアップデートしなければならないか
PHPUnitは、Webシステムのテストにも使えるUnitテストフレームワークです。有名なOSSプロジェクトでも採用されており、デファクトスタンダードとも言えます。メルカリでもPHPUnitを使っていて、今回のアップデートに着手した時点で、24万行のテストコードがありました。
アップデートをしなければいけないネガティブな理由としては、PHPUnitの古いバージョンのサポート切れや、新しいPHPのバージョンでテストが動かなくなってしまうのを防ぐという目的がありました。バージョン4, 5はすでにサポートされておらず、バージョン6のサポート期間も2019年2月1日までです。そのため、近いうちに7まで上げる必要が出てきます。また、PHP7.2の環境でPHPUnit4を使おうとするとDeprecatedエラーが出ます。これは将来のPHPで動かなくなる可能性を示唆しています。
ポジティブな理由としては、新しいPHPUnitのほうがより良い書き方をサポートしているという点を挙げました。名前空間、型宣言などモダンな機能を取り入れることができるようになり、可読性と拡張性が向上するのは良いことです。
アップデートの作業は、小さなプロジェクトであればcomposer.jsonを修正してcomposer updateを実行後、失敗するようになったテストを修正すれば終わります。
立ちはだかる大規模プロジェクトならではの難しさ
しかし、今回の対象となったプロジェクトはメルカリのバックエンドを支えるAPIで、関係する開発者が3カ国数十人もおり、1週間にPull requestが75本も生まれるような巨大プロジェクトでした。また、メルカリでは開発フローにGithub flowを採用しています。一般的な方法でアップデートをしようとすると、自分のアップデートをmasterブランチに取り込む際に、次のような問題が発生することは明らかでした。
- 他の開発者の手元の環境にあるテストが動かなくなる
- 並行して開発している機能で使おうとしているライブラリとの依存関係が解消できなくなる
- composer.lockがコンフリクトして手がつけられなくなる
さらに悪いことに、後述の事情からPHPUnitのバージョンアップが本番に影響する可能性もありました。
DQNEOさんは、1年半ごとに半導体の向上とアーキテクチャの変更を交互に繰り返す開発戦略、Intel Tick-Tockにヒントを得ました。テストコードの修正とPHPUnitの更新を交互に繰り返していくことで、安全にPHPUnitのバージョンアップできるのではないかということに気づきました。
この戦略を採用する際にポイントとなるのが「前方互換(forward compatibility)」です。バージョンアップ前のPHPUnitに依存した状態のまま、互換レイヤーを挟むことで前方互換を達成させつつ、PHPUnitのバージョンをアップデートしていくというものです。こうすることで、他の開発者がアップデート前のバージョンのテストコードを追加しても、アップデート時に動かなくなる問題を減らせます。
PHPUnitの変更が本番環境の動作に影響する可能性についてですが、実は一部の環境でのみ、--no-dev
オプションをつけずにcomposer install
しており、require-devされているパッケージの挙動が本番環境の挙動に影響するかもしれないことがわかったのです。幸い使われていることがなかったのですが、この影響調査では地道にすべてのクラスを確認しました。念のため、update前後のcomposer.lockを使ってvenderディレクトリに変更が発生しないことも確認しました。
この経験から、本番環境とQA環境では--no-dev
オプションをつけ、開発環境とCI環境ではオプションなしでcomposer install
を実行するのがよいという戦略に至りました。また、composer install
を実行した際は、そのコマンドをコミットメッセージとして残すことで、他の開発者も再現できるので良いというTipsも発見しました。
OSSへの還元
こうした経験をもとに最新のPHPUnitに追いつけていないOSSプロジェクト(DietCube、DiesCake、Monolog、PHPBench、AssertChain、Karen、Chronos、AWS SDK for PHP)に、Pull requestを送ったことを紹介しました。
DQNEOさんは「皆さんも困りごとやその解決方法をブログや勉強会で発表して、OSSに還元してください!」と述べ、今回のセッションを結びました。
金山啓子さん「Cygamesにおける長期運用タイトルのこれまでとこれから ~PHP7への道~」
株式会社Cygames サーバーサイドエンジニアの金山啓子さんは、ソーシャルゲームの運用について話しました。
今回取り上げるソーシャルゲームは運用4年以上の長期運営、現在のDAU(Daily Active User)は50万です。Webサーバは、Webサーバは、1サーバあたりリクエスト数1000-7000/分、構成はApache+PHP+MySQL+Memcachedというスタンダードなプロジェクトでした。
長期運用で仕様が複雑化、デバッグ工数の確保
ゲームを楽しんでもらうために、様々な仕様を追加してきました。その反面であまり使われなくなった機能も増えていきました。結果、テストが漏れる傾向が出てきたそうです。負荷対策では古いコードを見直すことが大変多く、実装者がすでにいない場合もります。考慮漏れやテスト不足、修正によるテスト漏れに気をつけて対応することが大切だということです。影響範囲を決めて対応し、うっかり手を伸ばさないことが重要だと話しました。
そして開発チームとデバッグチームとで深いコミュニケーションを取るようにしました。これにより、仕様の理解を細やかにしたテストができるようになりました。そして安定運用のために、「決めた場所だけを修正すること」「影響範囲は正確に出すこと」「無理な改修はしないこと」というルールを決めたとのことです。
PHP7.2への移行
CentOS6+PHP5.6から、CentOS7+PHP7.2へ移行したときを振り返りました。メンバーは3名で、期間は1ヶ月でした。また修正方針として、5.6でも7.2でもどちらでも動かせるように後方互換を保つようにしました。
まず互換性チェッカーで確認したところ、問題はそれほど出ませんでした。しかしながら実行してみると、すぐにエラーが発生しました。一部の関数の引数不足が指摘されたり、管理画面やデバッグ機能も含めると膨大な確認が必要でした。このままでは直せないので、静的解析を利用する方針に切り替えましたと言います。コードを実行せずに検証するツールで、Phanを利用しました。
まずフレームワークに対応させるために(静的実行なので)アノテーションを書いておく必要がありました。そして実行してみたところ、今度はPhanの実行結果が大量に出てしまう結果になりました(コメントブロックについてのレポートが大量に出てきてしまったとのことです)。そこで、まずは数を減らすためにPHP5.6からPHP7の変更点を見て、テストコードを作成したそうです。
テストコードの結果を目視で確認していきました。一例としてゼロ除算の結果をIntにキャストすると、結果が逆になってしまう状態が見られました。また、可変変数の評価順の変更に伴う問題、foreach内での内部ポインタ参照箇所などが見つかりました。
修正にはPHPStormのコメントやリテラル内を省いてくれる機能が便利だったそうです。方針として、機能別ではなく、まとめて対応をし、調査で問題になった未使用な関数は一旦削除するようにしました。不具合のうち、修正する必要がないものはその旨記載することで影響範囲を小さくました。WarningやNoticeの改修も、Phanの解析結果から効率よく処理できました。オーバーライドしたメソッドの引数が違うとWarningが発生したりしていました。
こうしてPHP7.2に移行した結果、レスポンスが3割ほど速くなりました。メモリ使用量も大幅に減少し、移行の効果は高かったことが分かりました。また、不具合がないことを証明する意識で対応にあたったのが、細分化して対応するより早く効率的に進められたとのことです。
負荷対策
元々は毎分4万リクエスト想定だったものが運用を続けている間に5倍ほどになってしまったとのことです。結果として遅延レスポンスが増加してしまいました。サーバーサイドとしてより安定した環境を作りたいので、負荷対策が重要と話しました。ツールとしてはNewRelicとXhprofを使って、負荷対策を進めました。
NewRelicはレスポンスを測るツールで、DBの部分、PHPの部分、キャッシュの部分それぞれを視覚化して調べられます。XhprofはPHPのプロファイラで、関数毎の処理時間を見ることができます。
改善すべきAPIは大きく分けて、遅いAPIと負荷が高いAPIの2種類でした。実行時間がかかってしまう応答が遅いAPIはNewRelicで、APIの一覧を平均処理時間が遅い順に表示して抽出しました。また、負荷が高いAPIは処理時間が早くても、合計応答時間が遅いものです。これはNewRericで、APIの一覧を合計応答時間の長い順に表示して抽出しました。
修正するAPIを決めたら、処理時間の内訳をNewRericで確認します。PHPなのか、Memcacheなのか、DBなのかの切り分けを行い、PHPだった場合にはXhprofでPHPの中身を確認していきました。例えば不自然に呼び出されすぎていないかを確認して、結果として1秒近くかかっている関数を発見しました(例として、スライドではarray_multisortが原因であることを示していました)。
また、ログイン時やクエストの処理が遅いことがあったそうです。ログイン時にフレンドやプレイヤー情報を表示していましたが、運営が続き、取得する情報が拡大していったためです。この対策としては散らばったデータをまとめ、ユーザー情報をキャッシュするようにしまいた。これによってアクティブユーザーのログイン体感が速くなり、結果としてログイン時の負荷が20%減ったそうです。
PHPの実行よりもMemcacheの時間のほうが長かったAPIもありました。これはキャッシュに積んだデータが大きすぎたことによるものです。元のデータはCSVでした。CSVだった経緯は開発上の手軽さのためでした。データの使用状況を再確認したところ、プライマリーキーで各行にアクセスしていましたが、取得したキャッシュの内の1割も利用していませんでした。この対策としては、CSV全体をそれぞれの連想配列の形でキャッシュに収めるのではなく、CSVの1レコードのカラムデータを文字列結合し、キャッシュに積むようにしました。また整合性を保つため、データを処理するオブジェクトも作成し、その中でパースできるようにしました。これにより、30%の速度アップに成功しました。
このようにして、全体のAPIが50ms前後になりました。
対策はチームで共有する
こうして対策した結果は、可視化することが重要だと指摘しました。対策している最中も図をチーム全体で共有し、負荷対策をチームで意識するようにしたそうです。そして効果測定を行い、対応したAPIの数値を見て目標値と比較し評価したと言います。また、負荷対策を行った知見は、すぐに情報交換するようにしたのも良かったと話しました。
最後に金山さんは「安定運用を意識するという心構えの部分と、ツールを使って客観的判断する部分が必要だ」と締めくくりました。