# ISUCONチートシートv2022 公開版
今年も3人で出場!!
- 指揮:とーふとふ
- その他:全員
## 開始まで
- 集合: **7月23日(土) 9:00** Discord
## 運用ルール
- GitHubでやる
- ゴリゴリのデプロイスクリプトとミドルウェアの設定ファイルも含める
- ブランチで開発→デプロイ→問題なければmasterマージ
- `make deploy BRANCH=<BRANCH名>` でブランチできる
- 1コミット1目的は守りたい(ロールバック用)
- レビューは不安なときだけ
- PRなし
- ベンチ回すコードは必ずupstreamにコミットが行ってる状態で
- 戻しはリバートで(歴史改変は禁止)
初動は1台のみを想定
## 開始直後(とーふとふ)
<details>
### レギュレーション・当日マニュアルを読む
メモ:
:::spoiler
- 全部読む
- よく読む
- ベンチマークのオプションとかはないか
- 外部APIでAPI制限はないか
- 特定のエンドポイントが高配点になっていないか
- 細かいチューニングポイントはないか
- 過去には適切な商品リスト(好みのジャンルに該当する商品)を返すことで売れやすくなって得点が伸びる事があった
- 別途注意することはないか
:::
### Webページのスクリーンショットを撮る
担当:とーふとふ
:::spoiler
重要なページのレイアウトとかを保存する
メモのページに投げる
:::
### インスタンスの性能などを確認する
Cloud Formationみる
### デプロイスクリプトを作る
```bash=
#!/bin/bash -eux
# 設定ファイルのコピー
# 実行ファイルのビルド
cd /home/isucon/webapp/go
go build -o isucondition
# アプリ・ミドルウェアの再起動
sudo systemctl restart nginx
sudo systemctl restart mariadb
sudo systemctl restart isucondition.go
# slow query logを有効化する
QUERY="
set global slow_query_log_file = '/var/log/mysql/mysql-slow.log';
set global long_query_time = 0;
set global slow_query_log = ON;
"
echo $QUERY | sudo mysql -uroot
# log permission
sudo chmod 777 /var/log/nginx /var/log/nginx/*
sudo chmod 777 /var/log/mysql /var/log/mysql/*
```
### interpolateParamsの有効化
### 実装メモ
</details>
## 開始直後(takashi_trap)
<details>
### 動いている実装をGoに変更する enable/startしていることを確認する
### 最初の状態を保存する
:::spoiler
- ディレクトリ構成
- `./(host名)/`: ホスト毎に設定ファイルを配置。実際のディレクトリ構成と一致させる
- `./app/`: アプリケーションコード
- ミスったときにも復活させることができるようにソースコード一式をGitにおさめてPush
- リバースプロキシやMySQLの設定ファイルも保存
- コピーを取るだけ
- デプロイスクリプト内でcpで配置するようにする
- DB初期化ファイルとかあるならそれも
:::
Makefile
https://github.com/narusejun/isucon11-qualify/blob/master/Makefile
### nginx設定
担当:takashi
:::spoiler
絶対早くなるタイプの設定を入れる
##### 上の方
```
worker_processes auto; # コア数と同じ数まで増やすと良いかも
# nginx worker の設定
worker_rlimit_nofile 4096;
events {
worker_connections 1024; # 128より大きくするなら、 max connection 数を増やす必要あり。さらに大きくするなら worker_rlimit_nofile も大きくする(file descriptor数の制限を緩める)
multi_accept on; # 複数acceptを有効化する
# accept_mutex_delay 100ms;
use epoll; # 待受の利用メソッドを指定(基本は自動指定されてるはず)
}
```
##### ログ周り httpディレクティブの中
```
log_format ltsv "time:$time_local"
"\thost:$remote_addr"
"\tforwardedfor:$http_x_forwarded_for"
"\treq:$request"
"\tmethod:$request_method"
"\turi:$request_uri"
"\tstatus:$status"
"\tsize:$body_bytes_sent"
"\treferer:$http_referer"
"\tua:$http_user_agent"
"\treqtime:$request_time"
"\truntime:$upstream_http_x_runtime"
"\tapptime:$upstream_response_time"
"\tcache:$upstream_http_x_cache"
"\tvhost:$host"; # alp 用の log format
access_log /var/log/nginx/access.log ltsv;
# access_log off;
```
##### 基本設定 httpディレクティブの中
```
# 基本設定
sendfile on;
tcp_nopush on;
tcp_nodelay on;
types_hash_max_size 2048;
server_tokens off;
open_file_cache max=100 inactive=20s; # file descriptor のキャッシュ。入れた方が良い。
# proxy buffer の設定。白金動物園が設定してた。
proxy_buffers 100 32k;
proxy_buffer_size 8k;
# mime.type の設定
include /etc/nginx/mime.types;
# Keepalive 設定
# ベンチマークとの相性次第ではkeepalive off;にしたほうがいい
# keepalive off;
keepalive_requests 1000000;
keepalive_timeout 600s;
http2_max_requests 1000000;
http2_recv_timeout 600s;
# Proxy cache 設定。使いどころがあれば。1mでkey8,000個。1gまでcache。
# proxy_cache_path /var/cache/nginx/cache levels=1:2 keys_zone=zone1:1m max_size=1g inactive=1h;
# proxy_temp_path /var/cache/nginx/tmp;
# 上記を設定した場合、ディレクトリ作成とパーミッション付与が必要かも
# sudo mkdir -p /var/cache/nginx/cache
# sudo mkdir -p /var/cache/nginx/tmp
# sudo chown nginx /var/cache/nginx/cache
# sudo chown nginx /var/cache/nginx/tmp
# オリジンから来るCache-Controlを無視する必要があるなら。。。
#proxy_ignore_headers Cache-Control;
```
##### 静的ファイル serverディレクティブの中
```
# static file の配信用の root
root /home/isucon/webapp/public/;
location ~ .*\.(htm|html|css|js|jpg|png|gif|ico) {
expires 24h;
add_header Cache-Control public;
open_file_cache max=100; # file descriptor などを cache
gzip on; # cpu 使うのでメリット・デメリット見極める必要あり。gzip_static 使えるなら事前にgzip圧縮した上でそちらを使う。
gzip_types text/html text/css application/javascript application/json font/woff font/ttf image/gif image/png image/jpeg image/svg+xml image/x-icon application/octet-stream;
gzip_disable "msie6";
gzip_static on; # nginx configure時に --with-http_gzip_static_module 必要
}
# デバッグ用エンドポイントのタイムアウト対策&&ログ除外
location /debug {
send_timeout 600s;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_connect_timeout 600s;
access_log off;
proxy_pass http://localhost:1323; # !!!ポート注意!!!
}
```
[nginx-backend間でKeepAliveする - road288の日記](https://road288.hatenablog.com/entry/2017/12/13/095408)
```
upstream app {
server 127.0.0.1:3000;
keepalive_requests 1000000;
keepalive 128;
}
```
```
proxy_http_version 1.1; // http1.1する=デフォルトでKeepAliveする
proxy_set_header Connection ""; //nginxはデフォルトでcloseを入れてしまうので空にする
proxy_pass http://app;
```
:::
### MySQLの設定
担当:takashi
:::spoiler
```
[mysqld]
max_connections=1000 # <- connection の limit を更新
innodb_buffer_pool_size = 1GB # ディスクイメージをメモリ上にバッファさせる値をきめる設定値(メモリの75%くらい)
innodb_flush_log_at_trx_commit = 0 # 1に設定するとトランザクション単位でログを出力するが 2 を指定すると1秒間に1回ログを吐く。0だとログも1秒に1回。
innodb_flush_method = O_DIRECT # データファイル、ログファイルの読み書き方式を指定する(実験する価値はある)
innodb_file_per_table=ON # InnoDBのデータ領域をテーブルごとに変える
# innoDBの更新ログを保持するメモリ
innodb_log_buffer_size = 16MB
# InnoDBの更新ログを記録するディスク上のファイルサイズ(innodb_buffer_pool_sizeの4分の1程度)
innodb_log_file_size=250MB
# ORDER BYやGROUP BYのときに使われるメモリ上の領域
innodb_sort_buffer_size = 4MB
read_rnd_buffer_size = 2MB #
key_buffer_size = 256MB # これも可能なら大きくした方が良い
```
#### MySQL8の場合以下を追加する
```
disable-log-bin
innodb_doublewrite = 0
# [MySQL 8.0.22 で innodb_log_writer_threads の効果を見てみる - Qiita](https://qiita.com/hmatsu47/items/06489ef05bfcaaf310f3)
# CPUコアが少ない場合はOFF
innodb_log_writer_threads = off
```
##### logを書き出す場所をtmpfsにすると更に早くなるかも
[ISUCON10予選で12位になり本選進出を決めました - Gマイナー志向](https://matsuu.hatenablog.com/entry/2020/09/13/131145)
```
innodb_log_group_home_dir = /dev/shm/
```
この場合はAppArmorに怒られるらしいので以下の設定を入れる
`/etc/apparmor.d/usr.sbin.mysqld`
```
/dev/shm/ r,
/dev/shm/** rwk,
```
#### その他
[MySQLパフォーマンスチューニング -my.cnfの見直し- - Qiita](https://qiita.com/mamy1326/items/9c5eaee3c986cff65a55)
:::
</details>
## 開始直後(sekai)
<details>
### 事前準備
#### リポジトリ
:::spoiler
- コード用リポジトリ作っておく
- デプロイキーをおいておく
- ISUCONグループに所属させておく
:::
#### 管理用サーバ(HQ)
:::spoiler
- EC2インスタンス立ち上げ
- Ubuntu Server 22.04 LTS (HVM), SSD Volume Type
- t3a.large
- 1x 30GB gp2
- ユーザーデータ
```
#!/bin/bash
mkdir -p /home/ubuntu/.ssh
curl https://github.com/kaz.keys >> /home/ubuntu/.ssh/authorized_keys
chmod 700 /home/ubuntu/.ssh
chmod 600 /home/ubuntu/.ssh/authorized_keys
```
- セキュリティグループ
- SSH
- HTTP
- HTTPS
- IPアドレスを確認して `yaml` を編集
- `dns`で`make apply`
- `inventory/hosts` を確認
- `ansible-playbook playbooks/hq.yml -u ubuntu`
:::
### 当日
#### 環境構築
:::spoiler
- CloudFormationテンプレを落としてきてStackを作成する
:::
#### 競技サーバ
:::spoiler
- IPアドレスを確認して `yaml` を編集
- `dns`で`make apply`
- `inventory/group_vars/all.yml` を編集
- メインプログラムのサービス名
- リポジトリのパス
- `ansible-playbook playbooks/srv.yml -u isucon`
:::
### pproteinの導入
#### お手軽
:::spoiler
- `go get github.com/kaz/pprotein`
- 環境変数 `PPROTEIN_GIT_REPOSITORY=/home/isucon/repo`
##### standalone
```go
package main
import (
"log"
"net/http"
"github.com/kaz/pprotein/integration/standalone"
)
func postInitialize() {
go func() {
if _, err := http.Get("https://pprotein.angelkawaii.com/api/group/collect"); err != nil {
log.Printf("failed to communicate with pprotein: %v", err)
}
}()
}
func main() {
go standalone.Integrate(":8888")
http.ListenAndServe(":8080", nil)
}
```
##### echo(v4)
```go
package main
import (
echoInt "github.com/kaz/pprotein/integration/echov4"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
echoInt.Integrate(e)
e.Start(":8080")
}
```
##### echo(v3)
```go
package main
import (
echoInt "github.com/kaz/pprotein/integration/echo"
"github.com/labstack/echo"
)
func main() {
e := echo.New()
echoInt.Integrate(e)
e.Start(":8080")
}
```
##### gin
```go
package main
import (
"github.com/gin-gonic/gin"
ginInt "github.com/kaz/pprotein/integration/gin"
)
func main() {
r := gin.Default()
ginInt.Integrate(r)
r.Run()
}
```
##### mux
```go
package main
import (
"net/http"
"github.com/gorilla/mux"
muxInt "github.com/kaz/pprotein/integration/mux"
)
func main() {
r := mux.NewRouter()
muxInt.Integrate(r)
http.ListenAndServe(":8080", r)
}
```
:::
#### めんどい方
:::spoiler
`go get github.com/felixge/fgprof`
#### goji
```go
goji.Handle("/debug/pprof/", pprof.Index)
goji.Handle("/debug/pprof/cmdline", pprof.Cmdline)
goji.Handle("/debug/pprof/profile", pprof.Profile)
goji.Handle("/debug/pprof/symbol", pprof.Symbol)
goji.Handle("/debug/fgprof", fgprof.Handler())
```
#### echo
```go
pprofGroup := e.Group("/debug/pprof")
pprofGroup.Any("/cmdline", echo.WrapHandler(http.HandlerFunc(pprof.Cmdline)))
pprofGroup.Any("/profile", echo.WrapHandler(http.HandlerFunc(pprof.Profile)))
pprofGroup.Any("/symbol", echo.WrapHandler(http.HandlerFunc(pprof.Symbol)))
pprofGroup.Any("/trace", echo.WrapHandler(http.HandlerFunc(pprof.Trace)))
pprofGroup.Any("/*", echo.WrapHandler(http.HandlerFunc(pprof.Index)))
e.Any("/debug/fgprof", echo.WrapHandler(fgprof.Handler()))
```
:::
</details>
## 開始直後
### それぞれの手元にリポジトリをクローンしてくる
担当:全員
### 実装の確認
担当:全員
実装メモ:
:::spoiler
- エンドポイントの数
- コールバックでエンドポイントが定義されてたら
- テーブルの数
- テーブルごとにどのエンドポイントで読み取り・書き込みがあるかのまとめ
- initialize時に全部キャッシュできるかも
:::
## 各種情報
### ツール一覧(競技サーバ)
:::spoiler
- [alp](https://github.com/tkuchiki/alp)
```
sudo cat /var/log/nginx/access.log | alp ltsv -m "^/api/schedules/.+$"
```
- [pt-query-digest](https://www.percona.com/doc/percona-toolkit/LATEST/pt-query-digest.html)
```
pt-query-digest /var/log/mysql/mysql-slow.log
```
- [slackcat](https://github.com/bcicen/slackcat)
```
echo -e "hi\nthere" | slackcat --channel general --filename hello
```
- [dstat](https://orebibou.com/ja/home/201606/20160616_001/)
- [htop](https://orebibou.com/ja/home/201605/20160531_001/)
:::
### Makefileの使い方
:::spoiler
- `make deploy`
- pullしてビルドしてサービスを再起動する
- `<hostname>/deploy.sh`にデプロイスクリプトを書く
:::
## 使うかもシリーズ
### 静的ファイルの圧縮配信
:::spoiler
圧縮率はbrotli > zopfli > gunzip > gzipで、Chromeはbrotliに対応してるのでbrotliを利用します。
isucandrはAccept-Encodingを送ってきているのでそれを見てみると良い。
圧縮コマンド
#### gzip
```
$ gzip -9 -kf public/js/*.js public/css/*.css
```
#### zopfli
brewで入る
```
$ zopfli -i500 public/js/*.js public/css/*.css
```
#### brotli
brewで入る
```
brotli -f -9 public/js/*.js public/css/*.css
```
:::
### systemdの環境変数ファイルを読み込んで実行する
:::spoiler
```
$ (set -o allexport; source envfile; target-command)
```
> [systemdのenvfileを普通のコマンド実行時に流用する - たごもりすメモ](https://tagomoris.hatenablog.com/entry/2020/08/18/173225)
`set -o allexport`して`source envfile`すると、シェルに環境変数を設定できる。
:::
### fishに環境変数を設定する
:::spoiler
[【fish shell】環境変数を追加・削除する方法 | Public Constructor](https://public-constructor.com/fish-environment-variable/)
#### 追加
```
set -x KEY VALUE
```
#### 削除
```
set -e KEY
```
:::
### MySQLにユーザーを追加
:::spoiler
`bind-address` を 0.0.0.0に変えることを忘れない。
```
CREATE USER isucon@'%' IDENTIFIED BY 'isucon';
GRANT ALL ON *.* TO isucon;
```
:::
### Unixドメインソケット
:::spoiler
#### Nginx
```conf
upstream app {
server unix:/var/run/app.sock;
}
```
#### echo
```go
// ここからソケット接続設定 ---
socket_file := "/var/run/app.sock"
os.Remove(socket_file)
l, err := net.Listen("unix", socket_file)
if err != nil {
e.Logger.Fatal(err)
}
// go runユーザとnginxのユーザ(グループ)を同じにすれば777じゃなくてok
err = os.Chmod(socket_file, 0777)
if err != nil {
e.Logger.Fatal(err)
}
e.Listener = l
// ここまで ---
```
#### gin
```go
// ここからソケット接続設定 ---
socket_file := "/var/run/app.sock"
os.Remove(socket_file)
l, err := net.Listen("unix", socket_file)
if err != nil {
panic(err)
}
// go runユーザとnginxのユーザ(グループ)を同じにすれば777じゃなくてok
err = os.Chmod(socket_file, 0777)
if err != nil {
panic(err)
}
r.RunListener(l)
// ここまで ---
```
#### goji
```go
// ここからソケット接続設定 ---
socket_file := "/var/run/app.sock"
os.Remove(socket_file)
l, err := net.Listen("unix", socket_file)
if err != nil {
panic(err)
}
// go runユーザとnginxのユーザ(グループ)を同じにすれば777じゃなくてok
err = os.Chmod(socket_file, 0777)
if err != nil {
panic(err)
}
goji.ServeListener(l)
// ここまで ---
```
:::
### シンボリックリンク貼る
:::spoiler
```
cd /isucon/home
ln -s isucon11-quarify/webapp
```
これだけで貼られる
:::
## やったミス
- ポートが空いてない
- Bind Addressが間違っている
- 全角・半角
## なにもない時
- アクセス傾向を調べてみる
- kataribeとかもチェック
- kibana
- myprofiler slowquery
- query統計(phpmyadmin)
- netdata(CPU使用率、メモリ)
- ちゃんとキャッシュ(304)されてる?
- Cache-Control
- Unixドメインソケットにする
- 非同期化できる部分はないか
- DBの非正規化でクエリを減らしたりできないか
- index張れそうだったらはる
- jsonライブラリはgo-json使うといいらしい
- 配列使うときはsync.Pool使うと5〜6%高速化
- [sync.Poolの使い方 - 体はドクペで出来ている](https://dokupe.hatenablog.com/entry/20190501/1556686106)
- [sync.Pool で楽して高速化 - 隙あらば寝る](https://yoru9zine.hatenablog.com/entry/2015/12/05/143414)
- 事前に処理できるものはないか
- メモリが余裕ならGOGCを大きくしてみる
- CPUアフィニティを設定する
- [Core functionality](http://nginx.org/en/docs/ngx_core_module.html#worker_cpu_affinity)
- [tasksetコマンドの使い方 - hana_shinのLinux技術ブログ](https://hana-shin.hatenablog.com/entry/2021/12/23/205413)
- WebフレームワークをFiberに置き換える
- [ISUCON向けfiberメモ - HackMD](https://hackmd.io/0hATEnWPQeeXWk5oxvibVQ)
- DBを分割できないか 水平、垂直
- 各インスタンスの性能に差がないかチェックgola
## ラスト1時間
### コードフリーズする
### 再起動試験をする1
### 各種ログのOFF
- Nginx
- MySQL
- Appの標準出力・標準エラー
### ツール類を落とす
- netdata
- pprotein-agent
- sshのトンネル
- pprof
### 再起動試験をする2