pixiv insideは移転しました! ≫ http://inside.pixiv.blog/

健全なpixivは健康なPHPに宿る〜モダンPHPを保つ7つの鍵

ピクシブ株式会社 Advent Calendar 2015の18日めの記事でございます。

qiita.com

こんにちは、うさみです。去年のAdventCalendarではEmacsにpixiv-novel-modeを作ったって記事を書きました。趣味はQiitaです

f:id:zonu_exe:20151218210301p:plain:w128,h128

現在pixiv.netではPHP 7に移行を進めていますが、それに先駆けて、この数年間に実施してきたpixivの改善について紹介いたします。

実際のところ私自身はあまり仕事をしてないので、この記事の内容のほとんどはほかのメンバーの業績チームの成果です p(ixi)v

最初に宣伝をしますが、技術評論社さまのWEB+DB PRESSにて、pixivの開発チームとして「PHP大規模開発入門」を連載しています。この記事でpixivのPHP開発に興味が湧きましたら、ぜひ購読していただければ ヾ(〃><)ノ゙☆

1. デプロイ機能の抜本的刷新

inside.pixiv.net

いわゆる「アトミックデプロイ」です。ここでは、HTTPリクエストを受け付けるときに「ファイル転送中」のような状態に陥ることがなく、常に必要なファイルが完全に揃った状態を保ったままデプロイができる環境のことです。

率直に言って、アトミックデプロイの導入は自分にとって革命的でした。基本的な動作としては「デプロイサーバーにgit checkoutして、各サーバーにAPする」という仕組みは変っていません。しかし、デプロイがアトミックになることでデプロイ動作中の数秒間に生じてしまっていたエラーを避けることができます。

これによってもたらされた変化は、ファイル名やクラス名の変更や分割を伴うリファクタリングが恐れるべきことではなくなったことです。気軽にリファクタリングを実施できない環境では、妥協によってソースコードの健康状態が悪化していきます。

以前はファイル名を変更するとincludeするファイルが消失してしまうタイミングが生じ、瞬間的にユーザーにエラー画面を見せてしまうことになるため、リファクタリングには及び腰でした。同様に依存ファイルを増やしたり減らしたりといったことにも障壁がありました^1

一方で現在はrsyncで全てのファイルをコピーが完了してからシンボリックリンクを切り替える方式です。そのため読み込むべきファイルの転送が完了してなくてエラー画面を見せてしまうといったストレスをユーザーに与えてしまう不本意な状況から脱却することができました。

なお、pixivのアーキテクチャは特にマイクロサービスではありませんが、一日のデプロイ回数はおよそ30回です。

2. リポジトリ統合

www.slideshare.net http://niconare.nicovideo.jp/watch/kn305

リポジトリ統合はデプロイ刷新と同時期に行われました。それまでは、たとえば「共通部分(DAO層などを含む)」「PC版サービス(www.pixiv.net)」「スマートフォン版サービス(touch.pixiv.net)」「モバイル版サービス(m.pixiv.net)」などのリポジトリに分かれていました。

当時開発プロセスでは、pixivのDBにアクセスする層は共有ライブラリのGitリポジトリにあるため、共通部分の開発と、git submoduleを利用して個別のサービスリポジトリを同期する作業がとても煩雑でした。

当時PHP・ニュービーだった私は、それら全てのプロジェクトの変更が平行して行われることで、git logがとてつもなく破滅的な事態に陥るのではないかと恐れていました。

ところが実際に統合してみると、git submoduleが排されることによるメリットは絶大でした。結合度の高いモジュール同士を無理やり疎結合にしようとサブモジュール分割したことで、開発プロセスにおいては無用なコストを払い続けていた状態だったのです。言ってみれば無用な複雑度が本来のあるべき水準まで戻っただけなのですが、私の人生の中でもこれほど開発が高速化した経験はありません。

また、リポジトリが統合されたことで、はじめは共通部分の変更コストと全サービスをチェックするコストが高騰することを恐れていました。しかし実際には全てのプロジェクトが一体化したことにより、以前ではリファクタリングを躊躇したような箇所にも大なたをふるうことができるようになり、後述するテストの拡充もあって各モジュールの疎結合化が促進されていきました。

