---
# System prepended metadata

title: ISUCON9 予選

---

# ISUCON9 予選

[TOC]

## レギュレーション
https://isucon.net/archives/53555007.html

## マニュアル
https://github.com/isucon/isucon9-qualify/blob/master/docs/manual.md

## 接続情報
```
Host isubench
	HostName 13.113.185.7
	User ubuntu
	IdentityFile ~/.ssh/isucon7q-sshkey.pem
	ServerAliveInterval 60

Host isucon1
	HostName 35.72.103.176
	User ubuntu
	IdentityFile ~/.ssh/isucon7q-sshkey.pem
	ServerAliveInterval 60
  
Host isucon2
	HostName 35.73.89.21
	User ubuntu
	IdentityFile ~/.ssh/isucon7q-sshkey.pem
	ServerAliveInterval 60

Host isucon3
	HostName 35.76.49.129
	User ubuntu
	IdentityFile ~/.ssh/isucon7q-sshkey.pem
	ServerAliveInterval 60
```

## 内部IP
- isucon1: 172.31.45.45
- isucon2: 172.31.43.113
- isucon3: 172.31.34.49

## スプレッドシート
https://docs.google.com/spreadsheets/d/1mwPr_hABe4BZlAj6ZXEWs5krmSfZvZrIevQKDNnjnzs/edit?usp=sharing

## 事前
- [ ] Elastic IPを登録しておく
- [ ] AWSインスタンス作成時にダウンロードした鍵を共有しておく
    - isucon-sshkey.pemで~/.sshに保存
    - chmod 600 ~/.ssh/isucon-sshkey.pem
- [ ] ~/.ssh/configの設定
- [ ] isucon[X]-qualifyレポジトリを空で作成しておく
    - `git@github.com:isucon-kuolc38th/isucon(X)-qualify.git`
<!--     - テンプレート準備
        - local
        - pprof
        - results
        - tools
        - .gitignore
        - docker-compose.yml
    - （各自）クローンし、ローカルで諸々のセッティングをしておく
        - VSCode
        - Github Desktop
    - (要検討)これらはサーバー内に用意されている方を踏襲したほうがよさそう?
        - mysql
        - nginx
        - env.sh -->
- [ ] isucon[X]-qualify-utilレポジトリを作成し、各自Pullして動作確認を済ませておく
    - `git@github.com:isucon-kuolc38th/isucon(X)-qualify-util.git`


## 初期構成
- isucon1
    - Nginx
    - App
- isucon2
    - 何もなし
- isucon3
    - (Redis)
    - MySQL

## AWS
- [ ] EC2でインスタンス作成→マシンイメージ選択で指定されたAMIを選択
- [ ] インスタンスタイプを選択
- [ ] ボリュームはSSDで16GB（インスタンスタイプによって変わるかも）
- [ ] セキュリティグループの設定
    - 「すべてのTCP」、ソースに「0.0.0.0/0」を指定
    - 「すべてのICMP」、ソースに「0.0.0.0/0」を指定
    - セキュリティグループはあらかじめ作成しておいたものを使うのが良い
- [ ] キーペアにisucon-sshkeyを指定
- [ ] インスタンスが起動したら、Elastic IPのページからインスタンスに事前に用意したElastic IPを紐付け
- [ ] それぞれの名前をつける

