ISUCON7 予選1日目1位通過しました

ISUCON7予選一日目 一位通過しました

ISUCON7

ISUCONとは

お題となるWebサービスを決められたレギュレーションの中で限界まで高速化を図るチューニングバトル、それがISUCONです。過去の実績も所属している会社も全く関係ない、結果が全てのガチンコバトルです。

運営の方々のご尽力もあり、今年のISUCON7も無事開催されました。
ISUCON7 開催&日程決定 本当にありがとうございます。
1日目、2日目共に開始時刻が遅延することにはなりましたが、競技中は問題はなく、快適に開発をすることができました。

遅延している間、チームでは、「今どんな発言も運営の人にとってはノイズみたいなものだから、黙って待って、開催出来たら感謝、振替で参加できなかったとしても、ここまで頑張ってくれたことに感謝しよう。」という話をしていました。
無事開催され、本当に運営の方々には感謝しています。

チーム「MSA」

私はチーム「MSA」として予選一日目に参加させていただきました。
社内でプログラミング経験者を募集して、まずsuzukiさんが一緒に出てくれることになり、応募締め切りの一週間ほど前にken39argさんが一日目であれば参加可能ということで、チーム結成となりました。
チーム名はそれぞれの苗字の頭文字からとっています。

チームでの役割としては、私はアプリを担当し、suzukiさんはインフラ、ken39argさんにはアプリを実装してもらいつつインフラもみてもらいました。
私も社会人3年目でweb開発はまだ2年も関わっていないぐらい、suzukiさんも新卒でしたので、ken39argさんに引っ張ってもらう感じでした。

事前準備

とりあえず、一緒にランチに行き、「こんな準備がいるかなー」とか「こんな問題がでるかなー」とか話していました。
これが予選の10日ほど前だったと思いますが、githubリポジトリを作成して、issueに当日の流れを書くぐらいで、チームで集まって過去問を解くなどはしていなかったので、正直色々もたつくだろうと思っていました。(実際ちょっともたついてしまいました。)

予選当日

先にISUCON7予選の問題について触れておくと、 isubata というアプリでした。
チャットアプリケーションである idobata が元ネタだと思います。
チームの作業の流れとしては下記のような流れでした。

  1. スローガンを決める
  2. レギュレーションと当日マニュアルをみんなで読む
  3. とりあえず初期状態でベンチマーク実行
  4. 構成確認
  5. gitにコードを含めて、変更後デプロイをコマンド実行でできるようにする
  6. nginxとmysqlのログを吐くようにする + mysqlのインデックス張る
  7. ユーザーのアイコン画像の情報がDBに入っていたので引き剥がす
  8. 既読管理によるDBへの集計クエリループ削除
  9. nginxでCache-Control設定
  10. メッセージ作成のループクエリ削除
  11. 画像のproxy方法の変更
  12. 終了2時間前にスコアが急激に下がって焦る
  13. 終了30分前からは反省を始める

(残念なことにISUCONについてはほぼほぼ初心者の集まりだったためスコアを記録するなどしていないので、どういうスコア推移になったのかはわかりません。)

スローガンを決める

開始が延期したこともあって、チームでの作戦を午前中に立てていました。
そこで決めたスローガンは「落ち着く」「いつも通りのことをする」「終了1時間前になったら変更をやめる」の3つでした。
特にISUCON用に準備していたわけでもないので、いつもやっていること以外はやっても不安定になるだけだろうということで、チャレンジはしないことにしました。

レギュレーションと当日マニュアルをみんなで読む

どこまで変えていいのか、最後の測定は再起動などどうなるのか、知っておかないとスコア残らないなんてことにもなるので、じっくり読みました。

とりあえず初期状態でベンチマーク実行

初期でちゃんとアプリケーションが動くかどうか確認するため、golangに切り替えてベンチマークを実行しました。

構成確認

最初の構成を確認して、appが2台とdbが1台であることを確認しました。
1台だけcpu、メモリなどが違うなどの偏りはありませんでした。

