isucon6本戦に出場して7位でした


IMG_0020

@Konboi@tkuchikiとisucon6本戦に出場して7位でした。

振り返りとして、やったことと反省点をまとめておきます。

お題

Docker上にReact(兼Nodeサーバ) + APIサーバー(各言語実装)で構築されたリアルタイムお絵かき掲示板

やったこと

事前準備

普段のギョームではruby(rails)を書いているので、帰宅後にコツコツGoを書く日々を過ごしていました。
掲示板を一通り作ってみた感想としては、GoでWebアプリケーションを作るのは大変だなぁというのが正直なところです。
自分が普段いかにレールの上を乗っていたかを思い知らされました。

当日

前半

  • 早めに家を出て会場に到着
  • コンビニで朝食とお菓子を調達(今回はレッドブルには頼らなかった)
  • 開始直後サーバが立ち上がらないというトラブルがあったものの、運営の方のスピード対応で復旧
  • インフラ周りはtkuchiki、アプリ周りはKonboiと自分といういつものパターンで進めること
  • サーバが立ち上がるまではレギュレーションを読む
  • サーバが立ち上がったら、ブラウザでアプリケーションがどんなものかを試しつつ、sshでサーバに入ってコードを読んでみる
  • コードを読んでる間にtkuchikiがコードをリポジトリ管理するようにしたり土台を整えてくれた
  • Konboiと二人で「Reactなるほど分からん」となりつつ、自分がNodeのコードを、KonboiがGoのコードを重点的に見てみることに
  • この間にtkuchikiはDockerを剥がすという対応を進めてくれていた
  • /img/:id が中でAPIを呼んでSVG生成をしていたので、Go側で直接SVGを返すようにしようという話になり、自分はここをやることに
  • Konboiは閲覧者数の部分をredisに置き換える対応をやってくれていた

中盤

  • ローカル環境は素早くつくることができた
  • node側で生成していたSVGのソースを読みながら、DBの内容と照らし合わせつつ以下のように愚直にxmlを書いていく
func getImg(ctx context.Context, w http.ResponseWriter, r *http.Request) {
    idStr := pat.Param(ctx, "id")
    id, err := strconv.ParseInt(idStr, 10, 64)
    if err != nil {
        outputError(w, err)
        return
    }

    m := make([]string, 0)
    m = append(m, `<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">`)

    room, err := getRoom(id)
    if err != nil {
        outputError(w, err)
        return
    }

    width := strconv.Itoa(room.CanvasWidth)
    height := strconv.Itoa(room.CanvasHeight)
    m = append(m, `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" baseProfile="full" width="`+width+`" height="`+height+`" style="width:`+width+`px;height:`+height+`px;background-color:white;" viewBox="0 0 `+width+` `+height+`">`)

    strokes, err := getStrokes(room.ID, 0)
    if err != nil {
        outputError(w, err)
        return
    }

    for _, s := range strokes {
        strokeId := strconv.FormatInt(s.ID, 10)
        strokeWidth := strconv.Itoa(s.Width)
        red := strconv.Itoa(s.Red)
        green := strconv.Itoa(s.Green)
        blue := strconv.Itoa(s.Blue)
        alpha := strconv.FormatFloat(s.Alpha, 'f', 1, 64)

        points, err := getStrokePoints(s.ID)
        if err != nil {
            outputError(w, err)
            return
        }

        var p = []string{}
        for _, point := range points {
            p = append(p, strconv.FormatFloat(point.X, 'f', 0, 64)+","+strconv.FormatFloat(point.Y, 'f', 4, 64))
        }

        m = append(m, `<polyline id="`+strokeId+`" stroke="rgba(`+red+`,`+green+`,`+blue+`,`+alpha+`)" stroke-width="`+strokeWidth+`" stroke-linecap="round" stroke-linejoin="round" fill="none" points="`+strings.Join(p, " ")+`"></polyline>`)
    }

    m = append(m, `</svg>`)

    var res = make([]byte, 0)
    for _, v := range m {
        res = append(res, v...)
    }

    w.Header().Set("Content-Type", "image/svg+xml; charset=utf-8")

    w.WriteHeader(http.StatusOK)
    w.Write(res)
}
  • 14時過ぎに一通りできて、tkuchikiにnginxの/imgの向き先をGoに変えてもらってベンチ実行
  • スコア0でFAILかつエラーメッセージもなしで焦る
  • ちょうどfujiwaraさんがidobataで同じ質問をされていて、SVGのパースが失敗しているということが判明
  • ここからひたすら原因の調査に時間が持って行かれる
  • 適当なidで差分がないか比較してみようと思い、もともとnodeが出力している/img/900とGoで置き換えた/img/900のdiffを取ってみたものの差分なし
  • ブラウザのヘッダーを確認していたらContent-Lengthを付けていなかったことに気づく
  • 「これだ!」と思ってContent-Lengthを付与して再デプロイ
    w.Header().Set("Content-Length", fmt.Sprintf("%d", len(res)))
  • が、またもやスコア0でFAILかつエラーメッセージなし
  • この間Konboiはstroke pointをredisにキャッシュする対応を進めてくれていて、tkuchikiは複数台構成の調整をしてくれていた

