インフラのパフォチュー
2021-08-29
はじめに
どうも、僕です。
先日、ISUCON に出場して、学生枠で6位、5位と2000点差で惜しくも本戦出場を逃しました。(チームメンバーに助けられまくってたので僕はなにもしてませんが)
普段はフロントエンドの実装をメインとしてやっているのですが、最近仕事で Docker や k8s 周りを触ることが多く、少しサーバサイドやインフラについて少し興味が出てきたときに出場した ISUCON だったので以前よりもだいぶサーバサイドやインフラに興味が湧いてきました。
これまであまりここら辺に積極的に触れることがなかったのですが(というよりそこまで経験が多くないから触ったことがなかっただけ)、興味が出てきたのでパフォーマンスチューニングどんな感じにするんだろうみたいなことを書きます。
インフラのパフォーマンスを上げるには
いろいろありますが、基本的には同時に大量のリクエストを捌くことがメインになるので効率よく、並列で、分散して処理を行う必要があります。
負荷を調べる
エンドポイントごとの負荷を調べるツールがあるみたいです。(一緒に出た同期に教えてもらった)
alpというやつらしく、これがめちゃくちゃ便利でした。
例えば今年の ISUCON だと以下のような形になっていました。(どの時点だか忘れた)
| COUNT | 1XX | 2XX | 3XX | 4XX | 5XX | METHOD | URI | MIN | MAX | SUM | AVG | P90 | P95 | P99 | STDDEV | MIN(BODY) | MAX(BODY) | SUM(BODY) | AVG(BODY) |
|-------|-----|-------|-----|------|-----|--------|------------------------------------------------------------------------------|-------|-------|---------|-------|-------|-------|-------|--------|------------|------------|--------------|------------|
| 70985 | 0 | 66658 | 0 | 4327 | 0 | POST | /api/condition/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ | 0.012 | 0.136 | 620.396 | 0.009 | 0.016 | 0.100 | 0.100 | 0.026 | 0.000 | 14.000 | 112.000 | 0.002 |
| 335 | 0 | 125 | 0 | 210 | 0 | GET | /api/isu | 0.244 | 1.008 | 241.508 | 0.721 | 1.000 | 1.000 | 1.004 | 0.385 | 0.000 | 10000.000 | 333516.000 | 995.570 |
| 859 | 0 | 779 | 0 | 80 | 0 | GET | /api/isu/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/icon$ | 1.540 | 1.540 | 169.452 | 0.197 | 0.668 | 1.000 | 1.000 | 0.276 | 0.000 | 135259.000 | 14695211.000 | 17107.347 |
| 295 | 0 | 207 | 0 | 88 | 0 | GET | /api/condition/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ | 0.040 | 3.000 | 73.560 | 0.249 | 0.632 | 0.908 | 1.000 | 0.312 | 0.000 | 7243.000 | 956315.000 | 3241.746 |
| 169 | 0 | 104 | 0 | 65 | 0 | GET | /api/isu/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/graph$ | 0.028 | 1.952 | 43.876 | 0.260 | 0.876 | 1.000 | 1.080 | 0.329 | 0.000 | 6875.000 | 465450.000 | 2754.142 |
| 52 | 0 | 12 | 0 | 40 | 0 | GET | /api/trend | 0.020 | 1.004 | 41.616 | 0.800 | 1.000 | 1.004 | 1.004 | 0.363 | 8204.000 | 8204.000 | 65442.000 | 1258.500 |
| 178 | 0 | 138 | 0 | 40 | 0 | GET | /api/isu/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ | 1.000 | 2.304 | 23.268 | 0.131 | 0.368 | 0.668 | 1.100 | 0.273 | 0.000 | 154.000 | 19532.000 | 109.730 |
| 173 | 0 | 68 | 0 | 105 | 0 | POST | /api/auth | 0.004 | 2.716 | 10.956 | 0.063 | 0.104 | 0.192 | 1.456 | 0.260 | 0.000 | 19.000 | 1196.000 | 6.913 |
| 55 | 0 | 51 | 0 | 4 | 0 | POST | /api/isu | 0.004 | 0.124 | 3.604 | 0.066 | 0.088 | 0.092 | 0.124 | 0.022 | 15.000 | 154.000 | 7021.000 | 127.655 |
| 64 | 0 | 29 | 0 | 35 | 0 | GET | /api/user/me | 0.000 | 0.508 | 1.764 | 0.028 | 0.092 | 0.124 | 0.508 | 0.084 | 21.000 | 43.000 | 1823.000 | 28.484 |
| 36 | 0 | 21 | 0 | 15 | 0 | POST | /api/signout | 0.000 | 0.300 | 0.912 | 0.025 | 0.068 | 0.216 | 0.300 | 0.060 | 21.000 | 21.000 | 315.000 | 8.750 |
| 120 | 0 | 53 | 67 | 0 | 0 | GET | /assets/vendor.d5e1a410.js | 0.000 | 0.020 | 0.244 | 0.002 | 0.004 | 0.008 | 0.016 | 0.004 | 743417.000 | 743417.000 | 39401101.000 | 328342.508 |
| 120 | 0 | 53 | 67 | 0 | 0 | GET | /assets/favicon.d0f5f504.svg | 0.000 | 0.016 | 0.208 | 0.002 | 0.004 | 0.004 | 0.012 | 0.003 | 592.000 | 592.000 | 31376.000 | 261.467 |
| 1 | 0 | 1 | 0 | 0 | 0 | POST | /initialize | 0.196 | 0.196 | 0.196 | 0.196 | 0.196 | 0.196 | 0.196 | 0.000 | 23.000 | 23.000 | 23.000 | 23.000 |
| 120 | 0 | 52 | 68 | 0 | 0 | GET | /assets/index.f8c2722b.js | 0.000 | 0.016 | 0.184 | 0.002 | 0.004 | 0.004 | 0.012 | 0.003 | 0.000 | 26685.000 | 1387620.000 | 11563.500 |
| 120 | 0 | 52 | 68 | 0 | 0 | GET | /assets/index.144d8ca8.css | 0.000 | 0.016 | 0.148 | 0.001 | 0.004 | 0.004 | 0.012 | 0.003 | 0.000 | 19066.000 | 991432.000 | 8261.933 |
| 98 | 0 | 82 | 16 | 0 | 0 | GET | / | 0.000 | 0.012 | 0.128 | 0.001 | 0.004 | 0.008 | 0.012 | 0.003 | 0.000 | 528.000 | 43296.000 | 441.796 |
| 120 | 0 | 52 | 68 | 0 | 0 | GET | /assets/logo_white.svg | 0.000 | 0.016 | 0.108 | 0.001 | 0.004 | 0.004 | 0.012 | 0.002 | 0.000 | 3285.000 | 170820.000 | 1423.500 |
| 52 | 0 | 51 | 1 | 0 | 0 | GET | /assets/logo_orange.svg | 0.000 | 0.020 | 0.104 | 0.002 | 0.004 | 0.008 | 0.020 | 0.004 | 0.000 | 3288.000 | 167688.000 | 3224.769 |
| 30 | 0 | 10 | 20 | 0 | 0 | GET | /isu/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/condition$ | 0.000 | 0.004 | 0.012 | 0.000 | 0.000 | 0.004 | 0.004 | 0.001 | 0.000 | 528.000 | 5280.000 | 176.000 |
| 10 | 0 | 10 | 0 | 0 | 0 | GET | /isu/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/graph$ | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 528.000 | 528.000 | 5280.000 | 528.000 |
| 3 | 0 | 1 | 2 | 0 | 0 | GET | /register | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 528.000 | 528.000 | 176.000 |
| 10 | 0 | 10 | 0 | 0 | 0 | GET | /isu/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 528.000 | 528.000 | 5280.000 | 528.000 |
エンドポイントごとに項目が出されていて、それぞれのリクエストの最小から最大や、合計などについて出してくれます。
1つ1つのリクエストはそこまで負荷がかかってないけどたくさん飛んできてるから合計が大きくなってるみたいなところは削る必要がありますし、そういうのを可視化するのはとても大事だなと思いました。
INDEX をはる
INDEX はデータベースの検索のパフォーマンスを上げるために必要不可欠なものですが、ちょっと舐めてた部分がありました。
個人的にデータベースで INDEX を貼る前後のベンチマークをとったことがなく、速くなるんだな〜くらいで思っていました。
今回の ISUCON では `jia_user_id` 、`jia_isu_uuid` が至る所で使われてることを一緒に出た同期が見つけてくれて INDEX を貼ってくれたところ、スコアが15000点くらい?あがったのですげえってなりました。
ただ、INDEX を貼ることによって場合によっては INSERT が遅くなるということもあるのでそこら辺は注意する必要がありそうです。
あまりアルゴリズムとか知らなかったのでDBスペシャリスト試験の学習サイトにあった [データベース性能を向上させる「インデックス」を理解する](https://atmarkit.itmedia.co.jp/ait/articles/1703/01/news199.html) を読んで勉強をしました。
N+1 問題をつぶす
N+1 問題も INDEX と同じくらい重要かつ当然のように認知されています。
今回の ISUCON ではデータを取ってきて、そのデータを使用して for ループでさらにクエリを発行してる箇所が2箇所あり、その両方をつぶすことはできませんでしたが、そこをつぶせていればもう少しスコアが上がったのではないかなと思います。終了後に他の参加者の方でそこを解決してるコードを読んでほえ〜ってなりました。来年こそ潰します。
bulk insert
これは複数のデータをいっぺんに INSERT するものです。
ドキュメントは [こちら](https://docs.microsoft.com/ja-jp/sql/t-sql/statements/bulk-insert-transact-sql?view=sql-server-2017) 。
例えば以下の SQL があるとき、
INSERT INTO hoge(fuga) values('a');
INSERT INTO hoge(fuga) values('b');
INSERT INTO hoge(fuga) values('c');
bulk insert では1度で処理を行うことができます。
INSERT INTO hoge(fuga) values('a'), ('b'), ('c');
このように書くと、`トランザクションを発行する→INSERT する→コミットする` の回数が減るのでコンテキストスイッチの回数が減り処理が速くなります。人間と同じですね。
INSERT を行う処理がいっぺんに飛んでくる場合や、短時間に大量のリクエストが来る場合は bulk insert をすることで処理を軽くすることができます。
処理を分散
今回はサーバが3つ与えられていたので、1つはデータベースのサーバ、もう2つはアプリケーションのサーバとして利用しました。
また、その2つのアプリケーションサーバも分散し、リクエストが多かった `/api/condition/` とそれ以外でリクエストの飛ばすサーバを変えたことでだいぶ処理が楽になったような気がします。
nginx 周りは全部同期がやってくれたのでそこらへんのコードも後から見返してとても勉強になりました。
まとめ
これだけではないかもしれませんが、だいたいこんな感じのことをするだけでだいぶパフォーマンスが改善されます。
基本的にはネットワーク I/O や CPU 使用率などどこに行ってもボトルネックになるものは変わらない気もするのですが、フロントエンドのパフォーマンスチューニングとの違いは一度にたくさんの処理を捌く必要があるかどうかという部分にあると思いました。(当たり前)
今回で全く知らなかったことが減ったので、少しずつ勉強していきます。
今回自分はあまり戦力になれなかったのでこの一年で力をつけて来年はもっと戦えるように頑張ります!!
ISUCON に参加した方お疲れ様でした。一緒に出てくれた二人ありがとう。
おしまい。