gitにコードを含めて、変更後デプロイをコマンド実行でできるようにする (@suzuki)

gitにコードをpushし、makeコマンドのみで変更の反映ができるようにしました。

nginxとmysqlのログを吐くようにする (@ken39arg)

alpとpt-query-digestで解析できるように、nginxのアクセスログmysqlのslowlogを書き出す用に設定しました。
結果から言うと、準備不足で最後までpt-query-digestの実行はできず、コードを見つつ、slowlogを目でざっと追ってindex張るなどしました。

ユーザーのアイコン画像の情報がDBに入っていたので引き剥がす (アプリ: @mizkei、インフラ: @suzuki)

ユーザのアイコン画像がDBにBLOBで入っていたため、それを引き剥がしました。
アプリとしては特に難しいこともないので、 POST /profilemysqlにデータを入れていた箇所をファイルに書き出すようにして、 /initialize でデフォルトユーザのアイコンをDBから引き出して、ファイルに保存しました。

インフラとしては、初期の状態ではDBサーバのリソースがだいぶ余っていたのでappもおいてしまい、DB+appのサーバーに POST /profile をproxyして、静的ファイルは全てそこにおいておき、 /icons へのリクエストをそちらに流すようにしました。

既読管理によるDBへの集計クエリループ削除 (@ken39arg)

isubataではチャンネルのメッセージに対して、個々のユーザがどこまで読んでいるかを記録し、チャンネルの一覧部分で、未読件数をバッジで表示していました。
この未読件数はチャンネルのループに対して、メッセージの集計クエリがループするというだいぶ重いものになっていました。
isubataではメッセージの削除機能は存在していなかったため、メッセージの総数などはテーブルのカラムに追加して、ループクエリは削除しました。
これも /initialize でデフォルトのメッセージをカウントしてテーブルの更新をしていました。

nginxでCache-Control設定 (@suzuki)

イコン画像などは変更がなければ返す必要すらないので、304 not modifiedになるようにnginxで設定を行いました。

メッセージ作成のループクエリ削除 (@mizkei)

メッセージ作成用の関数に jsonifyMessage という、いかにもjson用の構造体など作っていますよ、みたいな名前のやつがいましたが、こいつは中でクエリを実行していて、ループクエリしていました。
そこでぱぱっとクエリのループを消して、よしベンチ、と思ったらfail。
メッセージに紐づくユーザを引いてくるという処理なのですが、元のクエリではuserのidは引いていなかったので、紐付けるidがないという状況でした。

私のくだらないミスで、10分で終わる修正が50分になり、 ken39argさんにも何がおかしいか見てもらうなど時間を使ってしまいました。

画像のproxy方法の変更 (@suzuki + @ken39arg)

ここでボトルネックがDB+appサーバーのリソースに移っていたので、appを消し去り、DBのみとして、他2台でappの処理をすべて行うようにしました。
イコン画像の処理はメモリを使うこともあって、1台で POST /profile を受けることはせず、それぞれ保存して、nginxからtry_fileで存在しなかったら、もう片方にproxyするようにしました。 ここでスコアは46万点を超えました。

終了2時間前にスコアが急激に下がって焦る

残念なことにここで私達の修正は終わりました。19:30を過ぎた頃だったと思います。(一応1日目の終了時刻は21:15ぐらい)
再起動試験をしつつ、DBのチューニングにようやく取り掛かろうというところで、急にスコアが30万まで下がりました。
原因がわからず、もう一度回すと20万、10万と下がっていきました。
なんだ?と思っていたら、appサーバー2台がswapし始めていました。
今まで変更のたびに再起動していましたが、DBのみの修正でapp再起動しななかったのですが、連続でベンチを実行することでメモリが太っていくことがわかったのです。

これは推測でしかありませんが、アイコン画像のアップロードについて、ioutil.ReadAllしている箇所があり、これが良くなかったのかなと思っています。
普段はReadAllなんてほぼほぼしないですが、ボトルネックにもなっていなかったし、画像データのsha1からファイル名を作っていたりと、修正が若干面倒だったので、放置していたのがよくなかった。

