アプリケーションエンジニアがISUCON7の予選を解いてみた その2

前回のポストは以下をご参照下さい。

前回画像のバイナリーをMySQLに突っ込んでいる箇所がヤバそうということが分かったため、今回はそこの改善からやっていきます。

画像をDBアクセスせずに返す

以下の記事を参考にしながらISUCON7の予選突破チームがどういう戦略を取ったのか確認しました。

blog.bitjourney.com

ざっと見た感じだとfujiwara組さんの webサーバーに画像ファイルを保存して画像がなければDBにアクセスする という戦略が一番コストが少なく実装できそうなのでその戦法を採用。

beatsync.net

ちなみに現在の環境だとホストの/iconsをサーバーがマウントしているため、1つのサーバーでiconsをアップロードすると自動的にすべてのサーバーで同じ画像を参照できるようになるはずです。

そうすると必ずローカルファイルがヒットするため、余計なパフォーマンスゲインがあるはず。ただ今回は学習が主目的なのため、その辺りは見なかったことにしておきます。

DBから画像を抜き出す

以下の変更で起動時にMySQLから画像を抜き出し、icons配下へ保存する変更を加えます。

github.com

getIconでローカルの画像ファイルを返す

アプリケーションのコードを変更して,はじめにローカルで画像を検索するように変更します。

github.com

この辺は以下のコードほぼそのまま。

github.com

postProfileでローカルに画像ファイルを保存する

postProfileでアップロードした画像をローカルへ保存するように変更します。

github.com

この辺も以下のコードほぼそのまま。

github.com

ここまででスコアは 52828 まで上昇しました。

現状のボトルネックをもう一度把握し直す

vmstat

app01 (webサーバー)

isucon@78041320129c:~/isubata$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
24  4 103056 522844  46264 243300    2   34   792   633 1817 5224  7  9 83  1  0
13  0 103056 515344  46708 243388    0    0    52  2416 21887 52629 35 66  0  0  0
30  0 103056 512984  47108 244908    0    0    44  2892 20290 58955 38 62  0  0  0
42  0 103056 511828  47380 244936    0    0     0  1444 15136 67755 57 44  0  0  0
46  0 103056 512360  47712 244640    0    0     0  1724 17967 61763 39 62  0  0  0
27  1 103056 508080  48056 245948    0    0     4  2344 20889 63859 48 52  0  0  0
42  5 103056 506900  48364 245316    0    0    12  1504 20968 62356 39 62  0  0  0
39  0 103056 502560  48688 246260    0    0   260  1452 21662 62227 42 59  0  0  0
44  0 103056 501072  48992 246584    0    0     0  1492 17674 68371 53 47  0  0  0
39  0 103056 495348  49312 246536    0    0     8  1312 20131 61326 44 57  0  0  0
32  0 103056 489940  49620 246988    0    0     0  1248 19318 56272 49 51  1  0  0
44  2 103056 485044  49900 246768    0    0     0  1068 19536 55937 41 59  0  0  0
36  0 103056 481352  50228 247532    0    0     4  1436 19397 58469 48 52  0  0  0
39  0 103052 479432  50500 247636    0    0     0  1112 18378 63232 46 54  0  0  0
45  0 103052 472420  50812 247488    0    0     0  1060 18072 53886 35 65  0  0  0
29  1 103052 469852  51124 247856    0    0     0  1240 18979 56133 40 60  0  0  0
43  1 103052 469072  51432 247544    0    0     4  1424 18615 66641 45 55  0  0  0
35  0 103052 469168  51720 248268    0    0     0  1100 19934 62102 41 59  1  0  0
45  0 103052 468000  52056 248368    0    0     0  1308 21721 70621 37 64  0  0  0
35  0 103052 467772  52380 248664    0    0     4  1232 18308 63425 46 55  0  0  0
38  0 103052 467248  52708 248848    0    0     0  1212 20266 64809 41 59  0  0  0

app03 (dbサーバー)

