🚅

【ISUCON14対策】private-isuチャレンジ記

2024/11/25に公開

はじめに

2024/12/8(日)に開催のISUCON14へ向けて練習した記録です。

お題は private-isu と呼ばれるISUCON練習問題の中でも、特にメジャーなものを解くことにしました。

結論として、最終スコアは44,2852点でした。ここに辿り着く道のりは険しかったです💦

練習リポジトリ:https://github.com/melanmeg/private-isu-challenge

また、パフォーマンス改善タスクを求められる現場の方々にも、実際に役立つテクニックも恐らくあるはずで、興味のある方、色んな方に読んでいただけると嬉しいです。

改善したことだけ知りたい方は スコア推移 から読むのをオススメします

筆者のISUCON歴

わたしの過去のISUCON歴としては、ISUCON12は最終スコア0ISUCON13では最終スコアが初期スコアを下回り、スコアの出た中で下から3番目という悲惨な経歴を持ってます!w

元々ISUCONにはとても興味があり、モチベーションを高く勉強して挑んできましたが、やはり技術力がまだ未熟で上位スコアには程遠い戦績でした...

今年は、秋に転職して @nwiizoさん @megumish_unsafeさんと出会って、新たなチームとして一緒に ISUCON へ参加いただくことになりました!お二人とも各分野で活躍されてる方で、一緒に参加できることが楽しみです👍

今年の目標

今年の目標は、『上位15%』 です

3人で話し合った結果、もし上位15%を取れなかった場合...

私たちは12月に解散します!!普通の女の子に戻りたいんです!

ソースはこちら ⇒ ニッカン名言集(^。^)
自分が生まれてない時代の名言を引っ張ってきた

というわけで、今年は上位目指すぞっ!とモチベーションをもって、練習に取り組み始めました。

早速、パフォーマンスチューニングした事例を紹介していきます。

環境について

AWS環境構築

自作したCloudFormationで環境を用意しました。
https://gist.github.com/melanmeg/41e5f575b494ca83b7ca8ba76c91cd05

  • 作成されるリソース
インスタンス名 役割
isu1 サーバー1
isu2 サーバー2
isu3 サーバー3
isu-bench ベンチマーカー

AMIはprivate-isu公式で用意されているものを使ってます。

サーバー環境構築

  • 一番初めだけ各サーバーにSSHログイン後、以下コマンド実行する
# SSH後に実行。デプロイキー作成
$ cd ~ && \
  git clone https://github.com/melanmeg/private-isu-challenge.git && \
  mv private_isu private_isu.bk && \
  mv private-isu-challenge private_isu && \
  ssh-keygen -t ed25519 -C "" -f ~/.ssh/id_ed25519 -N "" && \
  sudo apt update -y

# GOROOTのフォルダが空だったので、そのような場合にGoをインストールする
$ sudo rm -rf /usr/local/go && \
  TAR_FILENAME=$(curl 'https://go.dev/dl/?mode=json' | jq -r '.[0].files[] | select(.os == "linux" and .arch == "amd64" and .kind == "archive") | .filename') && \
  URL="https://go.dev/dl/$TAR_FILENAME" && \
  curl -fsSL "$URL" -o /tmp/go.tar.gz && \
  sudo tar -C /usr/local -xzf /tmp/go.tar.gz && \
  rm -f /tmp/go.tar.gz && \
  cat <<EOF >> ~/.bashrc
export GOROOT=/usr/local/go
export GOPATH=$HOME/go
export PATH=/usr/local/go/bin:$PATH
EOF

VSCodeを使ってデバッグなどしたい方は、初めにVSCodeで Go: Install/Update Tools しておく。

Ansibleで構築しています。

ソースコードはここに配置されてます
https://github.com/melanmeg/private-isu-challenge/tree/main/ansible

やっていること

以下ファイルで、どのサーバーにてどのタスクを実行するかを定義してます。
基本、全サーバーに同じ処理を競技開始時に実行させる想定です。