3. ComposerとSatisの導入

デプロイ機能が刷新されたことで、満を持してComposerを導入しやすい環境になりました^2。しかし、デプロイを実行するごとにGitHubなどからファイルをダウンロードしてくると、単純に時間がかかります。また、トラブルが発生したときに何度もデプロイをするとGitHubのAPI Limitに到達する、といった現象が起こってはサービスの可用性に支障をきたします。

その問題は社内にSatisを導入することで解決することができました。定期的にJenkinsでパッケージの更新を行っているので、いつでもリアルタイムに近い状態でリポジトリの最新版を取得できます。新しいライブラリの導入にはsatis.jsonの追記が必要ですが、毎日増やすようなものでもないので現在のところボトルネックにはなっていません。

実際にComposerとSatisの導入作業に取り組んだのは私ではありませんが、「composer 導入をまじめに考える - Qiita」はComposerのアーキテクチャを理解する上で、とても助けになったバイブルのような記事です。

なお、composer.pharは現在のところGitリポジトリのルート直下に直接コミットする運用をとっていて、私がたまにself-updateをかけています。

composer.jsonについて

前述したリポジトリ統合時には、それぞれのサービスのディレクトリごとにcomposer.jsonを持っていました。現在は、純粋なデプロイ時のパフォーマンス問題(つまり、セットアップ時のcomposer installの回数を減らせば単純に早くなってデプロイ時間が短縮できる)により一つにまとめています。大雑把な運用ではありますが、それによってライブラリの細かいバージョンアップの手間は減ったので、悪くはないかなと感じてます。

4. StaticMockによるテスト

inside.pixiv.net

pixivにおける「継続的なテスト^3」の歴史は意外と浅く、pixiv共通ライブラリにおけるgit logで辿れる最初のPHPUnitのテストファイルは、2012年9月にコミットされた、たったの6ファイルでした。それが現在では、pixivの共通処理部分のテストコードだけで737ファイル、合計で85124行の規模まで増やすことができました。残念ながらテストカバレッジとしては、まだそれほど高くはありませんが、テストコードを書く文化は着実に根付きました。

さて、そのテストを支える上での強い武器となったのがStaticMock(tototoshi/staticmock)です。これはPHPの実行時にメソッド定義を書き換えることができる拡張ライブラリRunkitのラッパーです。これをうまく活用することで、本番サービスでDIコンテナなどを利用せずとも、依存性を分離したテストを書くことができます^4

PHP7への移行につき、一時はStaticMockの存続が(私の中で)危ぶまれましたが、TysonAndre/runkit7の登場により、またしばらくは安定して動作させ続けることができそうです。

StaticMock以外の手法を使った既存コードのテスト戦術については、id:bash0c7WEB+DB Press Vol. 81に「テストコードのないアプリケーションの改修」として記事を掲載しました。

5. AppRunner

pixivはフレームワークなしのPHPですが、そこで困るのがエラー処理、特に例外の扱いです。

単にPC版、スマートフォン版といったサービスの中でもHTMLを出力するPHP、JSONを出力するPHPなどがあります。フレームワークのような制御構造になっていないPHPアプリケーションで例外を捕捉するには、逐一try-catchとエラー処理を書く、set_exception_handlerをセットするといった手法がありますが、pixivではAppRunnerという枠組みを作りました。

なんのことはなく、いわゆる「制御の反転」の一番シンプルなパターンの中でtry-catchして、例外の種類によって画面を出し分けようというだけです。

AppRunnerのサンプルコードをGistに置きました。単純なコードなので、このGistのソースコードのライセンスはCC0です。実際の運用では「サービス×レスポンスの種類」のAppRunnerクラスを定義して、それぞれのエラーハンドリングができるようにしています。

クールなエラー画面

filp.github.io

上記のAppRunnerのサンプルコードにも含まれてるのですが、Woopsはカッコイイエラー画面を出力するためのイカしたツールです。

Before

f:id:zonu_exe:20151218204128p:plain

非常に「よくある」、リクエストと例外のバックトレースをテキストで出力した画面です。

