🎢

ISUCON11本戦に出場して学生一位になった

2021/09/21に公開

ISUCON11の本戦に学生三人チームの雑談係で参加してきました。
結果は全体9位、学生1位でした。

いかすチームメンバー

名前(役割)

とさ(app)
まし(人力DB+app)
さんぽし(人力CI/CD)

みんな仕事を終えてappの改善に取り掛かっていたのでapp係の自分は実質無職です。

当日やったこと

app担当の自分が主に実装していた部分です。
alp, pprof, slow queryを参考にしながら、ボトルネックを発見していきます。

  • pprofと再起動試験対策のdb接続poolingのスクリプトを仕込む
  • シークを法を実装してページングの改善getAnnouncementList GET /api/announcements お知らせ一覧取得
  • GetGradeのN+1を一つ削除
    • IN句を用いて一度に取得するように変更
  • 後半は適当な部分にlimitいれたりbulk insertを実装してみたりいろいろしましたが、決定打にならず

中盤、ページングクエリを解決したあたりで、GET /api/users/me/gradesからのレスポンスが遅くてベンチマークが落ちるケースがありました。

終盤もGET /api/users/me/gradesがnginxのログを見る限りかなり重かったのですが決定的な改善はできず。N+1をこまごま解決するなどで少しは速くなりましたが潰し切りたかったです。

3台構成にしていたものの、mysqldがほぼcpuをもっていっていたので、app側でキャッシュや計算処理を担当することができればもう少し点数が上がったかもしれません。

ところどころ、改善案やエラーの解消方法がわからなくなったところは画面を共有してペアプロをしてもらっていました。

感想

チームメンバーと練習を重ねてきたので、ボトルネックになっている箇所を特定する流れが非常にスムーズだったと思います。また、最終的に一番点数が高かった箇所のcommitまで戻すなど冷静な判断ができたのも非常に良かったと思います。
あとは、見つけたボトルネックをどのように改善したらいいのかの引き出しを増やしくこと、実装できるように馬力をつけることを意識していきたいと思いました。

最終結果としては75000点で学生一位、全体九位でした!
学生最後の出場で一位をとれてとてもうれしかったです!

他のチームメイトのブログ
https://sanposhiho.com/posts/isucon11-final/
https://mesimasi.com/posts/isucon11-final

自分用メモ

文字数が少なくて寂しいので、予選が始まる前に、心構え的なメモを書いていたので放流します。

心構え編

どうせ当日には全て忘れるけど

  • 基本的な部分の解消がテンポよくできれば予選は突破できる(と思う)
  • 本質的な改善を先に(最初から安易にmemcacheに走らない)
  • alp -> pprofを見る
    • まずは全体で遅い箇所->Appで対応が必要な箇所
  • 改善できたからと言って点数が伸びるとは限らない
    • 複数ボトルネックを解消する必要がある
    • 改善できたかどうかは点数ではなくchromeのnetworkタブのレスポンス速度で確認すべし
    • 速度=点数ではない場合があるので当日マニュアルをよく読もう
  • 修正する箇所は関数に分割しよう
    • すぐに戻れるように
  • コメントを書いてコードを理解しよう
    • 概要把握が大切
  • ローカルで動作させられるようにしよう
    • 時間をかけても大丈夫
    • 最初の1~2時間はこちらに集中してもいいくらい
    • mysqlにデータが入ればクエリ書き放題
    • 2~3台目で練習もあり
    • [コメント]今回は本戦も予選も準備しなかったです。時間がかかりすぎるのも問題なのでできる範囲の方がいいと思います。
  • 大きいボトルネックは簡単な改善を積み重ねていく
    • N+1はまずは複数回クエリを発行しているところを全取得->app側で処理に変えてみる
    • その後に効率よく解決していけばいい
  • リクエストが見られるならリスポンスをみよう
    • 実装後に再度叩いてみて同じレスポンスが帰ってくるか確認する
    • 速度は変わっているかどうか確認する
    • 余裕があればテストを書く

具体的行動編

考えるべき箇所

  • メモリ
  • 帯域
  • ディスクI/O
  • フロントエンドCache(nginxでpublicつけるといけるらしい)

再起動対策

  • DBにはpingで問い合わせる
  • openは参照のみなので注意

MySQLのデータの内容を確かめる

https://qiita.com/ikenji/items/b868877492fee60d85ce

  • rows数が多いいテーブルをselect * from hogeしていないか
  • memoryに載せるならサイズは大丈夫か

DBのサイズ

SELECT 
    table_schema, sum(data_length) /1024/1024 AS mb 
FROM 
    information_schema.tables  
GROUP BY 
    table_schema 
ORDER BY       
    sum(data_length+index_length) DESC;

Tableのサイズ

SELECT  
    table_name, engine, table_rows AS tbl_rows,
    avg_row_length AS rlen,  
    floor((data_length)/1024/1024) AS dmb,
    floor((index_length)/1024/1024) AS imb
FROM 
    information_schema.tables  
WHERE
    table_schema=database()  
ORDER BY
    (data_length+index_length) DESC;  

どんなクエリが書かれているか確かめる

https://github.com/tenntenn/isucontools

queryを出してくれる
みるべきはupdateとinsertが走っているテーブル
それ以外は不変なので楽にキャッシュができる(再起動に注意)

top

メモリやCPU使用量をみる
top -s 4
とか良さそう
memoryは
top -s 1 -o mem
でソートできるそう

余計なこと編

  • httpsならhttp2, http3にすることで高速化できる

コード編

再起動対策

	dsn := fmt.Sprintf(
		"%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=Local",
		user,
		password,
		host,
		port,
		dbname,
	)

	dbx, err = sqlx.Open("mysql", dsn)
	if err != nil {
		log.Fatalf("failed to connect to DB: %s.", err.Error())
	}
	defer dbx.Close()

	for {
		err := dbx.Ping()
		if err == nil {
			break
		}
		log.Println(err)
		time.Sleep(time.Second * 1)
	}

pprof

import (
    _ "net/http/pprof"
)
	go func() {
		log.Println(http.ListenAndServe("0.0.0.0:6060", nil))
	}()

Discussion