isucon@ad3bfb718b03:~/isubata$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
40  1 103076 726992  26404 216824    2   36   843   630 1514 4259  6  7 86  1  0
48  0 103076 720664  26764 217648    0    0     8  2644 21197 67151 42 58  0  0  0
45  1 103076 717708  27128 219444    0    0     4  2100 18611 65462 52 48  0  0  0
35  0 103076 712372  27476 220976    0    0    16  2496 22080 63306 42 59  0  0  0
33  1 103076 705160  27964 221184    0    0   112  2284 24161 56237 30 67  2  1  0
41  0 103076 700180  28292 221188    0    0    70  1172 21565 65008 42 59  0  0  0
41  0 103076 694624  28656 221208    0    0     4  1192 21318 63835 44 56  0  0  0
43  0 103076 689028  29044 222596    0    0     4  4032 18573 71293 52 49  0  0  0
39  1 103076 682212  29412 222556    0    0    16  3040 23174 67367 36 64  0  0  0
31  0 103076 673816  29728 223524    0    0    12  3348 23139 64012 43 57  0  0  0
41  0 103076 668700  30032 224448    0    0     8  2992 19105 66637 46 55  0  0  0
40  1 103076 662992  30348 225036    0    0     4  2788 19637 69112 43 57  0  0  0
44  0 103076 661076  30636 224876    0    0     0   872 20857 55790 40 60  1  0  0
32  1 103076 659488  30984 225540    0    0    20  1036 23152 65758 39 61  0  0  0
33  0 103076 657004  31296 225744    0    0     0   976 21149 63253 42 58  0  0  0
40  0 103076 655972  31616 225904    0    0    12  1040 19495 69417 47 54  0  0  0
33  0 103076 646500  31928 226716    0    0    12  1544 20629 57917 44 55  1  0  0
34  1 103076 645540  32256 226756    0    0     8  1048 21868 66012 39 61  0  0  0
34  0 103076 644352  32568 227280    0    0     0   964 22850 72506 44 56  0  0  0

myprofiler

##  2019-02-10 05:32:19.35 +0000
 488 SELECT COUNT(*) as cnt FROM message WHERE channel_id = ?
 378 SELECT * FROM haveread WHERE user_id = ? AND channel_id = ?
 276 SELECT name, display_name, avatar_icon FROM user WHERE id = ?
  76 INSERT INTO message (channel_id, user_id, content, created_at) VALUES (?, ?, ?, NOW())
  32 SELECT * FROM message WHERE id > ? AND channel_id = ? ORDER BY id DESC LIMIT N
  29 SELECT * FROM user WHERE id = ?
  14 INSERT INTO haveread (user_id, channel_id, message_id, updated_at, created_at) VALUES (?, ?, ?, NOW(), NOW()) ON DUPLICATE KEY UPDATE message_id = ?, updated_at = NOW()
   3 SELECT COUNT(*) as cnt FROM message WHERE channel_id = ? AND ? < id
   1 SELECT * FROM message WHERE channel_id = ? ORDER BY id DESC LIMIT ? OFFSET ?
   1 SELECT id FROM channel

pprof

f:id:shuheiktgw:20190210144114p:plain
pprof Frame Graphe

getIcon メソッドのチューニングで気持ち良いまでに SELECT name, data FROM image WHERE name = ?ボトルネックではなくなったことが分かる。

一方で以前としてDBの負荷は高いため引き続きDB周りのチューニングが必要そう。

pprofを見ると fetchUnread メソッドの負荷が高く、myprofilerを見ると SELECT COUNT(*) as cnt FROM message WHERE channel_id = ?SELECT * FROM haveread WHERE user_id = ? AND channel_id = ? という2つのクエリのサンプル数が多い。

fetchUnread メソッドを確認するとたしかにこの2つのクエリが呼ばれていることが確認できます。次はfetchUnread メソッドをチューニングしていきたいと思います。

追記

実際にはechoのプロセスの再起動がうまくいっていませんでした... ↑の値は恐らく本当の値とは異なります...

fetchUnread のN+1を解消する

fetchUnread はユーザーの未読メッセージ数チャネルごとに集計して返すメソッド。チャネルIDごとにループをかけて以下の作業を行っているため、N+1が発生している。

  1. havreadテーブルからユーザーがそのチャネルでを開いた際の最新メッセージのIDを取得する
  2. messageテーブルからユーザーが最後に読んだメッセージ以降のIDのメッセージを取得する

パット見、messageテーブルからhavereadの差集合をとってchannel_idでgroup_byすればいいのではと思えてしまいますが、havereadテーブルはユーザーがそのチャネルを開いた際の最新メッセージのIDが入っているだけ(読んだすべてのメッセージが入っているわけではない)なのでその戦略は取れない。

ここは様々な選択肢がありますが、なるべくシンプルに以下の方法を採用します。

  1. channelテーブルに総メッセージ数を表すmessage_countカラムを追加する
  2. ユーザー * チャネルの既読メッセージ数を保持するread_countテーブルを作成する
  3. 未読数は message_count - read_count で取得する

この辺はスギャブロエックスさんの戦略を参考にしました。

message_countカラムの追加

alter table channel add message_count bigint unsigned default 0 not null; を実行してmessage_countカラムを追加する。

起動時にmessage_countをセットする変更

サーバー起動時にmessage_countを集計してセットするため、initMessageCount メソッドを追加します。

github.com

一点、私の環境では make start/app/go を実行しても上記のChannelInfo構造体の変更がうまく反映されませんでした。

echoのプロセスをきちんと殺してから出ないとうまく反映されないようなので、app01とapp02のプロセスを必ず殺してからmake start/app/go を実行するようにしたほうが良さそうです。

message投稿時にmessage_countを更新する変更

