はじめに
パフォーマンスやデータの整合性を担保した設計は勉強していても現場で経験して身につくところが多々あるかと思います。自分はミスを重ねて勉強しました。(キリッ
今回転職活動をするにあたってとある現場での3年ほどの経験を振り返り、学んだことや必須と思われる知識を列挙しました。
ここではLinux(CentOS)+Apache(2系)+MySQL(5.7系)+PHP(7系)を前提とした初心者向けの記事となっており、それぞれの項目を深掘りはしていません。
最初に陥りやすい問題
いざ大規模なLAMP案件に配属されてやってしまいがちな問題を列挙しました。
-
レスポンスが遅い。
アクセスが少ない場合は問題なかったりするが規模が大きくなると問題となることがある。(開発環境では問題とならなかったがリリース環境で問題となるなど。)最悪タイムアウトが頻発してアプリが起動できない、つながらないなどになる。DB周りがボトルネックとなっている場合が多い。 -
テーブルが巨大になってメンテナンス性が低い。
最初の設計が適切ではなく、カラムの追加などに1日メンテナンスを入れなくてはいけなくなり、HDの追加など物理的になんとかしなくてはいけなくなる。 -
データの整合性がとれなくなった。
トランザクションの処理を誤るとデータの不整合が起きる。ロールバックが難しく非常にまずい。(Aテーブルは更新したがBテーブルの更新に失敗したなど。) -
SQLインジェクションの危険性がある。
ユーザーの入力文字列を適切にエスケープできておらずそのままクエリに乗ってしまう場合など。最悪データを改ざん、破壊される危険がある。
抑えておきたい基礎知識
パフォーマンスを意識する
ネットワーク
-
物理的な距離が離れていないか。
グローバルコンテンツならばCDNはほぼ必須となる。
静的なデータはCDNを通して配信する。 -
サーバーリソースは足りているか。
CPU/メモリー/帯域などリソース面で限界となっていないか。
ただしリソース追加は最後の手段となり、まずは実装面で改善が見込めないか確認を行う。
ミドルウェア
-
PHPのバージョンは適切か。
PHPは5.6から7.0のバージョンアップで劇的に処理速度が向上しており、その後のマイナーバージョンアップでもパフォーマンスは上がっている。
またセキュリティの観点からもPHP5.6以前のバージョンはサポートはすでにサポートがすでに終了しているため5.6以前であれば早急にバージョンアップしたい。 -
アクセラレータは有効か。
PHPはインタプリタ型言語であるためアクセスごとにソースコードの解析を行い実行している。
OPCache等のアクセラレーターを入れておくことでコンパイル済みのコードをキャッシュすることができパフォーマンスが向上する。
実装
-
レスポンスが遅いAPIはないか調査する。
Apacheのアクセスログを見るなりツールを入れるなりで調査する。
1秒以上で相当遅いが起動時のみ呼ばれるAPIなどはしょうがないところも。 -
スロークエリは出ていないか
わかりやすくスロークエリが出ていれば出ないように潰していく。
EXPLAINで確認し、フルスキャンになっていないかなどチェックする。
参考:MySQLのIndexをはるコツ -
MySQLは1クエリにつき1つのインデックスしか使用できないので、どういった使われ方をするかでインデックスの張り方を考える。
-
インデックスも貼りすぎていると作成・更新・削除時に重くなり、またディスクも圧迫するため適切に張れるようにする。
-
カーディナリティの低いカラムにインデックスを張らない、できればユニークに。
-
プロファイラを用いてボトルネックをチェックする。
レスポンスが遅いAPIが判明したがどこがボトルネックになっているのかはっきりしない場合、プロファイラを用いればわかることがある。
XHProf,tidewaysやSaaSタイプのものなど、様々なツールがある。
参考:Profiling Tools For PHP7 -
転送データ量は可能な限り削減する。
当たり前だが無駄に重複するデータなどがないよう工夫する。 -
APIのコール数をでくるだけ少なくなるよう設計。
これも当たり前だが通信によるオーバーヘッドを減らすため同じところで複数回のAPIを呼ぶならばひとつに纏めたい。 -
同一リクエスト内で何度も呼ばれる計算結果は static にしておく。
そもそもの設計があれな場合もあるが同じ計算結果は static で保存しておくとステップ数が減らせる。
ただし途中で情報のアップデートがある場合はその限りではないので更新が反映されないなどのバグには注意。 -
共通で読み込む参照系クエリはキャッシュ(KVSなど)へ。
特に更新度が高くなく多くの人が参照するようなものはキャッシュへ保存しておく。
キャッシュは消失するデータであるためキャッシュサイズや損失するルールなど把握しておく。 -
DB更新時はロックに注意。
特にGvG系のイベントだと複数人が頻繁に同じレコードを更新することが想定されるため注意が必要となる。
トランザクション内でひとつのレコードに対して複数の更新がかかるとロック解除待ちが発生するが、それが頻発するとデッドロック状態となったり、レスポンスが遅いなどの問題となる。
トランザクションは可能な限り短く、またデッドロックが発生しないような実装を考慮する。
参考:
14.2.11 デッドロックの対処方法
デッドロックおじさん戦記
InnoDBで行ロック/テーブルロックになる条件を調べた #mysqlcasual Advent Calendar 2013
- マスターデータなどはJOINしたほうが速い。
例えばキャラクターのユーザーデータを取ってきてマスターデータをマージするケースでは2回クエリーを投げるよりもJOINして1回で済ませたほうが速い。
セキュリティ
- 予期せぬクエリが発行されないように気をつける。
PDOのプレースホルダを使用してクエリを生成する。
エスケープ処理を適切に行っていないとデータの改竄や破壊の危険性がある。
参考:
PHPでデータベースに接続するときのまとめ
安全なウェブサイトの作り方 (ページ内「安全なSQLの呼び出し方」を参照)
テーブル設計時の注意点など
-
巨大なテーブルは分割する。
パフォーマンスの観点からやらないで済むのであればしないほうがいい。
例えばひとつのテーブルにまとめるとカラム追加があった場合など非常に時間がかかるため垂直分割を、レコード数が多くなることが想定されるならば水平分割を検討する。
カラム数が多くなりすぎる場合やスナップショット的な状態を保存しておく場合など、オブジェクトとして圧縮しひとつのカラムに格納するのも場合によってはあり。
参考:ソーシャルゲーム案件におけるDB分割のPHP実装 -
パーティショニングを使う。
ログ系のテーブルなどはパーティションを切っておくとメンテナンス時にドロップしやすくなる。
同じパーティションでの参照であればパフォーマンス面でも期待できる。
参考:第52回 MySQLのパーティショニング機能 -
テーブル圧縮を使う。
パフォーマンス面で不安がある場合は圧縮テーブルと非圧縮テーブルのダブルバッファー状態にして検証するなどするといいかも。
参考:【MySQL】肥大化したInnoDBテーブルを圧縮機能で縮小する方法! -
仕様で工夫する。
ひとりあたりのレコード数を制限する、保存できる期間を指定するなど緩やかな増加となるよう仕様面でも工夫する。