(fujiwara組の記事にありますが、GOGCの設定でも充分に防げていたようです。)

終了40分前からは反省を始める

原因はわかったので、修正どうするという話になりました。
とりあえず、数回のベンチマークであれば30万点程度までの落ち込みで済んでいたので、アプリの再起動だけして、放置しました。
不安ではありましたが、無茶な変更してfailするよりはこのままにしようという決断をしました。
nginxやmysqlのログも切らずにそのままにして放置。

その間「これがよくなかったか。」「気づけたよなー」という話をずっとして過ごしていました。
私もken39argさんも「落ち着く」ができておらず、アプリの修正でくだらないミスをしていたりして、時間の浪費があったので、これはなくしたかったという反省をしていました。

まとめ

結果として私達のチームは一日目予選一位で通過しました。
ISUCON7 本戦出場者決定のお知らせ
ただ、全体としては5位であり、まだまだ改善できるポイントはチームの役割も含めてたくさんありました。
決勝で勝つために予選での反省点を活かしていきます。

最高に楽しいイベントを開催して頂いている運営の方々に感謝です。
決勝もよろしくお願いいたします。

ISUCON6に参加してきました

いい感じにスピードアップすることを目的とする大会「ISUCON」の第6回予選に参加しました。
結果としては、予選敗退でした。
初めての参加でしたが、苦しさと楽しさを感じることができて非常に勉強になりました。

やったことまとめ

ISUCON前

チームは同じ職場のtkyshmさんとgurisugiさんと組みました。
チーム名は「RedComp」。【赤】と【粉】です。 私はアプリを担当し、tkyshmさんがインフラ周り、gurisugiさんが司令塔といったような役割でした。

tkyshmさんと私はISUCONへの参加が初めてだったため、gurisugiさんに指導をしてもらいつつ、 pixiv private isuconと去年のISUCON過去問を解きました。

大体は業務でやっていることと変わらず、MySQLでちゃんとインデックスはって、それを使うようにし、 ループクエリにならないようにするなどをしました。
普段の業務はPerlなので、それをGoでさっと書けるようにしました。
また、redisを使った実装なども一通り触り、準備は十分かなという気になっていました。

ISUCON

ISUCON予選本番でやったことは下記のようなことです。

  • azureサーバ構築
  • Goのコード読み
  • MySQL周りの修正
  • isudaとisutarを一つに
  • Perlへの切り替え

動いていたサービス自体の概要としては、あるキーワードについて、その内容が書かれている。
記事の書かれたキーワードが他の記事に含まれているときにはそのキーワードのページへのリンクが作成される。
つまりははてな
各ユーザはスターをつけることができる。

この中で私がやったことをつらつらと書いていきます。

Goコード読み

最初にGoのファイルの有るディレクトリを開いたときに、いくつかgoファイルがあり、 main分割してるのかと思ったのですが、開いてみると、isudaとisutarという2つにmain関数が存在していました。
systemdで確認するとisupamというserviceも存在しており、これはなかなかに面白いのでは、と思っていました。

ざっとコードを読んで、isutarはスター(お気に入り?)のようなものを管理しているだけで、別れている必要がなく、 一つにするのも簡単だなと思いました。
テンプレートの中にループクエリがあるわけでもなく、メインの処理の中にも一つぐらいしか、ループクエリはありませんでした。

この時私は、「インデックスをはって、isudaとisutarを一つにすれば、とりあえず、スコアは跳ね上がるだろうなー」とか思ってました。

goのコードを変更せずにベンチを回すとタイムアウトの減点で0点でした。

isudaとisutarを一つに

isutarは処理はスターの登録とスターの取得でした。

登録の際にはisudaの方に記事があるかを確認してから登録し、取得の際にはisudaからisutarにリクエストが飛ぶなど、 相互依存の関係にありました。

