TOTEC2014 インフラチューニング(チューニンガソン)で優勝したはなし | サイバーエージェント 公式エンジニアブログ

インフラ&コアテク本部の仲山です。 「TOTEC2014 インフラチューニング」という社内チューニンガソンイベントで優勝をいただいたので、 技術ブログを書くことになりました。

TOTECは、サイバーエージェントグループ内の技術者コンテストで、 インフラ、フロントエンド、サーバサイドの分野ごとに、 「チューニンガソン」と呼ばれる形式でその速度向上を競い合います。

今回の「インフラチューニング」では、 参加者はソフトウェアのソースコードを改変できないため、 あくまでミドルウェア等の変更、チューニングや、サーバ構成の最適化のみで闘います。

主なレギュレーション

  • 運営があらかじめ用意したMediaWikiの応答速度を競う。
  • ソースコードの変更は禁止だが、設定ファイルの編集は可能。
  • 運営の計測サーバから、複数のURLに対してリクエストが送られ、その応答時間を合算したものがスコアとなる
  • レンダリング等は含まず、ページへのHTTPリクエストのみ
  • 起動状態のサーバ4台が渡されるので組み合わせは自由
  • MySQLのSQLダンプを食えるデータストアを使わなくてはいけない
  • DBのデータはディスクへの永続化がされなくてはいけない
  • データの更新結果が5分以内に反映されなくてはいけない
  • 終了直後にサーバを再起動する

最終的な構成

こちらが最終的なサーバ構成です。
なお、終了時に最終構成を手元にコピーし忘れたので、メモ書きだよりです。

最終サーバ構成

この記事は、この構成に至るまでの試行錯誤を淡々と描く物です。
過度な期待はしないでください。

方針だて

以下のような方針で進めることにします。

「MediaWikiがそんなにチューニングされていないわけがない」

一般的なチューニングテクニックはMediaWikiの開発サイドで一通り試されているはずで、 DBクエリとかも、せこい「ごまかし」的なやり方しか余地はなさそう。 なので、あまり一つの場所にこだわりすぎないようにする。 あと、色々試した先人の知恵がごろごろ転がっているに違いないのでそれを探し回る。

「前半は1サーバで素直なPHP環境でのチューニングを進める」

基本的なチューニング結果が最終的なスコアを底上げするはずなので、 Varnishの導入は後半になってからおこなう。 ボトルネック箇所が変わる可能性があるので、 サーバ分散させるのも最後になってからにする。

「割り切る」

細かくベンチ取って比較する時間は無いので、割り切って仮説ベースでざっくり進める。
時間が掛かる割に効果が薄そうなことは後回しにする。

[11:00] 開始

一番最初にやるべきは、初期状態の確認、保存と性能監視です。 初期状態の保存として、/etc以下をまるっと複製したり、rpm -qa | sort を保存したりしておきます。

性能監視は、sysstatは既に入っていたので、それに加えてdstatとmuninを入れておきます。