## タイムスケジュール
|timetable|all|rkoike|cojiro|ryunosuke|
|--|--|--|--|--|
|00:00 競技開始|||||
||マニュアル確認<br>初期ベンチ回す（レコード数確認）<br>アプリ触ってみる<br>レポジトリPush・各自クローン||||
|00:20 作業開始|||||
|||レポジトリPull<br>マシンセットアップ<br>Nginx（ログ含む）<br>MySQL（ログ含む）<br>App (GOインストール)<br>netdata|AppのDB接続設定<br>Appのエラーログ設定<br>measure設定<br>pprof設定|ALPのグルーピング<br>コードリーディング<br>仕様の整理<br>ボトルネックの洗い出し|
||（ここまでの目標）<br>スクリプトからデプロイできるようにする<br>ベンチを回してmeasure/ALPを吐き出せるようにする<br>ローカルでアプリが動くようにする<br>ボトルネックの洗い出し・タスク分割||||
|01:30 改善開始|||||
||仕様・ボトルネックの共有と把握<br>タスクの割り振り||||
|||Nginxの設定チューニング<br>Staticファイルのキャッシュ設定<br>ベンチマーク分析<br>インフラ最適化|MySQLの設定チューニング<br>インデックス作成<br>SQLクエリ改善|メインボトルネック解消<br>影響範囲の広いApp改善<br>変更のないデータのオンメモリ化|
|05:00 メインボトルネック解消|||||
|||ベンチマーク分析<br>インフラ最適化<br>時間のある時に再起動試験|影響範囲の狭いApp改善|使用頻度の高いデータのオンメモリ化|
|07:00 提出準備|||||
||再起動試験|netdata無効化<br>MySQLログ無効化<br>Nginxログ無効化|measure無効化|安全な（？）App改善<br>ボーダーが遠い場合にワンチャンかける|

## 作業マニュアル
### マニュアル確認

### ベンチマーク

### Git
#### SSH Key登録
``` bash
mkdir ~/.ssh
cd ~/.ssh && ssh-keygen
cat id_rsa.pub
```

- [ ] isucon[X]で名前をつける
- [ ] Write権限にチェックを入れること

#### 初回Push
- [ ] SSH Key登録

``` bash
cd ~/<apphome>
git config --global user.name "isucon[1-3]"
git init
git remote add origin git@github.com:isucon-kuolc38th/isucon[X]-qualify.git
# 必要に応じて.gitignoreをいい感じに編集
git add .
git commit -m "initial setup"
git push origin master
```

#### 初回Pull
- [ ] SSH Key登録
``` bash
cd ~
mv <apphome> <apphome>_backup
git clone git@github.com:isucon-kuolc38th/isucon[X]-qualify.git
git config --global user.name "isucon[1-3]"
```

フォルダをマージする場合
`rsync -av isucari_repo/ isucari/`

### Nginx
#### インストール
- [ ] `sudo apt install nginx`

#### セットアップ
- [ ] 設定をgit管理下にコピー
``` bash
cd ~/<apphome>
sudo mkdir /etc/nginx_backup
sudo cp -r /etc/nginx /etc/nginx_backup
mkdir -p ./isucon[1-3]/nginx/config
sudo cp -r /etc/nginx/* ./isucon[1-3]/nginx/config
```
- [ ] 初期実装サービスの停止
``` bash
# Apache
sudo apachectl stop
sudo service httpd stop

# h2o
sudo systemctl stop h2o
sudo systemctl disable h2o
```
- [ ] appのIPアドレス設定
```
http {
    upstream app {
        server app:8080;
    }
    
    location / {
        proxy_set_header Host $http_host;
        proxy_pass http://app;
    }
}

```

- [ ] ログの設定
``` nginx.conf
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";

    access_log  /var/log/nginx/access.log ltsv;
    error_log /var/log/nginx/error.log;
}
```
- [ ] Nginxサービスの`start`と`enable`

#### 設定のチューニング
```
worker_processes auto; # ワーカーの数。基本はautoでOK
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
worker_rlimit_nofile 4096; # nginxの開けるファイルとコネクションを合わせた数。too many file opensになった場合は引き上げる

error_log  /var/log/nginx/error.log error;

events {
	worker_connections 1024; # nginxの開けるコネクションの数。too many file opensになった場合は引き上げる。worker_rlimit_nofile以上にはできない
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    server_tokens off;
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    client_max_body_size 10m;

	open_file_cache max=100 inactive=65s; # ファイルを開きっぱなしにする。ファイルの読み込みが頻繁に発生する場合に有効
	gzip_static on;

	access_log off;

    # TLS configuration
    ssl_protocols TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';

    # リクエストのロードバランシング
	upstream app {
		server 127.0.0.1:8000 weight=2;
		server 172.24.83.44:8080 weight=7;
		server 172.24.83.45:8080 weight=1;
        keepalive 10; # バックエンドへの接続を繋ぎっぱなしにできる。接続数が多い場合に有効
	}

	server {
	    listen 443 ssl http2; # http2の有効化
	    server_name www.example.com;

	    ssl_certificate /etc/nginx/ssl/fullchain.pem;
	    ssl_certificate_key /etc/nginx/ssl/privkey.pem;

		root /home/isucon/isucari/webapp/public;
		location /static/ {
			add_header Cache-Control "public max-age=86400"; # staticファイルのキャッシュ設定
		}

	    location / {
            # keepaliveを有効にする場合に設定？
            proxy_http_version 1.1;
            proxy_set_header Connection ""; 

            proxy_pass http://app;
    		proxy_set_header Host $host;
	    }
	}
}
```