とりあえず、gurisugiさんにisutarのテーブルをisudaのdbと同じところに作成してもらいました。
スターの取得処理は単純にキーワードで引いているだけだったため、isutarからisudaにコピーするだけ。
スター登録処理もisudaからポストしていた値をそのままスターのテーブルにinsertするだけ。
初期化処理もtruncateするだけ。
一瞬で統合は終わりました。

この間にtkyshmさんがキーワードの長さをdbに登録するようにしてくれていたので、それを使って処理をするように書き換えて、 統合自体はこの処理をmergeした後にしようと思っていました。
結果から言えば、統合したコードは最後までmasterにはいることはありませんでした。

キーワードの長さをdbに登録するようにしてもベンチはまだ0点。

redisからkeywordを取ってくる

キーワードをリンクにするのが重い、ということでループクエリになっていたキーワード取得の部分をredisから引くようにしました。

キーワードの長さは既にdbに格納されるようになっていましたので、初期化処理の際にキーワードを値、キーワードの長さをスコアにするようにredisに格納しました。
キーワードのinsertの際はredisにも値が格納されるようにし、ループクエリとなっていた箇所はredisからの所得に置き換わりました。

しかし、これをしても点数は200点程度。初期実装のperlですら1800点ほど出しており、もう焦りが私を満たしていました。

regexpをなくす

これ以上はどうにもということで、pprofを打ち込んだのですが、圧倒的にregexpの置換が支配的でした。
みんなのGo言語にもありますが、Goは簡単な正規表現の範囲では、他の言語の正規表現実装よりも遅いです。

MustCompileしてReplaceしている箇所を消して、strings.Replaceに変えました。
ただ、これではキーワードの分だけ記事内容を全部舐めて置換する操作は消えないので、非常に重い。

予選が終了した後にtwitterでも多く流れていましたが、Replacerで置換するのが良いです。

傍観者と化す

この時点で既に16時半ごろでしたが、gurisugiがperlでぱぱっとキーワードの取得の部分をredisにしてくれました。
この修正でいきなり4万点までいき、isudoとisutarを統合したら7万まで点数は伸びました。(同時になんかちょこちょことした修正はしましたが、おそらく大きくは効いていない)

goでやったことと全く同じことをしただけでこの差でしたので、もう私の心は壊れかけていました。
現場では、「ごおおおおおおおおおおおおおおおおおおおおおおおおおおおおおふぁあああああああああああああああああああ」と叫んでいるだけの存在となり、 ただただ申し訳なかったです。

まとめ

私はあまりにもGoを書きたいという思いが強すぎた。
このような自体をおこさないようにするため、普段の業務からGoを書くようにしたい。(願望)

goを捨てる決断をするときも、私はまだ心の中で「待ってろよ!必ず戻って来てみせる!!」とも思ってました。
非常に悔しかったですね。また開催されるときには挑戦したい。

一緒にチームを組んでくれた二人には非常に感謝しています。
運営の方々には、このような楽しいイベントを開催して頂いて、本当にありがとうございます。

現場で使える実践テクニック「みんなのGo言語」

現場で使える実践テクニック「みんなのGo言語」

著者の一人であるfujiwaraさんから献本をいただきました。
fujiwaraさん、著者の方々、および技術評論社様ありがとうございます。

各章について、簡単に所感を書いていきます。

はじめに

「はじめに」にはGoの利点が簡単にまとまっています。
LL言語のような手軽さ、パフォーマンス、およびシンプルさなど、 この2ページを読んだだけでGoを使ってみようという気になるのには十分ではないでしょうか。

第1章

第1章はGoを使い始める準備の章です。

紹介されているツールはどれも気持ちよくGoを書いていくために有用なものばかりなので、 入れておいて損なものはないでしょう。

ファイル分割やパッケージ分割の考え方なども示されています。
多く語られているわけではありませんが、最小かつ十分な例と共に説明されており、 Goで少し大きなコードを書くことになった時には、非常に参考になると思います。

「現場で使える実践テクニック」ということで、vendoringやMakefileの書き方も綺麗に紹介されています。
ケルトンとして参考にするには十分なものであると思います。