ansible/playbook.yml
ansible/playbook.yml
- name: all
  hosts: all
  gather_facts: false
  user: "{{ ISUCON_USER }}"
  become: true
  max_fail_percentage: 0
  vars_files:
    - env.yml
  tasks:
    - include_tasks: tasks/tools/common.yml
    - include_tasks: tasks/tools/nginx.yml
    - include_tasks: tasks/tools/mysql.yml
    - include_tasks: tasks/tools/memcached.yml
    - include_tasks: tasks/tools/netdata.yml
    - include_tasks: tasks/tools/graphviz.yml
    - include_tasks: tasks/tools/pprotein.yml
    - include_tasks: tasks/tools/sysctl.yml
    - include_tasks: tasks/isu/isu_deploy.yml
    - include_tasks: tasks/isu/isu_challenge.yml

- name: isu1
  hosts: isu1
  gather_facts: false
  user: "{{ ISUCON_USER }}"
  become: false
  max_fail_percentage: 0
  vars_files:
    - env.yml
  tasks:
    - include_tasks: tasks/tools/collect.yml
    - include_tasks: tasks/tools/fetch.yml

やっていることで特に重要なタスクとしては、

  • nginx にアクセスログを設定
  • mysql にslowlog出力、すべてのネットワーク許可設定
  • memcached にすべてのネットワーク許可設定
  • netdata をインストール
  • pprotein-agent をインストール

Ansibleがインストールされているかつ hosts ファイルを用意されている上で、ローカルマシンから以下コマンドで実行ができます。

[isucon]
isu1 ansible_host=x.x.x.x
isu2 ansible_host=x.x.x.x
isu3 ansible_host=x.x.x.x
$ ansible-playbook --key-file ~/.ssh/[Githubに登録した鍵に対する秘密鍵] -i hosts ./playbook.yml -C

以下CIでも実行できるようにしてます
https://github.com/melanmeg/private-isu-challenge/blob/main/.github/workflows/isu.yml

計測について

pproteinとは

pproteinを使ってみました。これは、ISUCONで常勝常連のNaruseJunチームさんが作られたツールで alp, slp, fgprof の計測結果を一元して確認できる便利ツールです。

計測サイクルがかなり楽になるので、本番でも利用するつもりです!

  • 参考記事

https://zenn.dev/team_soda/articles/20231206000000

pprotein、phpMyAdminの用意

これもCloudFormationで、同じネットワーク内に一発で用意できるようにしました。
https://gist.github.com/melanmeg/d90533425d32b87f16b695667b8de141

isu-measure インスタンスが作成されます。

デプロイ後、自動的にpprotein、phpmyadminがdocker-composeで起動する仕組みとなってます。

そして、予め .ssh/configisu-measure を設定しておく。

  • 端末で実行して、ポートフォワーディング
ssh -NL 9000:localhost:9000 isu-measure  # pprotein
ssh -NL 8080:localhost:8080 isu-measure  # phpmyadmin

CloudFormationが完了し、数分~数十分でcloud-init処理(初期起動時の処理)が完了すると、 http://localhost:9000http://localhost:8080 にそれぞれアクセスできます。

pprotein の 画面

groupタブ

事前に pprotein-agent がインストールされているかつ各種ログ出力設定がなされている状態で、「Collect」をクリックすると alp, slp, fgprof の計測が開始されます。

settingタブ

  • group/targets の設定
[
  {
    "Type": "pprof",
    "Label": "webapp",
    "URL": "http://10.1.1.11:6060/debug/pprof/profile",
    "Duration": 60
  },
  {
    "Type": "httplog",
    "Label": "nginx",
    "URL": "http://10.1.1.11:19000/debug/log/httplog",
    "Duration": 60
  },
  {
    "Type": "slowlog",
    "Label": "mysql",
    "URL": "http://10.1.1.11:19000/debug/log/slowlog",
    "Duration": 60
  }
]
  • httplog/config の設定
    ポイントとしては、matching_groupsでエンドポイントごとにグループ化して計測結果を表示することができます。
sort: sum
reverse: true
query_string: true
output: count,5xx,4xx,method,uri,min,max,sum,avg,p99