### MySQL
#### インストール
``` bash
sudo apt install mysql-server mysql-client
```

#### セットアップ
- [ ] 設定をgit管理下にコピー
``` bash
cd ~/<apphome>
sudo mkdir /etc/mysql_backup
sudo cp -r /etc/mysql /etc/mysql_backup
mkdir -p ./isucon[1-3]/mysql/config
sudo cp -r /etc/mysql/* ./isucon[1-3]/mysql/config
```

- [ ] ログの設定
    - 設定が効いているか確認: `SHOW VARIABLES LIKE "general_log%";`
``` my.cnf
[mysqld]
log_output=FILE
general_log=1
general_log_file=/var/log/mysql/query.log
slow_query_log=1
slow_query_log_file=/var/log/mysql/slow_query.log
log_error=/var/log/mysql/error.log
long_query_time=1
log_queries_not_using_indexes=1
```

- [ ] MySQLサービスの`start`と`enable`
``` bash
sudo systemctl start mysql [mariadb]
sudo systemctl enable mysql [mariadb]
```

- [ ] (要検討)初期実装がmysql以外の時のスキーマやデータのコピー方法


#### データのdump
``` bash
mysqldump -u user db > data.sql
mysqldump -u user --no-data db > schema.sql
sftp isucon[1-3] /home/isucon/data.sql ./
sftp isucon[1-3] /home/isucon/schema.sql ./
```

#### 設定のチューニング
```
[mysqld_safe]
socket		= /var/run/mysqld/mysqld.sock
nice		= 0

[mysqld]
user		= mysql
pid-file	= /var/run/mysqld/mysqld.pid
socket		= /var/run/mysqld/mysqld.sock
port		= 3306
basedir		= /usr
datadir		= /var/lib/mysql
tmpdir		= /tmp
lc-messages-dir	= /usr/share/mysql
skip-external-locking
bind-address		= 0.0.0.0 # 外部から接続可能にする
key_buffer_size		= 16M 
max_allowed_packet	= 16M
thread_stack		= 192K
thread_cache_size       = 8
myisam-recover-options  = BACKUP
max_connections        = 1024
table_open_cache       = 64
thread_concurrency     = 10
query_cache_limit	= 1M
query_cache_size    = 16M

log_error = /var/log/mysql/error.log
slow_query_log		= 1
slow_query_log_file	= /var/log/mysql/mysql-slow.log
long_query_time = 0.5
log-queries-not-using-indexes = 1
```

#### インデックス最適化
- WHERE ORはUNIONにする
```sql
SELECT * FROM items WHERE seller_id = ? OR buyer_id = ?
--------------------------------------------------
SELECT * FROM items WHERE seller_id = ?
UNION
SELECT * FROM items WHERE buyer_id = ?
```

### systemd
`/etc/systemd/*`を編集し，appで`<appdir>/isucon[1-3]/env.sh`を読むようにする

### App
#### セットアップ
- [ ] Go最新版に入れ替え
``` bash
cd ~/local
wget https://golang.org/dl/go1.16.5.linux-amd64.tar.gz
rm -rf go
tar -xzf go1.16.5.linux-amd64.tar.gz
rm go1.16.5.linux-amd64.tar.gz 
```

- [ ] PATHを通す
    - `export PATH=$PATH:$HOME/local/go/bin`
    - `vim ~/.bashrc`をして`PATH=`に`$HOME/local/go/bin`を追記

