# ISUCON8予選
## 再現レポジトリ
https://github.com/isucon/isucon8-qualify
## gitレポジトリ
git@github.com:isucon-kuolc38th/isucon8-qualify.git
## レギュレーション
https://github.com/isucon/isucon8-qualify/blob/master/doc/REGULATION.md
## マニュアル
https://github.com/isucon/isucon8-qualify/blob/master/doc/MANUAL.md
## 環境
### ISUCON1
外部IP: 35.74.199.199
内部IP: 172.31.16.197
### ISUCON2
外部IP: 35.75.102.219
内部IP: 172.31.28.214
### ISUCON3
外部IP: 52.197.176.89
内部IP: 172.31.29.67
## 構成
### isucon1
- Nginx
- App (8080)
### isucon2
- 何もなし
### isucon3
- Redis
- MySQL
## ベンチマーカー
```
cd torb/bench
./bin/bench -remotes=35.74.199.199 -output result.json
jq . < result.json
```
## マニュアル読み
- 負荷走行後の確認がある
- 負荷走行後の確認 (適宜: 数秒〜数十秒)
- 一部のエンドポイントの点数がでかい
- **予約( POST /api/events/\d+/actions/reserve ) : 10点/1リクエスト**
- **予約のキャンセル( DELETE /api/events/\d+/sheets/\s/\d+/reservation ) : 10点/1リクエスト**
- **トップページ( GET / ): 5点/1リクエスト**
- **イベント詳細( GET /api/events/\d+ ): 5点/1リクエスト**
- 静的コンテンツ: 1点/100リクエスト
- その他: 1点/1リクエスト
- FAIL条件
- **GET /initialize へのレスポンスが10秒以内に戻らない場合**
- アプリケーション互換性チェックに失敗した場合
- 負荷走行後の確認へのレスポンスがそれぞれ下記の規定秒数以内に戻らない場合
- **POST /admin/api/actions/login: 20秒以内**
- **GET /admin/api/reports/sales: 60秒以内**
- その他、ベンチマーカーのチェッカが失敗を検出したケース
## ローカル
`docker-compose up -d`
データ入れたい時
```
docker exec -it mysql bash
cd docker-entrypoint-initdb.d
./init.sh
```
## エンドポイント
e.Static("/", "public")
e.GET("/", getIndexHandler, fillinUser)
e.GET("/initialize", getIntializeHandler)
e.POST("/api/users", postUserHandler)
e.GET("/api/users/:id", getUserHandler, loginRequired)
e.POST("/api/actions/login", postLoginHandler)
e.POST("/api/actions/logout", postLogoutHandler, loginRequired)
e.GET("/api/events", getEventsHandler)
e.GET("/api/events/:id", getEventHandler)
e.POST("/api/events/:id/actions/reserve", reserveEventHandler, loginRequired)
e.DELETE("/api/events/:id/sheets/:rank/:num/reservation", deleteReservationRankHandler, loginRequired)
e.GET("/admin/", getAdminHandler, fillinAdministrator)
e.POST("/admin/api/actions/login", postAdminLoginHandler)
e.POST("/admin/api/actions/logout", postAdminLogoutHandler, adminLoginRequired)
e.GET("/admin/api/events", getAdminEventsHandler, adminLoginRequired)
e.POST("/admin/api/events", postAdminEventHandler, adminLoginRequired)
e.GET("/admin/api/events/:id", getAdminEventHandler, adminLoginRequired)
e.POST("/admin/api/events/:id/actions/edit", postAdminEditEventHandler, adminLoginRequired)
e.GET("/admin/api/reports/events/:id/sales", getAdminEventReports, adminLoginRequired)
e.GET("/admin/api/reports/sales", getAdminReports, adminLoginRequired)
### エンドポイント
|resouce|method|desc|subfunc|sql|comment|
|-|-|-|-|-|-|
|/initialize|GET|ベンチマークごとの初期化処理。/db/init.shが実行される getInitialize|
|/|GET|ウェルカムページ表示(公開イベント一覧が表示)|
|/api/user|POST|新規登録ページ.同じニックネームは作れない.パスはSHA2でハッシュ|
|/api/actions/login|POST|ログインする.login_nameで検索→ハッシュされたパスと照合|
|/api/actions/loginout|POST|ログアウト|
|/api/users|GET|ユーザーマイページ||SELECT r.*, s.rank AS sheet_rank, s.num AS sheet_num FROM reservations r INNER JOIN sheets s ON s.id = r.sheet_id WHERE r.user_id = ? ORDER BY IFNULL(r.canceled_at, r.reserved_at) DESC LIMIT 5|
|/api/events|GET|全イベント取得.中で各イベントごとにsheetやreservation情報も取得している|
|/api/events/:id|GET|イベント取得.中でsheetやreservation情報も取得している|
|/api/events/:id/actions/reserve|POST|イベントのsheetを一つreserveする.sheetはsql slelect limitで選ばれる|
|/api/events/:id/sheets/:rank/:num/reservation|DELETE|reserveキャンセル.該当reservationをとってきてcanceled_atを更新する|
|resouce|method|desc|subfunc|sql|comment|
|-|-|-|-|-|-|
|/admin/|GET|adminのトップページ。セッションにadminIDがセットされているか確認|getEvents(true)|SELECT id, nickname FROM administrators WHERE id = ?|
|/admin/api/actions/login|POST|adminのログイン。nameで検索してadministratorのID取得||SELECT * FROM administrators WHERE login_name = ?<br/>SELECT SHA2(?, 256)|SHA2はSQLでやる必要なし<br/>administrator取得し直す必要なし|
|/admin/api/actions/logout|POST|adminのログアウト。sessionのID削除|||
|/admin/api/events|GET|イベント一覧取得|getEvents(true)||
|/admin/api/events|POST|イベント作成。引数: (title, public, price)。eventsにinsertしてgetEvent|getEvent(eventID, -1)||
|/admin/api/events/:id|GET|イベント詳細取得|getEvent(eventID, -1)||
|/admin/api/reports/events/:id/sales|GET|イベントのレポート。予約一覧をsold_atの昇順で取得|renderReportCSV(c, reports)|SELECT r.*, s.rank AS sheet_rank, s.num AS sheet_num, s.price AS sheet_price, e.price AS event_price FROM reservations r INNER JOIN sheets s ON s.id = r.sheet_id INNER JOIN events e ON e.id = r.event_id WHERE r.event_id = ? ORDER BY reserved_at ASC FOR UPDATE||
|/admin/api/reports/sales|GET|全体のレポート。予約一覧をsold_atの昇順で取得|renderReportCSV(c, reports)|select r.*, s.rank as sheet_rank, s.num as sheet_num, s.price as sheet_price, e.id as event_id, e.price as event_price from reservations r inner join sheets s on s.id = r.sheet_id inner join events e on e.id = r.event_id order by reserved_at asc for update||
## スキーマ
基本全て,NOTNULL制約あり
#### users
|field|type|desc|
|-|-|-|
|id|int|key incr|
|nickname|string||
|login_name|string|unique_key|
|pass_hash|string||
**UNIQUE KEY login_name_uniq (login_name)**
#### events
|field|type|desc|
|-|-|-|
|id|int|key incr|
|title|string||
|public_fg|tinyint||
|closed_fg|tinyint||
|price|int|notnull|
#### sheets
|field|type|desc|
|-|-|-|
|id|int|key incr|
|rank|string||
|num|int||
|price|int||
**UNIQUE KEY rank_num_uniq (rank, num)**
#### reservations
|field|type|desc|
|-|-|-|
|id|int|key incr|
|event_id|int||
|sheet_id|int||
|user_id|int||
|reservation_at|datetime||
|canceled_at|datetime|default null|
**KEY event_id_and_sheet_id_idx (event_id, sheet_id)**
#### administrators
|field|type|desc|
|-|-|-|
|id|int|key incr|
|nickname|string||
|login_name|string|unique_key|
|pass_hash|string||
**UNIQUE KEY login_name_uniq (login_name)**
## 改善点
- getLoginUser()でuserIDからnicknameを取得するところにredisを使う
- nicknameは一度追加されたら変更されないためすぐできそう
- 最後はgetEvent()を呼ばずにeventを作成できそう
- postAdminEventHandler()
- postAdminEditEventHandler()
-
## 改善済
- sheetsはイベントごとに固定で、DBに入れる意味はなさそう
- 値段 S: 5000, A: 3000, B: 1000, C: 0
- 席数 S: 50, A: 150, B: 300, C: 500
- S-1 -> 1, A-1 -> 51, B-1 -> 201っていうふうに席に一意の番号を振る (= sheet_id) -> sheetsは不要
## 学び
- nginxなど、使うやつはインストールされていない状態からセッティングできるようにしておく(権限周りなど詰まりやすいため)
- CentOSではファイアウォールの設定に気を付ける
- 最初は小池・豊國でインフラのセットアップ、岩井一人でAppのボトルネック解析を進めて直せるところは直しておくのが良いかも?
- エンドポイント一覧は時間かかる割にそんなに役に立たない
- ログ、デプロイの仕組みは今回の形でOK
- MySQLのgeneral logは最初の方はコメントアウトしておく
- 作業分担むずい
- 岩井: Appのコード全部読んで構成・ボトルネックを把握する, 他の人にタスクを振る
- 小池: インフラの構成設定
- 豊國: 状況に応じて2人のヘルプに入る
## 個人 (ryunosuke)
### セットアップ
isucon1
- ssh設定
- ssh-keygen & key登録
- git clone
- mv torb torb_backup
- git clone git@github.com:isucon-kuolc38th/isucon8-qualify.git torb
- 既存App無効化
- torb.perl
- h2o
- mariadb
- goのセットアップ
- 最新版インストール
- /etc/systemd/system/torb.go.service編集
- torb/isucon1/env.shの接続先(app3)編集
- nginxのセットアップ
- yumでインストール
- Permission Denied Trouble Shooting
- sudo setsebool httpd_can_network_connect on -P
- sudo setenforce 0
- sudo chmod 701 /home/isucon
- nginx.confの接続先(app3)編集
- restart
- torb/isucon1/restart.sh
isucon3
- ssh設定
- ssh-keygen & key登録
- git clone
- mv torb torb_backup
- git clone git@github.com:isucon-kuolc38th/isucon8-qualify.git torb
- 既存App無効化
- torb.perl
- h2o
- goのセットアップ
- 最新版インストール
- /etc/systemd/system/torb.go.service編集
- mysqlのセットアップ
- sudo chmod o+r+x /var/log/mariadb
- sudo chmod o+r /var/log/mariadb/*
- restart
- torb/isucon1/restart.sh
isucon-bench
- `cd torb && ./bin/bench -remotes=x.x.x.x -output=result.json`だけで動く
|path|entities|sql|note|
|-|-|-|-|
|/||
|/initialize||
|/api/users||
|/api/users/:id||
|/api/actions/login||
|/api/actions/logout||
|/api/events||
|/api/events/:id||
|/api/events/:id/actions/reserve||
|/api/events/:id/sheets/:rank/:num/reservation||
|/admin/||
|/admin/api/actions/login||
|/admin/api/actions/logout||
|/admin/api/events||
|/admin/api/events||
|/admin/api/events/:id||
|/admin/api/events/:id/actions/edit||
|/admin/api/reports/events/:id/sales||
|/admin/api/reports/sales||