After

f:id:zonu_exe:20151218204142p:plain

(今回の例は簡単すぎるのでメリットがわかりにくいですが)テキストベースのエラー画面ではトレースを追って問題を把握するのに手間が掛かりました。Woopsを導入することで、クリックだけで原因まで容易に辿りつけるようになりました。

6. 型安全でSQLインジェクション耐性のあるSQLラッパー

PHPにはPDO(PHP Data Objects)という、データベースの抽象化層があります。pixivはこれを更にラップしたクラスを使ってますが、基本的にはとても便利な機能です。しかし、型を判断して適切に値をバインドするのが難しいといった問題があります。

今年のPHPカンファレンス2015で徳丸浩先生が今どきのSQLインジェクションの話題総まとめという題でセッション発表されました。この中で「入門書にありがちなSQLインジェクション対策」がまとめられていますが、社内で開発されたPxvSqlはまさにこのような問題意識を解決するソリューションです。

そのような問題の対処には、たとえばDoctrineのような強力なO/Rマッパーを利用する手法も考えられます。しかしpixivには既に多くのSQLクエリがあるため、可能な限りそれを活かす形でPxvSqlを開発し、本番環境で安定して動作しています。

こちらに関してはGitHubで一般公開できるように現在準備を進めて居りますので、しばしお待ちください。

7. pixivレビュアー会・リファクタリング祭

pixiv開発に携わる人員は日々増えて居ります。組織として、コードベースの方針の軌を一にするために今年度からpixivを開発する各チームからコードレビューを担当するメンバーが集まって、毎週30分「レビュアー会」を行っています。

レビュアー会では、各メンバーがコードレビューをしていて気になった点、直近で導入したライブラリ、脆弱性を避けるためのノウハウ、コーディング規約の委細の確認、開発中の機能の設計の相談など、さまざまな話題が共有されています。

また、定期的にpixivの各チームの開発メンバーを横断的に招集して行うイベント「リファクタリング祭」があります。この日は通常の開発タスクから離れて、半日程度pixivのコードヘルスを保つための改善に取り組みます。

まとめ

pixivではPHPと正面から向き合って、サービス開発と同時に日々コードの健康を保つための活動を進めています。テストなどに関しては現状で不足している面もあり、機能開発とのバランスを保ちながら既存コードのテストを追加・メンテナンスしています。

私たちはコミュニティから多くの知見を得ているので、個人としても会社としても成果を還元していきます。

そんなpixivでの本気のPHP開発に興味のある方は、ぜひ一度オフィスに遊びに来てください ヾ(〃><)ノ゙

recruit.pixiv.net

明日の担当は敬愛するJSハッカーのgeta6です。


ステマ

冒頭でも紹介いたしましたが、技術評論社さまのWEB+DB PRESSでpixivの開発チームとして「PHP大規模開発入門」を連載しています。

  1. Vol. 80 モダンな開発環境を構築! ……パッケージ管理,ビルトインサーバ,デバッグ,テスト
  2. Vol. 81 テストコードのないアプリケーションの改修
  3. Vol. 82 安全なコードの書き方 ……エラーと例外処理の活用,PhpStormによる効率化,健全なチーム作り
  4. Vol. 83 PHP 5.5/5.6入門 ……新機能の紹介とアップグレードの注意点
  5. Vol. 84 高速な開発サイクルのためのデプロイ ……巨大Gitリポジトリ運用,Composerでのライブラリ管理,ゼロダウンタイムリリース
  6. Vol. 85 本番環境での不具合の発見と修正 ……例外やエラーの収集・可視化・通知,phpdbgによるデバッグ
  7. Vol. 86 PHPによる画像処理 ……Imagickの使い方,最適化,非同期処理,動的サムネイル生成
  8. Vol. 87 PHPDocでコードの品質を保つ ……チームでの仕様共有,IDEによる入力補完
  9. Vol. 88 HHVMでPHPの実行速度を高速化しよう ……インストール,設定,速度比較,運用監視
  10. Vol. 89 Thriftで実現するマイクロサービス ……言語間通信APIの作成,ドキュメント自動生成,活用事例