blog.handlena.me

コード書いたりゲームしたり

ISUCON7 予選通過

10/22にISUCON7の予選があり、参加してきた。 チームはISUCON4のときと同じメンバー構成のfujiwar組。

#isucon 4決勝にfujiwara組として参加した感想

結果としては、予選二日目参加チーム中3位で予選通過。 一日目もあわせた全チーム中の順位でも3位だった。 二日目手練多すぎ・・・。

ISUCON7 本選出場者決定のお知らせ : ISUCON公式Blog

やったこと

ここに書いていることは自分が行った変更についてのみなので、 チームとしてなにをやったのかを知りたい場合は他のメンバーのエントリも併せて読むといいでしょう。

事前の準備

言語はGoを使うということだけ決めていたので、スムーズにGoを書けるかどうかの確認。 書けるけど普段から書いているわけではないので。

開発マシン上に必要なものが揃っているかもあわせて確認。 実際go1.7のままだったりしたので確認しておいてよかった。 家のPCでは最近コード書いてないからな・・・。

役割分担

  • fujiwara=サーバー上の操作・サーバーリソースチェック・ベンチ挙動のチェック
  • acidlemon=サーバーリソースチェック・変更実装
  • handlename=変更実装

二人が実装方針を決めて、自分がとにかく実装する構成。 自分はサーバー上でほとんど操作していないし、fujiwaraさんはコードを一行も書いていない、くらいの分担具合だった。

レギュレーション確認

まずは全員でレギュレーション確認。大事。

初期ベンチ実行

Go実装の初期スコアは3854だった。 この時点では負荷レベルはほとんど上がらず。

開発環境構築

ローカルマシン上でアプリを動かすべくdocker-compose.ymlをせっせと作成。 複数台構成を再現するためにわざわざDockerを使っていたのだけど、 後から振り返ると自分が担当した変更には必要なかった。

画像の書き出し

アイコン画像がmysqlに保存されていたので、まずはこれをファイルに書き出し。 ローカルマシン上でこんなスクリプトを実行しておしまい。

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;

use DBI;
use Path::Class qw{file};
use FindBin;

my $dbh = DBI->connect("dbi:mysql:isubata;host=127.0.0.1", "root", "password");
my $sth = $dbh->prepare("SELECT id, name, data FROM image ORDER BY id ASC");
$sth->execute() or die $dbh->errstr();

while (my $r = $sth->fetchrow_hashref()) {
    my $name = $r->{name};
    warn $name;
    file("${FindBin::Bin}/icons/${name}")->spew($r->{data});
}

こういう書捨てのコードは慣れているPerlで書くのが速い。

この時点で1000レコードある画像テーブルに、実は67種類の画像しか格納されていないことがわかる。

画像取得の制限

画像をファイルにしたことでmysqlの通信負荷が激減。 さらに毎回該当する全レコードを取得していたのをLIMIT 1することで必要なものだけ取ってくるようにし、さらに通信減。

インデックス追加

お約束のインデックスがないスキーマの改善。 発行されているクエリを見て必要そうなインデックスを追加。

普段仕事で行っているプロジェクトでは、スキーマ定義の差分からalter文を自動生成している。 alter文を書くという発想がなくなっていたのでベンチ環境に反映する時にちょっと手間取った。 このときはtableそのものを作り直して初期データをロードし直すという乱暴(だけど確実)な方法で反映した。 初期データロードには2〜3分かかるので、以降はちゃんとalter文を用意している。

N+1問題解消

これもお約束。 ループ処理内でmysqlへのクエリが発行されていたので、これを解消。

/messageでは各messageごとにuserをselectしていたので、 messageをselectする時にuserもjoinすることにした。

https://github.com/isucon/isucon7-qualify/blob/40b6486c8dfe8bac6c12860c9cfb79845306bcec/webapp/go/src/isubata/app.go#L390-L396

/historyはページングが必要なAPIで、OFFSET/LIMITを指定してselectしていたので、

  1. OFFSET/LIMIT指定でmessageのidだけ取ってくる
  2. idでWHERE INしつつuserをjoinして必要なデータを取ってくる

という2つのクエリに分割した。

このままでは並び順が固定されずベンチマークが失敗するようになってしまったので、 ORDER BY FIELDを指定して並び順を固定。

/fetchが一番重かった。

sleepが入っていて処理の重さはそこまで影響していなかった(たぶん)のと、 出題の意図としては/fetchレスポンスタイムを遅くすることで全体のスコアを上げるというものがあったらしく、 解消の手間の割に効果は少なかったのかもしれない。

user情報をsessionに格納

ログイン状態のチェックがすべてのAPIに含まれており、 その中でmysqlにuser情報を取得するクエリが発行されていた。 思い切って必要なuser情報をsessionに持たせることにしてしまい、 mysqlへのクエリ発行をなくすことにした。

実装中にtime.Timeをそのままsessionに突っ込むというミスをやらかしてしまい、 他の値もsessionに記録されてなくなるというハマり方をしてしまった。 シリアライズできない型が突っ込まれると、session全体が保存されないらしい。

https://github.com/gorilla/sessions/blob/a3acf13e802c358d65f249324d14ed24aac11370/store.go#L107-L111

ちゃんとsession.Saveのerr確認しろよって話。

このミスのせいで変更の取り込みが競技終了間際になってしまい、 その頃はベンチマーク結果の上下が激しかったということもあり、 効果があったのかどうかはよくわからない感じだった。


いろいろ変更を加えはしたが、これらは画像のキャッシュ設定がうまくいって初めて効きはじめるもの。 知らぬ間に課題をクリアしてくれていたチームメンバーに感謝 :pray:

その他

ベンチマーカー

待ち時間がほとんど無く、非常に快適だった。 ただ、特に後半はベンチマークを実行するたびにスコアが大きく上下したため、 打ち手が有効かどうかに確信が持てなかったのが残念だった。

競技時間

時間としては例年通り8時間だったが、13:00開始〜21:00終了だった。 理由についてはISUCON運営の方が説明されている。

ISUCON7 予選開始の遅延について : ISUCON公式Blog

ISUCON運営は本当にしんどいので、遅延したことに対して文句をいうつもりはないけども、 21時というと普段はもう寝る準備をしている時間なので、体内時計的につらかった(子育て勢)。

スコアの記録

やったこととその時のスコアの対応をメモっていなかった。 後から見返してどうだったかがわからないので、ちょっと残念。

おわり

予選突破できてよかった。 fujiwara「本戦で3位以内を逃したこと無いから」というプレッシャーがかかっているので、 本戦もがんばりたい。

運営のみなさま、参加者のみなさま、ありがとうございました。 本戦もよろしくお願いします。