- [ ] `<apphome>/isucon[1-3]/env.sh`を読むように設定
``` bash
cd ~/<apphome>
touch ./isucon[1-3]/env.sh
sudo vim /etc/systemd/system/<service_name>
# [Service]
# EnvironmentFile=<apphome>/isucon[1-3]/env.sh
```
- [ ] 実装切り替え
    - 他の言語実装を`stop`と`disable`
    - Golang実装を`start`と`enable`

### netdata
``` bash
bash <(curl -Ss https://my-netdata.io/kickstart.sh) all --dont-wait
```

### measure
https://github.com/najeira/measure

インストール
```
go get github.com/najeira/measure
```

使い方
```go
import "github.com/najeira/measure"

// 測定
func hoge() {
    defer measure.Start("hoge").Stop()
}

// リセット
measure.Reset()

// 結果取得用ハンドラ (echoの場合)
e.GET("/stats", echo.WrapHandler(http.HandlerFunc(measure.HandleStats)))
```

`/stats`にアクセスし、結果を右クリック>stats.csvで保存

### pprof
使い方
https://christina04.hatenablog.com/entry/golang-pprof-basic

```go
import "net/http/pprof"

// echo
e.Group("/debug/pprof").Any("/cmdline", echo.WrapHandler(http.HandlerFunc(pprof.Cmdline)))
e.Group("/debug/pprof").Any("/profile", echo.WrapHandler(http.HandlerFunc(pprof.Profile)))
e.Group("/debug/pprof").Any("/symbol", echo.WrapHandler(http.HandlerFunc(pprof.Symbol)))
e.Group("/debug/pprof").Any("/trace", echo.WrapHandler(http.HandlerFunc(pprof.Trace)))
e.Group("/debug/pprof").Any("/*", echo.WrapHandler(http.HandlerFunc(pprof.Index)))

// goji
mux.HandleFunc(pat.Get("/debug/pprof/cmdline"), pprof.Cmdline)
mux.HandleFunc(pat.Get("/debug/pprof/profile"), pprof.Profile)
mux.HandleFunc(pat.Get("/debug/pprof/symbol"), pprof.Symbol)
mux.HandleFunc(pat.Get("/debug/pprof/trace"), pprof.Trace)
mux.HandleFunc(pat.Get("/debug/pprof/*"), pprof.Index)
```

ベンチマーク中に、稼働しているサーバーに対してローカルで次を実行
``` bash
// CPU
curl -s http://<ip>/debug/pprof/profile?seconds=60 > cpu.pprof

// メモリ
curl -s http://<ip>/debug/pprof/heap?seconds=60 > memory.pprof
```

ローカルで結果表示
``` bash
go tool pprof -http=localhost:8080 *.pprof 
```

Webでの閲覧にはgraphvizのインストールが必要
``` bash
brew install graphviz
```

### ローカル環境
#### セットアップ
- [ ] isucon-templateのdocker-compose.ymlのコメントアウトを編集
    - [ ] appのフォルダ
    - [ ] DBの環境変数
    - [ ] MySQLの変数
- [ ] local/publicにpublicをコピー
- [ ] local/nginx/config/nginx.confを編集
    - [ ] appのupstream
    - [ ] staticファイル
- [ ] local/mysql/initの各SQLファイルを編集
    - [ ] 00_setup.sqlにユーザー作成処理を書く
    - [ ] 01_schema.sqlにスキーマを書く

### キャッシュ

### ベンチマーク分析

### 再起動試験対策
#### AppのDB接続
``` go
for {
    var err error
    db, err = sql.Open("mysql", dsn)
    if err == nil && db.Ping() == nil {
        break
    }

    log.Println(err)
    time.Sleep(time.Second * 3)
}
```

#### initialize
``` go
func initialize() {
    ClearCache()
    
    // DBロード処理
}

func getInitialize() {
    ...
    initialize()
}

func main() {
    ...
    initialize()
}
```

### 掃除
- [ ] MySQLログ無効化
- [ ] Nginxログ無効化
- [ ] Appログ無効化
- [ ] Redisログ無効化
- [ ] netdata無効化
    - `systemctl stop & disble`