matching_groups:
  - ^/image/[0-9]+$
  - ^/image/[0-9]+\..+$
  - ^/posts/[0-9]+$
  - ^/posts.*
  - ^/@\w$
  • slowlog/config の設定
    • デフォルトで

phpMyAdmin の 画面

事前にmysql設定を bind-address = 0.0.0.0 などとしてすべてのネットワークを許可しておくことで、プライベートIPの 10.1.1.11 (isu1) にアクセスします。

ISUCONではDBにMySQLがよく使われます。
DBのコマンドを思い出すよりも、自分はUIで確認できる方が圧倒的に手間が省けます。

Go にプロファイル用コードを仕込む

echo, gin, mux, standalone で統合可能。
参考: https://github.com/kaz/pprotein/tree/master/integration

  • echov4の場合
import (
  "github.com/kaz/pprotein/integration/echov4"
)

func main() {
  echov4.EnableDebugHandler(e)
}
  • standaloneの場合
import (
  "github.com/kaz/pprotein/integration/standalone"
)

func main() {
  standalone.Integrate(":6060")
}

CI

Github Actionsでボタンを押すだけでベンチマークを回せるように設定しています。

.github/workflows/bench.yml
name: "Bench"
on:
  workflow_dispatch:
jobs:
  Bench:
    name: "Bench Job"
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Exe
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ vars.HOST_BENCH }}
          username: isucon
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          port: 22
          script: |
            sudo -u isucon /home/isucon/private_isu.git/benchmarker/bin/benchmarker -u /home/isucon/private_isu.git/benchmarker/userdata -t http://${{ vars.HOST1 }}

予めActionsにSSH_PRIVATE_KEY(秘密鍵), HOST_BENCH(ベンチマーカー), HOST1(サーバー1)変数を登録しておいた上で、CI実行するとベンチが走る実装となってます。

.github/workflows/isu.yml でisu1,2,3に対してAnsible実行する定義もあったりします。

スコア推移

基本的に、『達人が教えるWebパフォーマンスチューニング〜ISUCONから学ぶ高速化の実践』いわゆるISUCON本を参考にしながら進めていきました。
https://gihyo.jp/book/2022/978-4-297-12846-3

ISUCON本の実装例はRubyですので、Goで実装できるボトルネックを改善していってます。

1. 初期スコア(4193)

初期状態での slp の結果です。

2. 4193 → 34720

  • commentsテーブルにcomments_idx_1インデックスを追加する

以下クエリが最も遅かったので、EXPLAINで確認してみます。

SELECT * FROM `comments` WHERE `post_id` = N ORDER BY `created_at` DESC LIMIT N
実行計画
mysql> EXPLAIN SELECT * FROM comments WHERE post_id = 100 ORDER BY created_at DESC \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: comments
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 99667
     filtered: 10.00
        Extra: Using where; Using filesort
1 row in set, 1 warning (0.00 sec)

Using where; Using filesort となっており、インデックスで改善できるか考えます。

  1. まずは、post_id カラムに対してインデックスを貼ると Using where が消えました。
    これはISUCON本によると、二分探索で探すようになるため rows: 5 探索が5行で済みます。
alter table comments add index comments_idx_1 (post_id);
実行計画
mysql> EXPLAIN SELECT * FROM comments WHERE post_id = 100 ORDER BY created_at DESC \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: comments
   partitions: NULL
         type: ref
possible_keys: comments_idx_1
          key: comments_idx_1
      key_len: 4
          ref: const
         rows: 5
     filtered: 100.00
        Extra: Using filesort
1 row in set, 1 warning (0.00 sec)
テーブルスキーマ確認
mysql> SHOW CREATE TABLE comments \G
*************************** 1. row ***************************
       Table: comments
Create Table: CREATE TABLE `comments` (
  `id` int NOT NULL AUTO_INCREMENT,
  `post_id` int NOT NULL,
  `user_id` int NOT NULL,
  `comment` text NOT NULL,
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `comments_idx_1` (`post_id`)
) ENGINE=InnoDB AUTO_INCREMENT=100020 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)
  1. 次に Using filesort を改善します。
    こちらもISUCON本曰く「対象の件数(rows)は多くないが、ソート処理はデータベースにとって負担が大きい処理の1つである」とのことです。
    以下のように複合インデックスへと貼り直すと created_at でソートがなされ、 Using filesort が消えました。
