仲間内でやっている勉強会の一環で、ISUCON の過去問に挑戦した。
今回は ISUCON 3 のオンライン予選問題。
公式で AMI が提供されている。
今回のルールは下記の通り
- 個人戦
- 立ち上げる AWS EC2 インスタンスは m3.xlarge, ストレージは 8GB マグネティック
- 競技時間は 6 時間
- マシンを再起動後にベンチマークを3回実行し、その最高スコアを結果とする(※)
- 上記以外は公式のレギュレーションに則る
※ PHP 実装でベンチマークを回すと再現性無く FAIL が発生したため、平均ではなく最高スコアとした
私の最終的なスコアは下記の通り。
Result: SUCCESS RawScore: 3772.6 Fails: 6 Score: 3433.1
ISUCON3 予選突破ラインが約 10000 とのことなのでまだまだ。
インフラ・ミドルウェア周りを疎かにしていることがはっきりと結果に反映された。
目次
- 目次
- やったこと
- 初期スコア測定・環境の準備など
- index (雰囲気)を貼ったつもりになる
- PHP のバージョンアップ(に失敗)
- memos 取得の order by 指定を created_at から id に変更
- last_accessed の更新をやめる
- PHP7.0 再挑戦(セッション保存先をファイルに変更)
- $older, $newer が見つかったらループを抜ける
- N+1 問題を回避
- セッションを保存先を Redis に変更
- view 内のループで preg_split を使っていたので explode に変更
- MySQL のクエリキャッシュ設定
- 改めて、index(雰囲気)
- マシンを再起動し動作確認
- 反省・感想など
- まとめ
やったこと
※施策ごとのスコアは記録を取っておらず、記憶に頼って書いているので不正確であることに留意
最終的なソースはこちらにアップしてある。
初期スコア測定・環境の準備など
Result: SUCCESS RawScore: 1864.2 Fails: 4 Score: 1845.5
先述のとおり、PHP実装では再現性なく FAIL が発生してしまう。(この結果も何回か回した上でやっと出た)
その次に作業環境として
あたりをやっておく。
index (雰囲気)を貼ったつもりになる
最初にざっくりテーブル定義を確認し、ソースコードを見渡した後に「雰囲気」で index を貼った。
create index memos_user_id_index on memos (user, is_private);
しかしデータ初期化の仕組みを理解しておらず、ベンチマーク時には index が削除されてしまっている状態だったため、スコアは変動せず。
原因に気づいたのは最後の方だった。
PHP のバージョンアップ(に失敗)
ISUCON3 実施当時は PHP 7 などなかったので少しズルい気がするが、使えるものはなんでも使おう。
ということでまず最初に PHP7.1 のインストールを試みる。
# yum remove php* # wget http://rpms.famillecollet.com/enterprise/remi-release-6.rpm # rpm -ivh remi-release-6.rpm # yum --enablerepo=remi-php71 --disablerepo=amzn-main install php php-mysql php-pecl-memcached
が、 apache2.4 まわりの依存性解決につまずいたので断念。
ここに時間を割きたくなかったので PHP 7.0 で妥協。
# yum remove php* # yum install php70 php70-mysqlnd php70-pecl-memcached
「今度はうまくいきそう!」とベンチマークを回すも今度は FAIL が多発してスコアが 0 に。
どうやらセッションが保存できていなさそうなことを確認。
いったん元のバージョンに戻す。
memos 取得の order by 指定を created_at
から id
に変更
色んな所で memos テーブルのレコードを取得する際に created_at
カラムで order by していたが、主キーであり auto increment である id
を使っても同じやろ!ということで変更。
結構な確信を持ってベンチーマークを回したが、かえってスコアは下がり 1000 を切るようになった。
泣く泣く戻す。
※原因は後ほど
last_accessed の更新をやめる
last_accessed を更新しない · okashoi/isucon3-practice@d33c500 · GitHub
ユーザがログインする都度 last_accessed
というカラムの日時を更新していたが、どこにも使われていなかったので更新をやめる。
これで RawScore が 100 ほど改善 (1900)。
PHP7.0 再挑戦(セッション保存先をファイルに変更)
memcached がうまく動かないのでいったん使わないようにしておく · okashoi/isucon3-practice@9648b61 · GitHub
セッションの保存に失敗しているようなので、いったん memcache ではなくファイルに保存するように変更。
これで動くようになった。
PHP7.0 にする前と比べて RawScore が 400 ほど改善 (2300)。
そこまで劇的には変わらなかった。
$older
, $newer
が見つかったらループを抜ける
と が見つかったら break · okashoi/isucon3-practice@080f712 · GitHub
メモ閲覧画面には、1つ前 ($older
)・1つ後 ($newer
) のメモへのリンクが存在している。
それを見つけるロジックにて、見つけた後もループし続けていたため、見つけたらループ抜けるように変更。
RawScore が 100 ほど改善 (2400)。
N+1 問題を回避
N+1 を回避 · okashoi/isucon3-practice@76ae871 · GitHub
トップページにて、メモとユーザ名を結びつける部分があからさまに N+1 になっていたので修正。
RawScore が 300 ほど改善 (2700)。
セッションを保存先を Redis に変更
セッションの保存先を Redis に変更 · okashoi/isucon3-practice@ef0ffc3 · GitHub
「memcache がダメなら Redis だ!」という発想で Redis インストール。
sudo yum install --enablerepo=epel install redis sudo yum install php70-pecl-redis
うまく動いてくれた。
RawScore が 200 ほど改善 (2900)。
view 内のループで preg_split
を使っていたので explode
に変更
ループで preg_split を使わないようにした · okashoi/isucon3-practice@0b9792e · GitHub
view 内のループの中でメモの1行目を取得するために、贅沢にも preg_split
を使っていた。
正規表現を使った処理は重いので、文字列処理である explode
(と trim
) に変更。
シンプルだが、意外と効いて RawScore が 400 ほど改善 (3300)。
MySQL のクエリキャッシュ設定
デフォルトでクエリキャッシュは効かないようになっているため、効くように設定。
[mysqld] query_cache_limit=1M query_cache_min_res_unit=4k query_cache_size=256M query_cache_type=1
これで RawScore が 300 ほど改善 (3600)。
※ ただしあくまでクエリキャッシュであるため、本番の ISUCON では使い所が難しいとのこと。強いてやるとしたら、 init のタイミングで沢山実行されそうなクエリを予め実行しておく、など。これも init の時間制限があるの積極的には利用し難い。
改めて、index(雰囲気)
インデックス貼った · okashoi/isucon3-practice@373b2b3 · GitHub
データ初期化の仕組みを理解して、改めて冒頭の雰囲気 index を設定。
RawScore が 200 ほど改善 (3800)。
マシンを再起動し動作確認
残り時間 15 分を切ったあたりでマシンを再起動し、ベンチマークが問題なく回るか確認。
httpd が立ち上がらないようになっていたので、設定。確認してよかった。
そして競技時間終了。
反省・感想など
終わった後は、解説記事を読みつつ反省会。
スコアと作業の記録を結びつける
慣れているメンバーは作業ログを次のようにして残していた。
- ミドルウェアの設定ファイルも Git 管理下に置き、シンボリックリンクを張るようにして一元管理する
- 作業を Pull Request にすることで、Pull Request のページがそのまま作業ログのページになる
- Pull Request のコメントとして、その時点のベンチマーク結果を貼る
これは「何をやったら、どれくらいスコアが上がった」が見えるようになるので、振り返りもしやすく、とても良い。
後でブログ記事を書くのも楽だ。
施策に根拠を持つ
今回はなんとなく「この辺かな?」というところを手当たり次第触った感じだった。
これでは施策の優先順位付けや、「つまずいたときに『いつ』見切りをつけるか」という判断ができなくなる。
とくに SQL まわりはお粗末な対応だったのできちんと slowlog を見たり、explain の結果を見たりした上で施策を考えなきゃな、と痛感した。
「memos 取得の order by 指定を created_at
から id
に変更」の施策の効果が出なかったのは、クエリの where 句にて is_private
を指定していたため。
is_private
, created_at
の複合 index を貼るのが正しい対応で、他の参加メンバーはみんなきちんとこれをやっていた。
普段の業務とは違うアプローチを身につける
普段の私が業務で扱うシステムは高負荷状態になったり、極端にスピードが求められることが無い。
どちらかと言えば運用保守のし易さや、データの健全性を優先することが多く、考え方もガチガチに正規化テーブルに立脚されたものになりがちである。
上記の解説記事ではテーブルの非正規化によってスコアを改善していく部分(public_memos, public_count テーブルの追加、memos.title カラムの追加等)があるので、こういったスピード優先のアプローチを身に付けたい。
デフォルトの構成を決める
一緒に参加していた勉強会メンバー曰く、デフォルの構成を決めてしまい「いつも通りに作業ができる」土俵に持ち込むのが良い、とのこと。
勉強会メンバーではスキルセット的に PHP7.1 + nginx + php-fpm + MySQL5.6 という構成が良さそう、という話で落ち着いた。
ただ、例年の ISUCON を見るに PHP はやや不遇な扱いを受けているような気がしており「第二の選択肢は準備しておきたいよね」という話にもなった。
「身に付けたいスキル」という観点も含めて「言語としては Go あたりかな〜」という話をした。
まとめ
単独で ISUCON の問題に挑戦するのは今回が初めてだった。
いろいろ勉強になり、自身の課題もたくさん見えてきたので ISUCON に取り組むことは非常にいいことだと思う。
1人だとなかなか「やろう!」という気にならないが、人と一緒ならやる気になるので今のメンバーがいることに感謝。