終盤

  • tkuchikiにも見てもらったところ、nodeとGoでContent-Lengthが異なるデータがあることが判明
  • 該当のデータをdiffで見ても1行なのでどこが違うか分からないため、目grepでひたすら確認(これがキツかった)
  • すると、point.Xの部分で小数点以下の数値が返ってきていたことに気づく
  • point.Xは初期データが整数値だったので小数点以下を切り捨てるというやらかしをしてしまっていた。。。
- strconv.FormatFloat(point.X, 'f', 0, 64)
+ strconv.FormatFloat(point.X, 'f', 4, 64)
  • 小数点以下も返すようにして再デプロイ
  • ついにベンチが通った
  • とはいえ点数はそんなに伸びず
  • 原因の特定に時間がかかり過ぎてしまい気づいたら17時くらいになっていた
  • Konboiのキャッシュ対応を手伝いたかったがredis力不足でパッと分からなかったので、キャッシュ後もブラウザ上で正しく動作しているかの確認を手伝う
  • キャッシュ対応の方は線を書いた後に「ビュンッ」っと謎の線が出現してしまう現象に遭遇し時間的に断念
  • 再起動試験はtkuchikiが早めに終わらせてくれていたので残り時間で最後のチューニング
  • アプリケーションとDBが同じサーバに乗っていたので、DB専用サーバとして分離してもらい最後のベンチ
  • 最終スコアは17292点で7位という結果に

感想と反省点

今回/img/:idの対応でドはまりしてしまい、結局自分はこの対応しかできなかったのが反省点です。
また、途中Konboiが「ループクエリ発見」と言っていた時にループクエリの改善を手伝うことができたはずなのに自分の作業を止められず、ひたすら目の前のエラー修正のことしか頭が回ってなかったのも良くなかったと試合後に反省。

懇親会では問題出題者のedvakfさんや、高校生参加者のhideo54さんと色々お話できて楽しかったです。
今時の高校生はnginxとpythonでアプリケーション作ったりしててただただすごい。
高校生のお手本になれるように精進せねば。

isuconをきっかけにgoを学び始めましたが、せっかく学んだので引き続きgoで色々作っていきたいなぁと思いました。
次はLINE BOT AWARDSに向けて何か作ってみます!

isuconに出ると毎回まだまだ技術力が足りないなぁと痛感しますが3年前よりは少し強くなれた気がします。
来年もまた挑戦したいと思えたし、同じチームで次こそ勝ちたいという想いが強くなりました。

運営の皆様、予選から本戦まで準備/運営をありがとうございました!
非常に楽しい時間を過ごすことができました。
isuconカード大切にします。

Konboi、tkuchiki、二人と一緒のチームでずっと続けてこれて本当によかったなぁと思います。
いつも学ばせてもらってばかりなので自分も二人に負けないようにがんばらないと。
いつもありがとう。これからもよろしくです。