ISUCON6予選突破しました

ISUCON6予選突破

ISUCONというWebアプリケーションのパフォーマンス改善コンテストに参加し、予選突破しました。 やっとですよ。やっと。ISUCON4, ISUCON5と予選敗退していてプライドがズタズタになっていたのでやっと突破できて本当に嬉しいです。

スコアは124,271点(ベストスコアは13万点台)でした。

チーム:にるぽ

appengine ja night(現gcp ja night)つながりでチームを組みました。 @najeira がGo推しだったので言語はGoを選択。私は2年ぶりくらいにgolangを触ることにw

出場チーム数

317チーム中11位でした。

ちょっと気になってコレまでのチーム数を調べたら下記の通り

  • ISUCON1 20チーム
  • ISUCON2 25チーム
  • ISUCON3 74チーム
  • ISUCON4 185チーム
  • ISUCON5 271チーム
  • ISUCON6 317チーム

年々参加チーム数が増えてて強豪揃いの中、ナントカ予選突破できて良かったです。

予選問題

予選の問題は、はてなダイアリーはてなスターを模したisudaとisutarのhttpサーバが立っていてマイクロサービスアーキテクチャな構成でした。

isudaはキーワードと本文を投稿でき、キーワードが投稿されると全ての投稿本文にあるキーワードがリンクされます。まさにはてなダイアリーみたいな感じです。

予選でやったこと

ざっと変更点を箇条書きすると下記になります。

  • isudaとisutarの統合
  • インメモリーキャッシュ
  • strings.NewReplacerを使うように変更
  • 正規表現ライブラリをrubexに変更
  • /initializeで最初の1ページはHTML, Regexp, Starのキャッシュを生成
  • nginxで静的ファイル配信
  • テーブルにindex追加
    • entry#updated_at
    • star#keyword

ボトルネック

今回の問題はボトルネックがDBではなくキーワードリンクしたHTMLを生成するhtmlifyでした。 DBはuserが500件程度, entryが7000件程度と件数が少なかったためクソな検索でもボトルネックになっていませんでした。 フルスキャンしてるQueryはありましたが最後までそのまま放置し、ボトルネックに集中する戦略にしました。

改修

まずは目に付くマイクロサービスになっているisudaとisutarの統合。httpをやめて直接SQLを発行かつインメモリキャッシュできるように変更。ココはサクッと @soundTricker が行いました。振り返ってみるとあとはひたすらインメモリーキャッシュですね。htmlifyしたHTML、Regexpインスタンス、Star、Spam、Userはテーブル全件をキャッシュしました。

並行に作業を進めていたのでどれが劇的にスコアに影響を与えたのか言い難いですが、終盤スコアがあがっていったのは下記の変更を入れ始めてからだったと思います。

  • htmlifyの中で正規表現オブジェクトを毎回作るのではなくhtmlifyの外で正規表現オブジェクトを作る
  • HTMLと正規表現オブジェクトをキャッシュする
  • strings.NewReplacerの利用
  • 正規表現ライブラリをrubexに変更
    • 終盤は正規表現ライブラリを変更したら2万点くらい上がりました。

司令塔 najeira

@najeira が本当に素晴らしい仕事をしてくれてインフラとアプリの改修、ボトルネックの計測という全ての面で大活躍。「そこって遅いの?本当にボトルネックなの?」と問いかけどこを修正するべきかを指揮し、全てが的確でした。@najeira がhtmlifyの対応をしている時にじゃ他のところやるかーってなっていたのを「いや三人でココをナントカするべき」という判断を下したのが印象深いです。

また、事前に下記のgoライブラリを用意してました。

ボトルネックを調査するためにmeasureで計測

まず最初にmeasureを埋め込み/statsにアクセスすると計測結果を確認できるようにし、/stats_resetで計測結果をクリアできるように準備します。その後ベンチマークを走らせて/statsのurlを叩けば遅い箇所が一目瞭然になるようにしていました。計測結果はGoogle Spreadsheetで共有し @najeira が計測した結果を元に次にどこを修正すべきかの道筋が立てられたのは本当に大きいです。

この辺りは @najeira とは2回目のISUCON参加ということもあり、事前に考えて準備し工夫していた効果が出て良かったです。

debugとconv

私が普段golang触っていないこともあり、デバッグするためのライブラリdebugを作ってもらいました。とりかくdebug.Printしまくってポカミスを減らせました。debug.Printは実行時に実行されたファイル名とファイルの行数と引数の値を確認できるライブラリです。事前に即席で作ってもらいました。

golangに慣れてないせいで数値から文字列、文字列から数値変換など簡単な事もさっと出てこないためconvを作ってもらいました。Stringにしたかったらconv.Stringだし、Intにしたかったらconv.Int(s)すれば済みます。

また、/testというハンドラを用意しそこでデバッグするようにしてました。

r.HandleFunc("/test", myHandler(testHandler))
func testHandler(w http.ResponseWriter, r *http.Request) {
  debug.Print("== Call /test")
  responseText := conv.String("test")

  // ココでfuncのコードを動作確認

  w.WriteHeader(200)
  responseText = fmt.Sprintf("%s", responseText)
  fmt.Fprintf(w, responseText)
}

あとは起動してcurl http://localhost:5000/testする感じです

予習

事前に一度三人で集まってpixivの社内ISUCONの課題を解きました。素振り重要です!matsuuさんのおかげで初azureでもさっと課題に取り組めました。感謝。 matsuuさん毎年いつもありがとうございます。

環境構築

サーバ起動したらansibleで全員ssh入れるようにし主要なツールは入れられるようにしてました。

デプロイ

fabricを使いました。fabric使ったこと無かったけど便利ですね。簡単で軽量で。 これを使ってgoのビルド、デプロイやnginxの設定ファイルの配布やnginxの再起動など全てをfabコマンドで行えるようにしてました。

競技終了前のチェック

大事ですね。この時間ずっとドキドキしていました。17時までに修正をやめて再起動テストを行う時間を作ろうというのは最初から話をしていたので、大きな修正は16:50頃には終わっていました。そこからはずっと再起動テストを行いスコアがfailしないこと、レギュレーションを読み直して大丈夫だよね?ってのを確認してました。

最後に

本当にメンバーに救われました。@najeira, @soundTricker のおかげで本選に出場できました。メンバーには感謝です。 私はココをこうすればいいんじゃないの?とは言えてもgolangだと書き方がわからなくってみたいなのが多々あり、アイデアしか出せない場面もあったり本選は足をひっぱらないようにgo力高めておきます。

本選も優勝目指して頑張るぞい。