上の変更に合わせて今度はメッセージ投稿時にmessage_countカラムを更新する変更を加えます。

github.com

unreadをmessage_countとread_countの差分から取得するよう変更

create table read_count(channel_id bigint not null, user_id bigint not null, count bigint default 0 not null, primary_key(channel_id, user_id)); を実行してread_countテーブルを追加します。

また、下記の変更でunreadの値を message_count - read_count で取得できるように変更します。

github.com

ここまででスコアは 29537. echoのプロセスの再起動に失敗していた前回よりも下がってしまった...

ボトルネックの確認

vmstat

app01 (webサーバー)

isucon@78041320129c:~/isubata$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
41  2 211060 113192  68244 589800    0    1    25    26  129   52  0  1 99  0  0
44  2 211060 107412  68688 590348    0    0   164  1832 9276 21543 22 78  0  0  0
22  2 211060 104332  68980 591288    0    0    12  1244 8980 23578 27 73  1  0  0
51  0 211060  99060  69240 592212    0    0    12  1356 10100 24555 18 82  0  0  0
35  1 211060  97884  69592 590504    0    0    40  2256 11248 27079 27 73  0  0  0
35  1 211060  95060  69888 591760    0    0    24  1188 10560 24428 26 73  1  0  0
37  1 211060  92900  70128 591508    0    0     0  1036 10952 27629 25 75  0  0  0
23  3 211060  89236  70344 591220    0    0    16   860 10163 26372 22 78  0  0  0
46  0 211060  83464  70708 591068    0    0    28  1936 10844 29331 25 74  1  1  0
28  1 211060  81460  70924 590736    0    0    16   928 9565 25634 24 75  2  0  0
23  1 211060  77088  71240 590388    0    0    44  1424 9865 24277 25 74  1  0  0
43  1 211060  68280  71560 590576    0    0    24  1712 7960 19626 24 74  1  2  0
43  1 211564  70276  70368 584732    0  504   136  2976 10624 25587 26 73  1  0  0

app03 (dbサーバー)

isucon@ad3bfb718b03:~/isubata$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
26  0 246720  70460  12292 277964    0    2    27    27  133   63  0  1 98  0  0
35  0 246708  67508  12592 279092    8    0    12  2836 11041 26477 25 72  2  1  0
34  1 246852  77388  12328 269712    8  152   260  3816 10359 25888 25 75  0  0  0
31  0 246848  76024  12592 270912    0    0   144  3028 10650 25552 30 67  3  1  0
39  0 246848  72980  12876 271512    0    0   536  2832 9669 25179 29 71  1  0  0
25  2 247972  90360  12052 254780    4 1128    36  3996 8338 20401 23 74  2  1  0
38  1 247972  90492  12396 254768    0    0    24  3740 10414 24218 25 74  1  1  0
40  1 247972  84800  12752 257264    0    0    36  3652 10597 23771 31 68  0  1  0
42  1 247972  81048  13116 259752    0    0    12  2952 11236 30319 30 69  1  0  0
41  0 247972  73808  13392 261816    4    0   116  3000 10608 24685 22 78  0  0  0
25  1 247972  70020  13740 262728    0    0     4  2584 10891 27299 34 66  0  0  0
48  1 247968  67796  14004 262452    4    0     4   976 9167 25268 29 71  1  0  0
37  1 248136  73760  13608 253980    8  176    16  1360 8364 23565 24 76  0  0  0
40  2 248136  69236  13992 254152    0    0     0  1268 8915 25059 22 78  0  0  0
46  1 248136  66532  14232 254784    0    0   184   900 9999 24032 31 68  0  1  0
40  0 248664  86572  13440 233640    0  528     4  1572 9601 23854 19 80  1  0  0
34  2 248664  81040  13812 234672    0    0   124  1076 9558 23042 29 71  0  1  0
30  1 248664  71728  14060 237064    0    0     4   972 9178 23529 21 77  1  0  0
41  3 248660  68808  14400 238792    4    0    16  1944 8613 24236 31 69  0  0  0
42  1 249072  87220  13700 219072    0  412    12  1720 9105 23071 33 66  0  1  0
38  1 249072  75220  14080 223628    0    0    12  2564 9466 23631 32 68  0  0  0
35  1 249072  74384  14356 224320    0    0   712  2576 9613 23116 27 73  0  0  0
41  0 249072  70108  14720 227956    0    0   128  3044 10245 26519 23 77  1  0  0
41  1 249072  70044  15096 227036    0    0    48  1804 10282 27513 26 74  0  0  0
41  0 249072  66196  15328 230052   12    0   872  1108 9906 24017 29 67  2  2  0

Frame Graphe

f:id:shuheiktgw:20190212110045p:plain
pprof Frame Graphe

fetchUnread の負荷がガクッと下がって、getMessage の負荷が相対的に目立ってきました。そこで次は getMessage をチューニングしたいと思います。

長くなったので、今回はここまで。