Y

ISUCON12予選の復習をしました 2

この記事は何か?

ISUCON12の復習記録です。

第2回はISUCON12 予選の解説 (Node.jsでSQLiteのまま10万点行く方法)の「2 Ranking APIが重いのでひとまずループクエリをなくす」を試します。

目次

  1. 環境構築 (alp, pporf)
  2. 「2 Ranking APIが重いのでひとまずループクエリをなくす」を試す

1. 環境構築 (alp, pporf)

alpとpporfをインストールします。alpのインストール手順は過去に書いたので省略します。

  • pporfインストール
go install runtime/pprof
go install net/http/pprof

vi isuports.go

-----------------
+
+       "runtime"
+       _ "net/http/pprof"


@@ -137,6 +140,12 @@ func Run() {
        e.Debug = true
        e.Logger.SetLevel(log.DEBUG)

+       runtime.SetBlockProfileRate(1)
+       runtime.SetMutexProfileFraction(1)
+       go func() {
+               http.ListenAndServe("0.0.0.0:6060", nil)
+       }()
-----------------

make
sudo systemctl restart isuports

sudo apt install graphviz

セキュリティグループの設定で1080を開放

go tool pprof -http=0.0.0.0:1080 /home/isucon/webapp/go  http://localhost:6060/debug/pprof/profile?seconds=90

http://xxx.xxx.xxx.xxx:1080/にアクセス
  • alp結果
alp --sum -r -f /var/log/nginx/access.log | head -100
+-------+--------+--------+---------+--------+--------+--------+--------+--------+-----------+------------+-------------+-----------+--------+-----------------------------------------------+
| COUNT |  MIN   |  MAX   |   SUM   |  AVG   |   P1   |  P50   |  P99   | STDDEV | MIN(BODY) | MAX(BODY)  |  SUM(BODY)  | AVG(BODY) | METHOD |                      URI                      |
+-------+--------+--------+---------+--------+--------+--------+--------+--------+-----------+------------+-------------+-----------+--------+-----------------------------------------------+
|    18 |  2.764 | 16.020 | 189.032 | 10.502 |  2.764 | 12.688 | 15.708 |  5.091 |     0.000 |  14439.000 |  245022.000 | 13612.333 | GET    | /api/player/competition/9fa538f9/ranking      |
|    16 |  0.140 | 18.724 | 183.480 | 11.467 |  0.140 | 17.804 | 18.588 |  8.684 | 13563.000 |  13636.000 |  217081.000 | 13567.562 | GET    | /api/player/competition/9fa53e35/ranking      |
|    31 |  0.016 | 17.592 | 135.380 |  4.367 |  0.016 |  0.156 | 17.464 |  5.416 | 13650.000 |  13741.000 |  423394.000 | 13657.871 | GET    | /api/player/competition/9fa52cc0/ranking      |
|    27 |  0.028 | 17.632 | 132.436 |  4.905 |  0.028 |  0.192 | 10.464 |  5.561 | 13654.000 |  13708.000 |  368743.000 | 13657.148 | GET    | /api/player/competition/9fa53286/ranking      |
|    20 |  0.012 | 10.188 | 119.448 |  5.972 |  0.012 |  9.972 | 10.156 |  4.851 |     0.000 |  13730.000 |  260867.000 | 13043.350 | GET    | /api/player/competition/9fa53ed0/ranking      |
|    38 |  0.008 | 18.508 | 116.096 |  3.055 |  0.008 |  0.044 | 17.724 |  4.939 |   912.000 |   3439.000 |  124093.000 |  3265.605 | GET    | /api/player/competition/9fa5278c/ranking      |
|    19 |  0.000 |  9.648 | 105.476 |  5.551 |  0.000 |  9.428 |  9.620 |  4.686 |     0.000 |  13633.000 |  244237.000 | 12854.579 | GET    | /api/player/competition/9fa53fc1/ranking      |
|    23 |  0.020 |  7.888 |  99.780 |  4.338 |  0.020 |  6.480 |  7.888 |  3.786 | 13571.000 |  13721.000 |  312385.000 | 13581.957 | GET    | /api/player/competition/9fa5325a/ranking      |
|    26 |  0.020 |  7.624 |  98.900 |  3.804 |  0.020 |  0.252 |  7.620 |  3.710 |     0.000 |  13727.000 |  325444.000 | 12517.077 | GET    | /api/player/competition/9fa537bf/ranking      |
|    34 |  0.016 |  7.572 |  95.484 |  2.808 |  0.016 |  0.076 |  6.908 |  3.116 | 13457.000 |  13669.000 |  458558.000 | 13487.000 | GET    | /api/player/competition/9fa52a27/ranking      |
|    28 |  0.020 |  8.356 |  87.816 |  3.136 |  0.020 |  5.228 |  7.048 |  2.963 | 13555.000 |  13682.000 |  379750.000 | 13562.500 | GET    | /api/player/competition/9fa52efc/ranking      |
|    33 |  0.924 | 15.380 |  84.456 |  2.559 |  0.000 |  1.804 |  6.764 |  2.576 |     0.000 |   1565.000 |   46830.000 |  1419.091 | GET    | /api/admin/tenants/billing                    |
|    17 |  0.076 |  8.368 |  80.968 |  4.763 |  0.076 |  7.756 |  8.232 |  3.897 | 13639.000 |  13653.000 |  231922.000 | 13642.471 | GET    | /api/player/competition/9fa546cf/ranking      |
|    16 |  0.072 |  7.352 |  79.784 |  4.986 |  0.072 |  7.128 |  7.320 |  3.299 | 13686.000 |  13686.000 |  218976.000 | 13686.000 | GET    | /api/player/competition/9fa54745/ranking      |
|    29 |  0.012 |  4.060 |  44.864 |  1.547 |  0.012 |  0.048 |  4.052 |  1.923 | 11049.000 |  14504.000 |  416088.000 | 14347.862 | GET    | /api/player/competition/9fa533be/ranking      |
|    28 |  0.012 |  3.320 |  36.696 |  1.311 |  0.012 |  0.036 |  3.316 |  1.593 |  2908.000 |  12357.000 |  330278.000 | 11795.643 | GET    | /api/player/competition/9fa53adb/ranking      |

GET /api/player/competition/:competition_id/rankingが遅いことが分かります。

  • pprof結果

tenantDB.SelectContextが4.89秒、retrievePlayerが3.14秒かかっていて、retrievePlayerはいわゆる「N+1問題」になっていることが分かります

2. 「2 Ranking APIが重いのでひとまずループクエリをなくす」を試す

mysqlがちょっと落ち着いたら、alpでリクエストごとの総レスポンスタイム(SUM)を見てみると、rankingが重いことが分かります。flockしているのも気になりますが、ロック区間内でループクエリが発生しているのがマズいので、まずはplayer_scoreを取るところにplayerをJOINして、retrievePlayerせずともdisplay_nameを取れるようにします。

解説ではSQLiteのロックは一旦そのまま放置して、N+1問題の解決をしています。

+type PlayerScoreJoinPlayerRow struct {
+       TenantID      int64  `db:"tenant_id"`
+       ID            string `db:"id"`
+       PlayerID      string `db:"player_id"`
+       CompetitionID string `db:"competition_id"`
+       Score         int64  `db:"score"`
+       RowNum        int64  `db:"row_num"`
+       CreatedAt     int64  `db:"created_at"`
+       UpdatedAt     int64  `db:"updated_at"`
+       DisplayName   string `db:"display_name"`
+}

- "SELECT * FROM player_score WHERE tenant_id = ? AND competition_id = ? ORDER BY row_num DESC",
+ "SELECT player_score.*, player.display_name FROM player_score JOIN player ON player.id = player_score.player_id WHERE player_score.tenant_id = ? AND competition_id = ? ORDER BY row_num DESC",

/*
  p, err := retrievePlayer(ctx, tenantDB, ps.PlayerID)
  if err != nil {
    return fmt.Errorf("error retrievePlayer: %w", err)
  }
*/
ranks = append(ranks, CompetitionRank{
  Score:             ps.Score,
  PlayerID:          ps.PlayerID,
  PlayerDisplayName: ps.DisplayName,
  RowNum:            ps.RowNum,
})
  • 変更後のpprof結果

N+1問題は解決しました。しかしスコアは2972と改善していません。解説にあるとおりflockを解決しないとスコア改善しないようです。

感想

今見ると分かりやすいN+1問題ですが、予選当日は対応できませんでした。アプリやコードの仕様理解をさくっとできるようになる必要があります。

また、対応できたとしてもスコアが改善しないので、「それはそれとして、改善を重ねていこう」というゆるい気持ちが必要そうです。

参考

1 ISUCON12 予選の解説 (Node.jsでSQLiteのまま10万点行く方法)