#サバフェス に参加してきました (後半戦)

サバフェスの後半戦について書きます。
サバフェスの前半戦に関してはこちらをご覧ください。

本大会は時間がたくさんあったため、いろいろ試すことができました。
最終的に使わなかったものも結構ありますが、せっかくやったことですし今後のためにも書き残しておきます。5日目以前に施した設定もあります。
レギュレーションはこちらをご覧ください。

ボツになった設定

Keepalived の設定は始め NAT で構成していました。この時点ではネットワークがボトルネックになることを想定していなかったからです。
今思えば、ISUCON3 での経験を活かすことができてなくてかなり残念な感じ。

今回、バックエンドのサーバ群の default gateway は LVSサーバではないので、static route の設定をする必要があります1
その場しのぎなら route コマンドで事足りるんですが、今回はサーバ再起動が行われるため設定ファイルも設置しました。

# route add -host <ベンチマークシステムのIP> gw <LVS のIP> dev eth0
# vim /etc/sysconfig/network-scripts/route-eth0
ADDRESS0=<ベンチマークシステムのIP>;
NETMASK0=255.255.255.255
GATEWAY0=<LVS のIP>;

しかし、この設定を施したあともGETのパフォーマンスは特に変化なし (8万くらいで伸び悩む)。
理由は、ネットワークがボトルネックになってしまっていたから。
今回の構成で Outbound のトラフィックを分散するにはDSR構成にしないとダメじゃん。。と気付き、涙ながらにこれらの設定をかき消すことに。

また、この段階では Weight Round Robin を使って DB サーバへのアクセスを減らしていたのですが、
Outbound の帯域を平均的に分散するために、重み無しの Round Robin にしました2

Nginx Lua Module

初めてNginx の設定ファイルに Lua を書きました。作戦は以下の通り。

  1. GET のレスポンスとして返すデータをRedis に入れておき、それをLuaで引っ張る。
  2. (自分の実力ではすぐ実装できなそうだけど)POSTのパラメタをパースしてPHPを経由しないでMySQL にINSERT、リダイレクトもやる

一つ目の作戦はすぐに実装できたのですが、レギュレーション満たしてるかなーと思い不安になって確認したところ、

ということを公式アカウントから言われたのでボツに。
Redisに入れたものをそのまま出し続けたら、ページ下部の【最近のコメント】の部分に相違が生まれてしまいます。

もちろん、定期的にRedisのValue を更新すれば (expireでも良い)のですが、
よくよく考えると、この努力はNginx のキャッシュ生成までのリクエストしか効果を発揮しないんですよね。そのためだけに実装するくらいなら、
もっと楽したい!そうだ、Nginx の cache expire を119 秒にして cron で毎分 curl すればキャッシュのない期間がほとんどなくなるからそれにしよう!
ということになり、やっぱりボツになりました。

POST に関しては実装を潔く諦めました。ここまでで6日目とかなんですけどボツの設定ばっかりしていて心傷つき打ちひしがれてしまい、元気がなかった。実装も難しそうでしたし「もうやだ (・へ・)」ということでやめました。

大会終了後にもやもやが残っていたのですが、まつけんさんが実装していました。流石です。
もやもやがスッキリしましたし、大変参考になりました。

MySQL Blackhole Engine

060301

今回の大会はPOSTの点数で大きく差が出ていました。POSTの最大のボトルネックはMySQL でした3
そのためBlackhole をフロントに置き、すぐさまレスポンスを返すことでスコアアップを目指しました。

POST で遅延していたのは、以下の2つでした。

  1. POSTデータのINSERT
  2. リダイレクトURL生成のために必要なコメントID を探すSELECT

ここでは、Blackhole のMySQL に対し、MyISAM のSlave をぶらさげるという方針。
この構成がきれば、POSTと GETで DBの参照先 (この大会ではPHPの参照先) を替える予定でした。
Slave 側ではINSERT しか降ってこないので、SELECT を処理しない分パフォーマンスが出るはず。ちなみにPOSTするたびにSELECTは7,8回くらい実行されてました。

レプリケーションはデータベース指定。

// my.cnf
replicate-do-db=wp_comments

Blackhole Engine への切り替え手順は以下の通り。

// Master 側
mysql> alter table wp_comments rename to wp_comments_saved;
mysql> create table wp_comments like wp_comments_saved;
mysql> alter table wp_comments engine=BlackHole;

// Slave 側 (Master での作業を実行した後に実施)
mysql> drop table wp_comments;
mysql> alter table wp_comments_saved rename to wp_comments;

しかし、ここで Blackhole のテーブルで AUTO INCREMENT の値が更新されないという問題が発生。これによって Slave では Primary Key 重複が生じ、レプリケーションエラーが起きました。
Blackhole Engine を使うのが今回が初めてだったということもあり、上記のような問題が起きるとは思いもしませんでした。

