🙋‍♂️

ISUCON13 初参加してきました

2023/11/30に公開

はじめに

フリマアプリでアプリケーションエンジニアとして働いているKatsumi-Nと申します。初投稿です!
今回 ISUCON13に一人で初参加してきたのでやったことをメモしたいと思います。

結果

9504点で302位でした!

準備

ISUCON本(https://gihyo.jp/book/2022/978-4-297-12846-3)を一通りやりました。
実務ではRailsを使用しているのでGoの文法を復習したりもしました。

やったこと

覚えている限り時系列で書きます
基本方針としては

  1. alpでリソースごとのボトルネックを見る
  2. pt-query-digestでスロークエリを見る
  3. ボトルネックっぽいところに取り組む
  4. ベンチ回す
    という流れで取り組みました。

インデックスを貼る 4,900

livestream_tags などにインデックスを貼りました

アイコン画像をnginxで配信 5,200

nginxをこんな感じに設定し

code
# ユーザーアイコンのリクエストを処理するlocationブロック
  location ~ ^/api/user/([-a-z0-9]+)/icon$ {
    # 静的ファイルが存在するか確認
    root /home/isucon/webapp/public/;
    try_files /image/$1.jpeg @proxy_to_app;
  }

  # 静的ファイルが見つからない場合のリバースプロキシ設定
  location @proxy_to_app {
    proxy_set_header Host $host;
    proxy_pass http://localhost:8080;
  }

postIconHandlerで画像が投稿されたときには画像ファイルに書き出し

code
tmpfile, err := os.CreateTemp("", "example")
defer os.Remove(tmpfile.Name())
if err != nil {
	log.Print(err)
}
if _, err := tmpfile.Write(req.Image); err != nil {
	log.Print(err)
}
if err := tmpfile.Close(); err != nil {
	log.Print(err)
}
imgfile := fmt.Sprintf("/home/isucon/webapp/public/image/%d.%s", userID, "jpeg")
err = os.Rename(tmpfile.Name(), imgfile)
if err != nil {
	log.Print(err)
	return echo.NewHTTPError(http.StatusInternalServerError, "failed to write new user icon: "+err.Error())
}

 // 静的ファイルの削除
iconFilePath := fmt.Sprintf("/home/isucon/webapp/public/image/%s.jpeg", user.Name)
if err := os.Remove(iconFilePath); err != nil {
	// ファイルが存在しない場合のエラーは無視する
	if !os.IsNotExist(err) {
		return echo.NewHTTPError(http.StatusInternalServerError, "failed to delete old user icon file: "+err.Error())
	}
}

err = os.Chmod(imgfile, 0644)
if err != nil {
	log.Print(err)
	return echo.NewHTTPError(http.StatusInternalServerError, "failed to chmod new user icon: "+err.Error())
}

getIconHandlerでsqlが発行された際にもその画像を書き出す処理を追加しました

code
// アイコンを取得する際に書き出す
imgfile := fmt.Sprintf("/home/isucon/webapp/public/image/%d.%s", user.ID, "jpeg")
err = os.WriteFile(imgfile, image, 0644)
if err != nil {
	log.Print(err)
	return echo.NewHTTPError(http.StatusInternalServerError, "failed to write icon: "+err.Error())
}

画像取得するSQLを直接ファイルを見に行くように変更 7,600

fillUserResponseの中でSELECT image FROM icons WHERE user_id = ?が叩かれていたので画像ファイルを見に行くように修正しました

code
iconFilePath := fmt.Sprintf("/home/isucon/webapp/public/image/%s.jpeg", userModel.Name)
image, err := os.ReadFile(iconFilePath)
if err != nil {
	// ファイルが存在しない場合のエラーは無視する
	if !os.IsNotExist(err) {
		return User{}, err
	}

	if err := tx.GetContext(ctx, &image, "SELECT image FROM icons WHERE user_id = ?", userModel.ID); err != nil {
		if !errors.Is(err, sql.ErrNoRows) {
			return User{}, err
		}
		image, err = os.ReadFile(fallbackImage)
		if err != nil {
			return User{}, err
		}
	}
}

N+1の解消 9500

fillUserResponse内でN+1があったので解消しました
(他にもいじった記憶がありますが割愛します)

code
userModel := UserModel{}
if err := tx.GetContext(ctx, &userModel, "SELECT users.*, themes.id AS `theme.id`, themes.dark_mode AS `theme.dark_mode` FROM users JOIN themes ON users.id = themes.user_id WHERE users.name = ?", username); err != nil {
	if errors.Is(err, sql.ErrNoRows) {
		return echo.NewHTTPError(http.StatusNotFound, "not found user that has the given username")
	}
	return echo.NewHTTPError(http.StatusInternalServerError, "failed to get user: "+err.Error())
}

/statisticsエンドポイントの改善 終わらず😇

alpのコマンドが間違っておりしばらくここのボトルネックに気づきませんでした...
ユーザごとの配信統計情報を出すエンドポイントですが重いSQLが複数発行されていたのでこれを改善しようとしたところで終了してしまいました

おわりに

  • 1万点超えたかった〜〜🥺
    ちゃんと素振りします
  • CI整備!
    alp, pt-query-digestを自動で実行するgithub actionsを準備すべきでした
  • 来年は複数人で出たいです🫣

Discussion