- [ ] measure削除
    - `github.com/najeira/measure`をインポートから外して関連コードを削除

<!-- ## 以下未整理（項目ごとに上の作業マニュアルに入れていく）
- [ ] 与えられたディレクトリ構成に合わせてこの資料を編集
    - (要検討)3台で別々のamiインスタンスが渡された場合のテンプレ

## 当日やること(ツール類の整備)
### 計測ツール検討中
- [ ] AWS X-Ray


## その他のツールやコマンド
### alp
``` bash
wget https://github.com/tkuchiki/alp/releases/download/v1.0.3/alp_linux_amd64.zip
apt install unzip
unzip alp_linux_amd64.zip
sudo mv alp /usr/local/bin/
rm alp_linux_amd64.zip
```

### 初期実装切り替え
#### init.d
``` bash
sudo chkconfig <service> on/off
sudo service <service> start/stop
```

#### systemd
``` bash
sudo systemctl enable/disable <service>
sudo systemctl start/stop <service>
```

#### supervisord
vim /etc/supervisord.conf 
```
[<service>]
...
autostart: true/false
```

```
supervisorctl restart
```

### 更新 & ベンチ
``` bash
cd <apphome>
git pull origin master

ps -aux | grep app
sudo kill <pid>

cd app && make build
make run &

cd ~/webapp && ./tools/benchmark.sh
# Running benchmark...

vim results/initial_score # TODO: 自動化

git add *
git commit -m "initial_score"
git push origin master
```
 -->
## Trouble Shooting
### Nginxで権限問題
- 迷ったら`nginx.conf`にはrootユーザーを書くのが無難
- 「13: Permission denied」[参考リンク](https://itips.krsw.biz/nginx-fail-port-forward-13-permission-denied/)

``` bash
sudo setsebool httpd_can_network_connect on -P
sudo chmod 701 /home/isucon
sudo systemctl restart nginx
```

- 「SELinuxの無効化」[参考リンク](https://qiita.com/hanaita0102/items/5d3675e4dc1530b255ba)
``` bash
# 一時的に無効化
setenforce 0

# 永続的に無効化(要サーバー再起動)
# vi /etc/selinux/config
SELINUX=enforcing
```

### MySQLで権限問題
``` bash
sudo chmod 755 -R /var/log/mysql

# git add できない
sudo chmod 755 -R isucon3/mysql/config
```

### MySQLで外部サーバーから接続できない
https://ameblo.jp/mojeld/entry-12645669164.html
``` my.cnf
# bind-address		= 127.0.0.1
bind-address		= 0.0.0.0
```


### Redisで権限問題
```
sudo chown redis:adm -R /var/log/redis
sudo chmod 755 /var/log/redis
```

### サーバー間通信でno route to host
- pingは通るがcurlは通らない
```
sudo systemctl stop firewalld
sudo systemctl disable firewalld
```

### デプロイコマンドで権限問題
```
sudo chmod 777 <apphome>/webapp/go
sudo chmod 777 <apphome>/isucon[1-3]/restart.sh
```

### worker_rlimit_nofileをあげてもtoo many open filesが解決しない
``` bash
ulimit -Sn 4096

ps aux | grep nginx
cat /proc/<pid>/limits
```

/etc/systemd/system/multi.target/nginx.service
```
[Service]
LimitNOFILE=65536
```


## やる必要があるかもしれないこと
- [ ] my.cnfの読み込み優先度が高いものが入ってないか確認
    - [ ] ` mysql --help | grep .cnf`の場所にファイル実体があればそれも読み込まれることを意識
    - mysqlコマンドで`defaults-extra-file`オプションがあればそれも入る
    - 候補は以下の通り
        1. `/etc/my.cnf` (git管理済み)
        1. `/etc/mysql/my.cnf` (git管理済み)
        1. `SYSCONFDIR/my.cnf`
        1. `$MYSQL_HOME/my.cnf`
        1. `--defaults-extra-file=path`
        1. `~/.my.cnf`
        1. `~/.mylogin.cnf`