この問題が出たときに考えついたのか、MySQL Proxy。
Lua を用いて、INSERT文をキャッチし Slave で起こる問題を解決するという作戦です。
しかし、MySQL Proxy をダウンロードしようとしたところ stable 版が存在しないということに気づきました。
これは、本大会の禁則事項 「stable 版以外の使用」に引っかかってしまいます。そのため、Blackhole Engine 作戦はここで幕を閉じました。残念。

採用された設定

ネットワークの構成は下記の通り。前述した通り、DSR構成です。

Network Diagram(5)

これが最終的なミドルウェアの配置図。

Network Diagram(6)

DBサーバに PHP-FPM を入れると10000レコードくらいまでは、良いペースでINSERT してくれるんですが、
その後はCPUリソースを奪い合ってしまい、パフォーマンスが低下するのでやめました。

Keepalived

DSR構成にしました。設定は他の人と大差ないので省きます。
いつもと違うのはVRRP の機能を切ってIPVSの機能のみを使うように設定したところくらい。

vim /etc/sysconfig/keepalived
# KEEPALIVED_OPTIONS="-D";
KEEPALIVED_OPTIONS="-D -C";

Nginx

proxy_cache ではなく、fastcgi_cache を利用。
expireは 119秒。cronで 1分ごとに curl して cache を作る。 <- この実装、ナンセンスな気がしてならない (笑) curl のオプションには --compress をつけて、キャッシュを作りました。そのため、gzip on。 ベンチマークのクライアントは Accept-Encoding: gzip がなかったので、上記以外は gzip することはありません。 圧縮済みキャッシュを作っておくと、Accept-Encoding: gzip を要求してないクライアントに対してもそのキャッシュを返すと思ってたんですが、 ベンチマークを比較したところ、圧縮したキャッシュを返してるとは思えないスコアだったので、実は意味なかったのかも? キャッシュファイルのサイズも特に変化なかったし。このへんはまだよくわからない。 参考文献↓ [nginx] nginxでgzip圧縮とプロキシキャッシュを利用してる時に気づいたこと

それと、少しでも帯域を節約するためにngx_pagespeed モジュールを入れました。
これは、コメントアウト削除や静的コンテンツの圧縮などをしてくれるものです。あまり効果はありませんでした。

PHP-FPM

バージョンは5.5.6、remi- php55 リポジトリを利用して yum で入れました。

PHP-FPM の設定パラメタは以下のとおり。プロセス fork の負荷がもったいないので、start,min_spare, max_spare, max_children の値は揃えています。
プロセスを増やしてもコンテキストスイッチの回数が増えてしまうせいか、あまりパフォーマンスは上がらなかったので値は絞りました。
max_requests の値はテキトーです。

pm = static
pm.max_children = 8
pm.start_servers = 8
pm.min_spare_servers = 8
pm.max_spare_servers = 8
pm.max_requests = 500

また、中間コードキャッシュとして、Zend OPcacheとAPCu を入れました。
初めに入れたAPCu の効果は微妙でした。OPcache に入れ替えたところブレイクスルーしました。

MySQL

ストレージエンジンは、InnoDBではなくMyISAM にしました。
INSERT のパフォーマンスが欲しかったからです。実際、MyISAMの方がスコアがよかったです。
参考文献↓
InnoDB vs MyISAM パフォーマンス DELETEとINSERT

それと、POST 対策として wp_options, wp_comments テーブルに対して 2,3個 Index を貼りました。

まとめ

ネタで一瞬だけ1位にはなったものの、その後は 4〜6位くらいをさまよっている感じでした。12月中旬にならないとわからないんですが、最終的な順位もそれくらいかなーと思います。
20万円ほしかった!残念!

スクリーンショット 2013-11-26 9.03.13

振り返ってみると、コードをいじりたかったということとレギュレーションをもう少し明確に、そして緩くしてほしかったと思いました。
とはいえ、レギュレーションを守りながらいろいろ模索した結果、「知ってるけど使ったことない」ものをいろいろ試せたので、とても良い経験になりました。次は、チームで出たいかな。一人はキツかった。

PHP は遅いのでPHP 5.5 にしてZend OPcache を入れよう!

本大会を開催していただきました、IDCF の皆様、ありがとうございました。
参加者の方々もおつかれさまでした!


  1. なんでそこまでしてNAT構成にこだわったのか謎... []
  2. スコアが単純に5倍にはならず、8万 -> 13万 になっていたので Outbound には余裕があったはず。であれば、wrr でもよかったかも。 []
  3. 厳密には、レコード数が少ない間はPHP, レコード数が増えるにつれてMySQLという形。 []