"Goに入ってはGoに従え"、節のタイトルになっており、よく目にする言葉です。
Goで文法を勉強した後に何気なく書いていると陥ってしまうことなどが示されています。
mapがスレッドセーフではない点など、文法を勉強した後に一度目を通して意識しておきたい点が押さえられています。

第2章

第2章はGoをマルチプラットフォーム対応にしていくための注意点を示してくれる章です。

Goではクロスコンパイルができますが、愚直に書いているだけでは、 プラットフォームによっては予期しない動作をすることがあります。
この章では、マルチプラットフォームで動かすために知っているべきことを紹介してくれるので、 CLIツールを作るときなどに参考になると思います。

また、マルチプラットフォームで動作させるための注意点の中に、 Goのテクニックも織り交ざって紹介されているので、 "マルチプラットフォームはまだあとでいいや"と思わず、読んで欲しい章だと思います。

第3章

第3章は現場で実用となるアプリをGoで書くときのポイントについて、解説されています。

ある程度の大きさのアプリケーションを動かすことになれば、大量のログやデータのやり取りが発生します。
その中で、io.Writerやio.Readerをラップすることで、バッファリングをうまく制御し、 大量のデータをやり取りするときにでも問題なく処理ができるようにするテクニックが紹介されています。
また、問題が起こった時など、人間が調査する必要がある点についても言及し、 人に見やすいアプリケーションを作るポイントも紹介されています。

io.WriterなどはGoを書いている中で最も多く利用するインターフェースの一つではないかと思います。
テストの時に出力先をBufferにして、出力を確認するするなど非常に便利に利用できます。

また、安全なアプリケーションの終了の仕方も載っており、 1.7でxパッケージからcontextパッケージになったcontextを利用したキャンセルなどのパターンも紹介されています。

第4章

第4章はGoでのCLIツール作成についてです。

Goはクロスコンパイルが簡単に行うことができ、シングルバイナリで配布することができます。
その点から、CLIツールを作成することにも非常に向いています。
この章では、どのようなパッケージの構成でCLIツールを作成していくのかに始まり、 flagパッケージを利用して、コマンドラインオプションの設定などを解説していきます。

特にflagパッケージの実装を追っていき、独自のコマンドラインオプションの型を追加する節は、 読んでいるだけで楽しく興奮してくるような面白さがあります!!

また、CLIツールは誰か(作成した自分も含む)が使い続けていく点をあげ、 メンテしやすく、使いやすくなるようなポイントを示してくれています。

第5章

第5章はreflectについてです。

この本を読んでこれからGoを始めようとしている方は、この章は一旦さらっと読むだけでも良いかもしれません。
この章でも言及されていますが、必要となるまで利用しないようにするのが、reflectだと思います。
しかし、現実の問題に対処していくとなると、いずれ必要になることでもあります。
その時には、この章は問題に対処するため、非常に有用な情報を与えてくれるでしょう。

上記のようには書きましたが、reflectについて非常に丁寧に書いてくださっていますので、 reflectを使いはじめるとしたら、この章は参考になることばかり載っていると思います。
reflect.SelectCaseを初めて知り、reflectについての自分の知識のなさを実感しました。

reflectを利用した際のパフォーマンスについても書かれており、ちゃんと良くない部分にもフォーカスしています。

第6章

第6章はGoでのテストについてです。

Goでのテスト、ベンチマーク、及びExampleの使い方などが紹介されています。
Exampleはdocsでコードの確認が簡単にでき、書かれていると他の人がdocsを見た時に非常に助かるものです。

また、並列が得意なGoにおいてのData Raceの検知方法や、 ちょっと面倒なMockの方法など解説されています。

1.7で追加されたテストの機能についても所々で触れられています。


全体まとめ

私の感想としては、チームでGoを使いはじめるのであれば、第一章は必読と言って良いと思います。
Goのコードを書き始める前に読んでおくことで、スムーズに開発に入って行くことができるでしょう。