alter table comments add index comments_idx_1 (post_id, created_at);
実行計画
mysql> EXPLAIN SELECT * FROM comments WHERE post_id = 100 ORDER BY created_at DESC \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: comments
   partitions: NULL
         type: ref
possible_keys: comments_idx_1
          key: comments_idx_1
      key_len: 4
          ref: const
         rows: 5
     filtered: 100.00
        Extra: Backward index scan
1 row in set, 1 warning (0.01 sec)
  1. 最後に Backward index scan を消します。 これは単にインデックス降順にすれば消えます。
alter table comments add index comments_idx_1 (post_id, created_at DESC);
実行計画
mysql> EXPLAIN SELECT * FROM comments WHERE post_id = 100 ORDER BY created_at DESC \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: comments
   partitions: NULL
         type: ref
possible_keys: comments_idx_1
          key: comments_idx_1
      key_len: 4
          ref: const
         rows: 5
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

消えました!

修正後の計測結果

3. 34720 → 34846

  • 静的ファイルをキャッシュする

webapp/public配下の静的ファイルのキャッシュを試します。

/etc/nginx/sites-avilable/isucon.conf
server {
  location ~ .*\.(ico|css|js|img) {
    expires 1d;
    add_header Cache-Control public;
  }
  ...
}

元々この時点でボトルネックではなかったため、スコアはほぼ変わらず。

修正後の計測結果

nginxでico,css,js,img拡張子はキャッシュする設定を入れることで、その拡張子に対して、alpの結果はアクセス時間は0秒となりました。