また、渡された4台ともc3.largeであるのを確認しました。
(AWSだとインスタンス上から http://169.254.169.254/latest/ 以下を見に行くとメタデータが取れます。)

この時点で、AWS上のAmazon Linuxなので、OSのチューニングにも(余程時間が余らない限り)手を出さないことも決めました。

[11:30] nginx + php-fpm環境の構築

Apache+mod_php5環境が用意されていたという噂ですが、 全くガン無視して nginx + php-fpm 環境に差し替えました。

PHPは21世紀なのに標準では非スレッドセーフなので、 Apache+mod_php5環境だとprefork MPMで動かす必要があり、 最大接続数がPHPのプロセス数に縛られます。 nginx + php-fpmの構成であれば、 FastCGIで切り離されているためそのあたりの柔軟性を確保することができます。 この手のチューニングだと、こうやって「疎結合」を挟むことで、 構成変更を容易にすることが重要になります。

(補足) より正確に書くと、PHP本体はスレッドセーフですが、 PHP本体をマルチスレッドにしたいわけではないのと、 ZTSを有効にしてPHPのリビルドが必要になる割に得られる物は多くないためです。 Apache+mod_fcgid + php-fpmという選択肢もありますが、nginxの方が慣れているので……。
また、Apache+mod_php5のままでnginxを前に追加するというアプローチもありますが、 無駄が多いので候補に挙がりませんでした。

また、PHP 5.5 で性能が改善されたという噂も聞いていたので、 ついでにPHP自体もバージョンアップしておきます。 みんな大好きAPCのかわりに、PHP 5.5でZend Optimizer由来のOPcacheが本体に取り込まれたため、 OPcacheを有効にします。 また、タイムスタンプ確認系だとか、load_comments, save_commentsみたいな必要なさそうな設定は全部殺します。

[12:00] 独自にベンチマークしながら設定試行錯誤

dstatとかtopを眺めつつ。
この時点では完全にPHPプロセスがボトルネックであるようです。

運営の計測を待っていると永遠に時間が掛かる(うえに施策と計測で時差が出る)ので、 アクセスログに出る計測リクエストを眺めがら、自分でabで計測します。 この時点でこれぐらいの性能が出ていました。

# ab -k -c10 -n1000 'localhost/wiki/index.php?title=%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8'


Requests per second:    18.15 [#/sec] (mean)


# top
top - 12:12:24 up  3:59,  2 users,  load average: 3.31, 4.30, 2.72
Tasks:  89 total,   2 running,  87 sleeping,   0 stopped,   0 zombie
Cpu(s): 40.0%us,  1.5%sy,  0.0%ni, 57.3%id,  1.0%wa,  0.0%hi,  0.0%si,  0.2%st
Mem:   3859076k total,  1750232k used,  2108844k free,    94156k buffers
Swap:        0k total,        0k used,        0k free,  1279920k cached


  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
 2652 mysql     20   0 1335m  99m 6184 S 13.0  2.7   7:12.04 mysqld
25668 nginx     20   0  463m  35m  18m S  9.3  0.9   0:03.38 php-fpm-5.5
25665 nginx     20   0  459m  31m  18m S  9.0  0.8   0:03.37 php-fpm-5.5
25670 nginx     20   0  457m  29m  17m S  9.0  0.8   0:03.54 php-fpm-5.5
25667 nginx     20   0  459m  31m  17m S  8.0  0.8   0:03.07 php-fpm-5.5
25669 nginx     20   0  459m  31m  18m S  8.0  0.8   0:03.24 php-fpm-5.5
25662 nginx     20   0  457m  29m  17m S  7.7  0.8   0:03.24 php-fpm-5.5
25663 nginx     20   0  457m  29m  18m S  6.7  0.8   0:03.22 php-fpm-5.5
25664 nginx     20   0  461m  33m  18m S  6.0  0.9   0:03.03 php-fpm-5.5
25661 nginx     20   0  459m  31m  17m R  5.7  0.8   0:03.22 php-fpm-5.5
25666 nginx     20   0  457m  29m  17m S  3.7  0.8   0:03.01 php-fpm-5.5
25693 totec201  20   0 15140 1196  912 R  0.3  0.0   0:00.03 top
    1 root      20   0 19488 1596 1280 S  0.0  0.0   0:00.74 init

ただ、mysqldも無視できないぐらいには負荷があるようです。

MediaWikiの設定ファイルや公式ドキュメントを見ていると、 memcachedに対応しているようなので、導入しておきます。

おひるごはん。
サンドイッチ、ボリュームあっておいしかったです。

[13:00] DBのチューニング

どうやら、「版の多いページ」が重い(計測上はタイムアウト)ようで、 タイムアウトのペナルティを避けるべく、調査に入ります。 自分でabかけてSQL見てみてみると、GROUP BYでの集計クエリでどうやっても重そう。
EXPLAINすると、見事に「Using temporary; Using filesort」です。

とりあえずmy.cnfがほぼ空っぽなので、先人の知恵をパチってきます(kazeburo様ありがとうございます)。
https://github.com/kazeburo/mysetup/blob/master/mysql/my55.cnf
(参考: MySQLの設定ファイル my.cnf をgithubにて公開しました & チューニングポイントの紹介

これをベースに、永続化はしろと言われたが消えたら死ぬとまでは言われていないので innodb_flush_log_at_trx_commit=0 を設定したり、 今回だとデメリットが少なそうなクエリキャッシュの有効化などを入れます。 sort_buffer引き上げとかも試して見ましたがほとんど変化がなく、 最後のあがきで、サブクエリJOINの性能向上が入ってるらしいMySQL 5.6に入れ替えてみます、が、 これも効果が無く、やはり設計からどうにかしないとだめだがどうしようもないようです。

※ 実はここでMySQL 5.6向けにmy.cnfを修正するのを忘れていました。

プログラムの修正が禁止でもmysql-proxyを挟んでSQLを無理矢理書き換えることもできるのですが、 すぐにクエリの改善策が浮かばなかったので、一旦放置することにします。

[14:00] Varnish導入

ルーキー部門優勝者の岩永さんがスコアを一桁引き上げてインチキ説が出始めた頃ですが、 そろそろ素のPHP構成で一通り普通のことはやったので、 予定通りこちらもVarnishを導入します。

「mediawiki varnish」でググれば先頭にManual:Varnish cachingという公式マニュアルの記事が出てきます。秘伝のたれっぽいdefault.vclが出ているので、もうこのままぶっ込みます。 また、Varnish導入時に罠とされる、データ更新時のキャッシュ破棄もMediaWiki標準で対応しているようで、 願ったり叶ったりです。偉大なり先人の知恵。

この段階で、単純なスコアは一桁上がります。

[15:00] Varnishの調整

Varnishで重いURLを調査します。
共有メモリを使う独特なロギング形式ですが、 リクエストをテキストで保存するにはvarnishncsaを使います。

# vim /etc/sysconfig/varnishncsa:
DAEMON_OPTS="-a -w $logfile -D -P $pidfile -F '%h %l %u %t \"%r\" %s %b %D fb:%{Varnish:time_firstbyte}x hit:%{Varnish:hitmiss}x hdl:%x{Varnish:handling}x'"


# sort -n -k11  /var/log/varnish/varnishncsa.log | less

これを見ながら、Varnishのストレージをmallocに変更したりします。 このあたりで今回のピーク性能(スコア93064)がでていました。

そして、ここでMediaWiki側からのPURGEリクエスト設定を忘れていたので投入します。 これで残念ながらスコアが少し落ちます。 どうも今回の計測方式ではあまり反映されなかったようで、PURGE無しでも良かったかもしれません。

[16:00] サーバの分散化

当初の予定通り、サーバの分散を実施します。

もう完全にPHPがボトルネックなので、まずはnginx + php-fpmを3台に乗せ、 さらにMySQLを残り1台のサーバに分けます。 その状態でabとかを掛けてもDBサーバに余裕がありまくりんぐなので、 最終的には、4台ともnginx + php-fpmを動かすことにしました。 ただ、メモリが潤沢では無いし重いクエリは依然残っているので、 MySQLを同居させるサーバだけはphp-fpmのプロセス数を半分に絞って微調整しておきます。

※ 本来は、Varnishからの振り分け頻度も変えなければダメでした……。

ただ、色々触りつつも、「決め手」が見つからないまま17時を迎えます。

[17:00] ゴールに向けた準備

今回のチューニンガソンがいわゆる本番環境と違うところは、 あらゆる保守性を捨て去って良いところです。 少しでもスコアを稼ぐために、監視系の処理やログ保存処理をを片っ端から殺します。

  • munin
  • munin-node
  • varnishlog
  • varnishncsa
  • mysql slowlog

また、不要そうなPHP拡張も切っておきます。

[17:30] 再起動テスト

終了直後のサーバ再起動というレギュレーションなので、 実際に再起動してきちんと復帰するかをテストします。 で、予想通りchkconfig漏れがありました。

また、sendmailの起動に時間が掛かっていて(掘り下げていませんがおそらくDNSのタイムアウト待ち?)、 nginx等の起動が待たされていたので、これも割り切ってsendmail自体の自動起動を止めました。

ラストスパートです。
再起動直後は各種キャッシュが空っぽになるので、以下を /etc/rc.local に仕込んでおきます。

  • /var/lib/mysql 以下のファイルをcat hoge > /dev/null
  • sever1/wiki/index.php?なんちゃら へのHTTPリクエスト

[17:55] 静かな心で終了を待つ

下手にいじって壊すと嫌なので、再起動した状態で5分前から触るのをやめ、 念のためブラウザからの動作確認をクリッククリックだけします。

ここで諦めたので試合終了です。

[18:00] 試合終了と心残り

試合終了時点で、心残りが2点ありました。

「最後のボトルネック」

実は最後の時点ではサーバ1台の時よりもスコアが若干落ちていて、 サーバ性能を使い切れていない状況でした。 「どこかにボトルネックがある」のはわかるもののそれが特定できず、 通信がサーバ間になり通信遅延が増えた分だけ損をしている状況です。

時間内に解消できることを目指して4台構成のまま最後を迎えましたが、 結局はそのままゴールとなりました。

「残された不一致」

計測リクエストの結果が、本来あるべき内容と異なると「不一致」としてスコアに残るのですが、 contentPage1(~4)というURLで0.1%程度が不一致となっていました。

キャッシュのパージもできているのでほぼリアルタイムで反映されるはずで、 不一致となってしまうのは何か問題があるはずなのですが、 時間不足でそれを追うことができませんでした。

まとめ

最終的に、以下のようなチューニングを実施しました。

  • PHPの実行環境をmod_php5からphp-fpmに変更し、5.5にアップグレード、opcacheを有効化、心なしかスピード凶寄りの設定
  • MediaWiki標準設定を投入したVarnishを前段に投入
  • memcachedによるMediaWiki標準キャッシュの有効化
  • MySQLを5.6にアップグレードと、心なしかスピード凶寄りの設定
  • 監視等の全カット
  • 再起動後にキャッシュあたためます

反省点もいろいろあります。

  • 最後のボトルネック(前述)
  • 残された不一致(前述)
  • 前半のPHP/MySQLチューニングははもっとスピード感持ってやれたはず。
  • 計測リクエストの分析が甘かった。Varnishレベルでもっとズルができたはず。

結局はごく一般的なサーバチューニングを一通りやりきったという感じで 「決め手」には欠ける感じですが、実際には決め手がないのが普通でもあります。
普通のことをやりきった事で1位を頂くことができたというように考えています。

サーバチューニングって楽しいよね!