どの章も有用なテクニックが多く示されており、Goを書いていく上では、 「プログラミング言語Go」と並んで有用な図書であると私は思いました。

Goのシンプルさもあってか、小さくまとまったコードサンプルも多く載っており、 読んでいてわかりやすく、楽しい本でもありました。

裏表紙や各ページの余白部分に怪人(?)やその手下(?)のような絵があり、 「Gopher君達は彼らと戦っているんだろうなー」とか思いながら読んでました。

elmを使ってゲームを作ってみた

まとめ

  • elmlangを使って、10年ぐらい前にガラケーでこんなゲームをやっていたという記憶を元に簡単なゲームを作ってみた
  • Haskellっぽいけど、そんなにHaskell書けなくても問題ない
  • html、css、およびjsに関する知識が必要ないので簡単に作れる
  • コンパイルのエラーも丁寧でシンプルなので、英語がそんなに得意じゃなくてもできる
  • ゲームをつくる場合はSignal(通常のページならAddress)を知る必要がある

背景

  • elmlangのversionが0.16になり、かなり使えるようになってきたという噂をきいたので、なんか作ってみたかった
  • jsを長い間触っていなかったので、リハビリがしたかった(結果的にjsを触ることはなかった)

目的

  • 単純なゲームをつくることでelmlangでの開発を一通り体験する

実装

  • 準備

elmlangのversionは0.16

単純に公式のライブラリを使って1ファイルを生成するだけなら下記を知っているだけで大丈夫

# npmでインストールできる
$ npm install -g elm
# 下記のコマンドでelmファイルからhtmlを生成できる
$ elm-make main.elm --output=main.html
  • ソース

ぱっと見てわかりにくいかなというところだけ載せておきます

残りはこちらで確認してください

delta = Signal.map Time.inMilliseconds <| Time.fps 30

input : Signal Input
input =
  Signal.sampleOn delta <|
    Signal.map4 Input
      Keyboard.space
      Keyboard.arrows
      initialSeed
      delta

上記のコードでは30fpsのタイミングでキーボードのspaceや矢印を取得して、状態の更新に利用しています。Signalはリストとして扱うことはできないのでmap、map2...map5まで用意されています。上記のコードでは4つのシグナルをまとめています

elmには関数宣言でのパターンマッチがないので、空リストの場合などはcase文でかく必要があります

viewMoji : Moji -> Form
viewMoji ({x, y, char, scolor}as moji) =
  text (Text.fromString char |> Text.color scolor)
    |> move (x, y)
    |> scale 4

上記のコードにある"|>"は"x |> f = f x"という意味なので、表示させる要素の色や大きさなど複数指定していく時に便利です。逆の"<|"もあります

Signalの(~)と(<~)ですが、version0.16で削除されているので使えません。私はこれで結構時間を使ってしまいました(importの仕方が違うのかとか調べていました) remove (~) and (<~)

ゲーム

  • 動作はchromeでしか確認していません
  • このページだと矢印キーでページが動いてしまうので、別ページで開いて遊んでみてください

URL

遊び方
  • キーでスタート
  • restartは未実装
  • 黒い文字は文字の示す方向の矢印キーを押す

"右"なら→、"下"なら↓

  • 赤い文字は流れているラインの方向を押す

一番左を流れる文字は←、真ん中は⇣

文字が上でも赤色で左のラインを流れていたら、←を押す

問題点

  • キーが押されている間は継続的にSignalが発行されてしまう -> 同じ方向の入力が連続するときに一回押すだけですべて処理されてしまう

おそらくChangeかキーが押されたときのSignalとmergeすればいいはずだが、まだ実装はできていない

  • ゲームのレベルが上がった時の文字の流れる速度がうまく調整できていない

うまく調整できる計算式があれば、ちゃんとしたゲームっぽくなると思う

  • ソースが汚い

Haskellもそんなに書けないし、elmlangも1日しか触っていないので無駄が多いコードになってると思います(是非、助言や疑問点があったら指摘をしていただきたいです)

参考