4. 34846 → 54217

  • 画像ファイルを取得と同時にすべてディスクに書き出し、/image/* にマッチするリクエストで該当ファイルが存在する場合はそのファイルを返す
Go実装 writeImageToFile()
func writeImageToFile(post Post) error {
	// ディレクトリパスを指定
	dirPath := "../public/images"
	// ディレクトリが存在しない場合は作成
	if err := os.MkdirAll(dirPath, os.ModePerm); err != nil {
		return fmt.Errorf("failed to create directory: %v", err)
	}
	// MIMEタイプに基づいてファイル拡張子を決定
	var ext string
	switch post.Mime {
	case "image/jpeg":
		ext = ".jpg"
	case "image/png":
		ext = ".png"
	default:
		return fmt.Errorf("unsupported mime type: %s", post.Mime)
	}
	// ファイル名をpost IDに基づいて作成
	fileName := fmt.Sprintf("%d%s", post.ID, ext)
	// 完全なファイルパスを生成
	filePath := filepath.Join(dirPath, fileName)
	// ファイルにバイナリデータを書き出す
	err := os.WriteFile(filePath, post.Imgdata, 0644)
	if err != nil {
		return fmt.Errorf("failed to write image to file: %v", err)
	}
	fmt.Printf("Image successfully written to %s\n", filePath)
	return nil
}
Go実装 getImage()
func getImage(w http.ResponseWriter, r *http.Request) {
...
	post := Post{}
	err = db.Get(&post, "SELECT * FROM `posts` WHERE `id` = ?", pid)
	writeImageToFile(post) // ← 追記 取得と同時に画像ファイルは書き出しする
修正後の計測結果

この実装で、/image/* にマッチするリクエストのアクセス時間は、大幅に改善されました。

5. 54217 → 72393

  • posts, users テーブル を JOINして LIMIT 20 で必要最小行数を取得する(N+1の改善)

まず、ホームページの1ページに表示される投稿の数は20件です。

L390のクエリを postsusers を JOINするクエリに変更する

webapp/golang/app.go
- 	err := db.Select(&results, "SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` ORDER BY `created_at` DESC LIMIT 20")
+	err := db.Select(&results, `
+		SELECT p.id, p.user_id, p.body, p.mime, p.created_at
+		FROM posts AS p
+		JOIN users AS u ON (p.user_id = u.id)
+		WHERE u.del_flg = 0
+		ORDER BY p.created_at DESC
+		LIMIT 20
+	`)
実行計画
mysql> EXPLAIN
    -> SELECT p.id, p.user_id, p.body, p.mime, p.created_at
    -> FROM posts AS p
    -> JOIN users AS u ON (p.user_id = u.id)
    -> WHERE u.del_flg = 0
    -> ORDER BY p.created_at DESC
    -> LIMIT 20;
+----+-------------+-------+------------+--------+---------------+---------+---------+-------------------+------+----------+----------------+
| id | select_type | table | partitions | type   | possible_keys | key     | key_len | ref               | rows | filtered | Extra          |
+----+-------------+-------+------------+--------+---------------+---------+---------+-------------------+------+----------+----------------+
|  1 | SIMPLE      | p     | NULL       | ALL    | NULL          | NULL    | NULL    | NULL              | 9791 |   100.00 | Using filesort |
|  1 | SIMPLE      | u     | NULL       | eq_ref | PRIMARY       | PRIMARY | 4       | isuconp.p.user_id |    1 |    10.00 | Using where    |
+----+-------------+-------+------------+--------+---------------+---------+---------+-------------------+------+----------+----------------+
2 rows in set, 1 warning (0.00 sec)

6. 72393 → 82680

  • postsテーブルにposts_idx_1を追加する

先の実行計画でpostsテーブルにて Using filesort となっていたので、ORDER BY でソートしている created_at に対してインデックスを貼ります。

alter table posts add index posts_idx_1 (created_at DESC);
mysql> EXPLAIN SELECT p.id, p.user_id, p.body, p.mime, p.created_at FROM posts AS p JOIN users AS u ON (p.user_id = u.id) WHERE u.del_flg = 0 ORDER BY p.created_at DESC LIMIT 20;
+----+-------------+-------+------------+--------+---------------+---------------------+---------+-------------------+------+----------+-------------+
| id | select_type | table | partitions | type   | possible_keys | key                 | key_len | ref               | rows | filtered | Extra       |
+----+-------------+-------+------------+--------+---------------+---------------------+---------+-------------------+------+----------+-------------+
|  1 | SIMPLE      | p     | NULL       | index  | NULL          | idx_created_at_desc | 4       | NULL              |  199 |   100.00 | NULL        |
|  1 | SIMPLE      | u     | NULL       | eq_ref | PRIMARY       | PRIMARY             | 4       | isuconp.p.user_id |    1 |    10.00 | Using where |
+----+-------------+-------+------------+--------+---------------+---------------------+---------+-------------------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)
修正後の計測結果

Extraが NULLとなり、rowsも 199 になりました。

7. 82680 → 86961

  • commentsテーブルにcomments_idx_2を追加する

先のslowlog結果の内、上位2位のクエリに対して実行計画をします。

mysql> EXPLAIN SELECT COUNT(*) AS `count` FROM `comments` WHERE `user_id` = 382 \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: comments
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 99590
     filtered: 10.00
        Extra: Using where
1 row in set, 1 warning (0.00 sec)
alter table comments add index comments_idx_2 (user_id);
mysql> EXPLAIN SELECT COUNT(*) AS `count` FROM `comments` WHERE `user_id` = 382 \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: comments
   partitions: NULL
         type: ref
possible_keys: comments_idx_2
          key: comments_idx_2
      key_len: 4
          ref: const
         rows: 102
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.00 sec)

rows: 102 となり、 Using where がなくなりました。
Using index は、インデックスに格納されている情報だけでクエリを完結できるという意味のようです。

修正後の計測結果

8. 86961 → 171163

  • N+1クエリ結果と遅いクエリ結果をmemcachedでキャッシュする

先のslowlogで遅いクエリは N+1 になっている実装のクエリになります。
変更差分が大きいため、修正コミットのリンクを貼ります。

https://github.com/melanmeg/private-isu-challenge/commit/65d11687fc1e7048a22a38271894a9e214858672#diff-7fd5e18e2f628f9339bef0c2b540b653031f2c48e8baa324553439eea82324e7L1-R994

slowlog上位2,3位については makePosts 関数内でキャッシュ利用することでループによる実行処理を何度もせずに済むように修正しています。

slowlog上位1位についても、 getPosts 関数内でキャッシュ利用すると少し高速されたのでキャッシュを利用する方針で修正しました。

シリアライズには、 gob というライブラリを使いました。 go-json よりも高速だったのでこちら使いました。

この辺は ChatGPTGithub Copilot を使えばすぐ実装できます。

修正後の計測結果



この時点でのCPU使用率は、ベンチマーク実行中で約45%くらい

修正後時点でhttplogは、他の計測結果に比べて特に遅いなと、今更になぜだろうと気になりました。

恐らくフロント処理が遅いだろうと目星を付け、 ChromePerformance で適当な操作を計測しました。

`Chrome` の `Performance` 計測結果

Layout shift 処理が特に遅いように見えました。
layout shifts は、ウェブページ上の要素がレンダリング後に位置を移動することを指します。

ChatGPT回答





回答を見てもよくわかりませんでしたし、計測方法や本当にボトルネックと言えるのかもちょっと自信ないです💦

また、デザインの変更はISUCONのレギュレーションに引っかかりそうな気もしますが、試しにデザインを変えてでも高速化が見込めるならやってみたいとは感じました。

もし、有識者がいらっしゃれば教えて頂きたいですw

9. 171163 → 179618

  • ログをやめてみた

10. 179618 → 207649

  • グローバルIPでなく内部ネットワークのIPに対してベンチを実行する。nginxにkeepalive設定をする。
/etc/nginx/sites-avilable/isucon.conf
keepalive_requests 10000;

upstream app {
  server localhost:8080;
  keepalive 100;
  keepalive_requests 10000;
}
...
修正後の計測結果

11. 207649 → 437170

  • サーバー分割

以下構成でサーバーを分割すると、一気に43万点に上がりました。
アプリを2つに分散したその分、特に伸びたんだと思います。

  • 構成
ホスト名 ミドルウェア
isu1 nginx、アプリ
isu2 memcached、アプリ
isu3 mysql

12. 437170 → 442852

  • 不要なログ、サービスを止める

やってみた印象について

  • 序盤は各サーバーでそれぞれ調査して、やはりサーバー分割は終盤で良いのかな
  • N+1の改善は、クエリ結果をmemcachedでキャッシュできそうなら、コードを修正して直せる場合でも、キャッシュした方が修正が少ないかつ手早く実装ができそう
  • 画像ファイル書き出しやJOINの改善は、手札として頭に入れておくと良さそう

まとめ

  • private-isuは何度やっても学びがあるとよく言われるようですが、今回も実感できました。
  • INDEXを適切に貼ることで、スコアを上げることにも特に繋がるのかなと思いました。本番でもどのようにINDEXを貼ると良いかを見つけることが重要そうです。
  • private-isu 以外の問題は練習してないけど、この準備したことが結果に繋がることを信じたいです。
  • その他改善余地としては、 nginx, mysql のパラメータ調節、コマンド呼び出し箇所の呼び出しをやめる、静的ファイルの事前zip化、html/template処理をやめる、Decode処理をどうにかする、その他キャッシュが良さそうな処理をキャッシュ化 などがありそうではある。

    ※100万点まで到達されている方もいるので、そこまで改善もできるはず。

最後に

余談です

私が昔こういう記事/リポジトリがあったら助かったなぁという記事を書く意識を持ちました。「親切でシンプルな記事を目指す」とプロフにも書いているので、なるべくそういう記事にはしたいと思ってます。

勉強したての頃はわからないことだらけだったので、内容がシンプルで手順を辿れば動作してくれると助かるのかなと考えます。
自己満ですが、当時の自分の気持ちと同じ方がいて私の記事が意図通りに役立てば幸いです。

話しが逸れましたが、ISUCONがんばってきます!
本番では、最終ベンチマークでスコア 0 にならないことを祈ります🙏

参考

Discussion