もくじ
スタートダッシュ Source->GitLab->EC2デプロイまで
ISUCONスタートダッシュ GitLab CDによるデプロイができるところまで
ぐぬさんによるまとめ><
0. 事前準備 ・デプロイコマンドを先に準備する ・アプリ再起動、キャッシュ削除など ・チェックリスト用意 1. まずベンチマークを実行する →ベンチマークを実行して、スコアやログはコミット時点なのかも記録 ※MYSQLの設定、nginXの設定などもコミットするように ※gitのタグを使ってスコアを記録するのもある ーーベンチマーク重要ポイントーーーーーーーーー ・細かく書いて1コミットをする。 ・1コミットをして1ベンチぐらいが最高 ・ぶれる可能性がるので2、3回ぐらい実行する ・測定すべき指標 ・CPU占有率の高いプロセスがいないか →htopをベンチ実行しながら確認 ・アクセスログにどんな記録がされているか →alpを利用、LTSVにする巣にぺっとを事前、思いエントポイントから直すAVG高いもの ・データベースに発行されていないくりは何か ・各種リソースはベンチマーク実行中どう使われているか ・CPU、メモリ、帯域、IO ーーーーーーーーーーーーーーーーーーーーーーー 2. マニュアルを1時間見る、以下の項目確認 ・採点ルール ・原点ルール ・ユーザーエージェントの確認 ※クローラ巡回拒否対策&検証方法 https://qiita.com/comefigo/items/1e0265b342c4166ee8bf) 3. サーバーの状態を確認 ・cpu確認:cat /proc/cpuinfo ・メモリ:free -h ・サービス:systemctl list-units --type=service --state=running 4. git管理 ・git init してgit add .&& git commit -m "initial state" 5. 開発 ・アプリのログを残して、ボトルネックとかを解決 6. やれることがないへの対象(何をすればいいかわからないの時) ・スキーマ、アクセスローグ、メトリクス、マニュアルを読み直す ・計測をして情報量を増やす 7. 便利なツールを入れてみる ・netdata, New relic、pprof 8. 掃除しておく ・MySql再起動しておく ・便利なツールを外す(リソースを食う) ・最終ベンチに向けて、アプリに必要なもの以外をきちんと止められるように準備 ・案外忘れられるRACK_ENVなどをちゃんと付ける 9. 他の人のスコアを見る ・解決できる部分をもっと調べる
0-1
- 最初にレギュレーションを3人で
- どんなアプリなのか。
- alp アクセスログ、スロウクエリで遅いクエリ
- EC2
- 評価スクリプト
- 何が原因で加点なのか?減点なのか?
- ルールの穴
・1秒で切れるようにキャッシュするようにするには? - ベンチマークしながら
コミットの点数をまとめていく
・点数高いやつ
・対応できそうなやつ - EC2へデプロイする為のGitの構築
・GitLabにリポジトリ … 今日つくっておいて
・UbuntuとGitLab - Git + SSH + Stashとかでシミュレーション(金広)
alp
- alp nginx
調べておく avg, countでcountが多くて
sum(count * avg) = そうレスポンスタイム
決めること
- チーム名(5分)
・ぐぬぬ!ぱんだちゃんハッカー(仮) - 戦術案(20分)
Go言語
●担当 - 当日までにやること(15分)
・ISUCON
https://github.com/isucon/isucon8-qualify
・H2O
・Goフレームワーク
・gin(岡田)
・Echo(金広)
・go redis cache echo
・毎週チェック
下記は草案
最初にやること
- アプリ全体をざっくり把握する
- スコア評価スクリプトを叩く
- スコア評価スクリプトが判定している内容を把握する
- Google Chromeのデベロッパーツールや、Firefoxのネットワークによって遅いAPIの処理を見つける
キャッシュ is King
- 値が変わらないキャッシュできるAPI
NoSQL RedisによるCacheをする - 値の変更が発生するAPI
・更新時にRedisのCacheをクリアし、再度キャッシュする
GO
- N^2になっているループ処理は修正する必要がある
GOインストール
VS Code プラグインチェック!
- 補完
- 関数ジャンプ
Go言語文法
金広はこれやってる
-
- Go言語からRedisにレスポンス内容をキャッシュ、POSTでの更新時にパージする方法
- 配列 … 要素数が固定 arr := [3]int{2, 3, 5}
- スライス … 要素数が可変 sl := []int{2, 3, 5}
・append() … 要素を追加
・make() … スライスを作成
・len() … 要素数取得
・cap() … 要素数最大取得。容量以上の要素が追加された時にメモリを2倍取得してしまう。メモリを気にするプログラミングで使う。またメモリを過剰に取得すると速度も落ちるので速度パフォーマンスを重視する時に使う。sl := []int{100, 200} fmt.Println(sl) // [100 200] sl = append(sl, 300) fmt.Println(sl) // [100 200 300] sl = append(sl, 400, 500, 600) fmt.Println(sl) // [100 200 300 400 500 600] sl2 := make([]int, 5) // 配列の作成 fmt.Println(sl2) // [0 0 0 0 0] fmt.Println(len(sl2)) // 5 ... 要素数取得 fmt.Println(cap(sl2)) // 5 ... 容量 ... メモリを気にするような時に使う sl3 := make([]int, 5, 10) // ... make([]T, 初期値で埋める要素数, 容量) sl4 := append(sl3, 1, 2, 3, 4, 5, 6, 7) fmt.Println(len(sl4)) // 12 fmt.Println(cap(sl4)) // 20
・range … GO言語にforeachがないのでforとrangeでforeachを実現
sl := []string{"Python", "PHP", "GO"} for i, v := range sl { fmt.Println(i, v) } /* 0 Python 1 PHP 2 GO */
- copy() … PHPのclone()
sla := []string{"PHP", "GO"} slb := sla slb[0] = "Kotlin" fmt.Println(sla) // [Kotlin GO] ... PHPが上書きされてしまった sl := []int{1, 2, 3, 4, 5} sl2 := make([]int, 5, 10) n:= copy(sl2, sl) // sl2にslをコピーして代入。nにはコピーできた要素数が代入される fmt.Println(n, sl2) // 5 [1 2 3 4 5]
-
-
-
- スプレッド演算子 […]
func Sum(s ...int) int { n := 0 for _, v := range s { n += v } return n } func main() { fmt.Println(Sum(1, 2, 10)) // 13 sl := []int{4, 5} fmt.Println(Sum(sl...)) // 9 }
-
- map … 連想配列 … m := map[string]int{“apple”: 100, “banana”: 160}
m := map[string]int{"apple": 100, "banana": 200} for k, v := range m { fmt.Println(k, v) } /* banana 200 apple 100 */ // 要素追加 fmt.Println(m["apple"]) // 100 m["grape"] = 400 fmt.Println(m) // map[apple:100 banana:200 grape:400] // エラーハンドリング _, ok := m["hoge"] // 変数okにはboolが入る。hogeというキーは存在しないのでfalseが代入 if !ok { fmt.Println("error") } // error
- for, range … foreach
sl := []string{"A", "B", "C"} fmt.Println(sl) // [A B C] for i := range sl { fmt.Println(i) // 0 // 1 // 2 } for i, v := range sl { fmt.Println(i, v) // 0 A // 1 B // 2 C } for i := 0; i < len(sl); i++ { fmt.Println(sl[i]) // A // B // C }
- 型アサーション … 型の復元と評価
-
func main() { i := interface{}("hello1") s := i.(string) fmt.Println(s) // hello1 // i2 := interface{}("hello2") // s2 := i2.(int) // fmt.Println(s2) // panic: interface conversion: interface {} is string, not int i3 := interface{}("hello3") n, ok := i3.(int) // 引数を2つ取ることでpanicを防止 fmt.Println(n, ok) // 0 false ... panicにならない s3, ok := i3.(string) fmt.Println(s3, ok) // hello3 true var x interface{} = 3 if x == nil { fmt.Println("None") } else if _, isInt := x.(int); isInt { fmt.Println(x, "x is Int") } else if s4, isString := x.(string); isString { fmt.Println(s4, "x is String") } else { fmt.Println("I don't know") } // 3 x is Int // switchで再現 switch x.(type) { case int: fmt.Println(x, "x is Int") case string: fmt.Println(x, "x is String") default: fmt.Println("I don't know") } // 3 x is Int // switchで再現 switch v5 := x.(type) { case int: fmt.Println(v5, "v5 is Int") case string: fmt.Println(v5, "v5 is String") default: fmt.Println("I don't know") } // 3 v5 is Int }
-
- defer() … 処理の最後に実行するもの ex.
func TestDefer() { defer fmt.Println("End") fmt.Println("Start") } func main() { TestDefer() defer func() { fmt.Println("1") fmt.Println("2") fmt.Println("3") }() // Start // End // 1 // 2 // 3 }
-
- 実践的な例
package main import ( "fmt" "os" ) func main() { file, err := os.Create("test.txt") if err != nil { fmt.Println(err) } defer file.Close() // 最後に実行される file.Write([]byte("Hello")) }
-
-
- エラーハンドリング … panic(), defer()
func main() { defer func() { if x := recover(); x != nil { fmt.Println(x) } }() panic("runtime error") fmt.Println("Start") // runtime error }
- 平行処理 go routin
package main import ( "fmt" "time" ) func sub() { for { fmt.Println("Sub Loop") time.Sleep(100 * time.Millisecond) } } func main() { go sub() go sub() for { fmt.Println("Main Loop") time.Sleep(200 * time.Millisecond) } }
- 構造体 struct
- エラーハンドリング … panic(), defer()
package main import "fmt" type User struct { Name string Age int // X, Y int } // コピーに対して更新していて更新できない関数 func UpdateUser(user User) { user.Name = "A" user.Age = 1000 } // こう書く🐱 func UpdateUser2(user *User) { user.Name = "A" user.Age = 1000 } func main() { var user1 User fmt.Println(user1) // { 0} user1.Name = "user1" user1.Age = 10 fmt.Println(user1) // {user1 10} user2 := User{} fmt.Println(user2) // { 0} user2.Name = "user2" fmt.Println(user2) // {user2 0} user3 := User{Name: "user3", Age: 30} fmt.Println(user3) // {user3 30} user4 := User{Name: "user4", Age: 40} fmt.Println(user4) // {user4 40} user7 := new(User) fmt.Println(user7) // &{ 0} ... ポインタ型になる // 🐱基本はこの書き方 user8 := &User{} fmt.Println(user8) // &{ 0} ... ポインタ型になる UpdateUser(user1) fmt.Println(user1) // {user1 10} ... 変わらない UpdateUser2(user8) fmt.Println(user8) // &{A 1000} ... 更新することができている🐱 }
構造体 メソッド
package main import "fmt" type User struct { Name string Age int // X, Y int } func (u User) sayName() { fmt.Println(u.Name) } // これだとコピーしたインスタンスのNameを更新していることになるので、更新できない func (u User) setName(name string) { u.Name = name } // 🐱こう書く func (u *User) setName2(name string) { u.Name = name } func main() { user1 := &User{"user1", 999} fmt.Println(user1) // &{user1 999} user1.sayName() // user1 user1.setName("A") user1.sayName() // user1 ... 変わってない user1.setName2("B") user1.sayName() // B ... 変わってる🐱 }
構造体 埋め込み
package main import "fmt" type User struct { Name string Age int // X, Y int } type T struct { User User } type T2 struct { User } func main() { t := T{User: User{Name: "user1", Age: 10}} fmt.Println(t) // {{user1 10}} fmt.Println(t.User) // {user1 10} fmt.Println(t.User.Name) // user1 t2 := T2{User: User{Name: "user1", Age: 10}} fmt.Println(t2.User.Name) // user1 }
構造体 コンストラクタ
package main import "fmt" type User struct { Name string Age int // X, Y int } func (u User) sayName() { fmt.Println(u.Name) } func NewUser(name string, age int) *User { return &User{Name: name, Age: age} } func main() { user1 := NewUser("user1", 999) fmt.Println(user1) // &{user1 999} }
- チャネル + go routin
-
package main import ( "fmt" "time" ) func reciver(c chan int) { for { i := <-c fmt.Println(i) } } func main() { ch1 := make(chan int) ch2 := make(chan int) go reciver(ch1) go reciver(ch2) i := 0 for i < 100 { ch1 <- i ch2 <- i time.Sleep(50 * time.Millisecond) i++ } }
-
- チャネル + go routin + close
- チャネル + go routin + close
package main import "fmt" func reciver(c chan int) { } // func main() { // ch1 := make(chan int, 2) // close(ch1) // ch1 <- 1 // panic: send on closed channel // } func main() { ch1 := make(chan int, 2) ch1 <- 1 close(ch1) i, ok := <-ch1 fmt.Println(i, ok) // 1 true i2, ok := <-ch1 fmt.Println(i2, ok) // 0 false }
-
-
-
-
- 応用
package main import ( "fmt" "time" ) func reciver(name string, ch chan int) { for { i, ok := <-ch if !ok { break } fmt.Println(name, i) } fmt.Println(name, "End") } func main() { ch1 := make(chan int, 2) ch1 <- 1 go reciver("1.goroutin", ch1) go reciver("2.goroutin", ch1) go reciver("3.goroutin", ch1) i := 0 for i < 100 { ch1 <- i i++ } close(ch1) time.Sleep(3 * time.Second) // 3.goroutin 1 // 2.goroutin 1 // 1.goroutin 0 // 1.goroutin 4 // 1.goroutin 5 // 1.goroutin 6 // 2.goroutin 3 // (略) // 1.goroutin 83 // 1.goroutin 84 // 1.goroutin 85 // 1.goroutin 86 // 1.goroutin 87 // 1.goroutin 88 // 2.goroutin 69 // 3.goroutin 82 // 3.goroutin 90 // 3.goroutin 92 // 3.goroutin 93 // 3.goroutin 94 // 1.goroutin 89 // 1.goroutin 96 // 2.goroutin 91 // 2.goroutin 98 // 2.goroutin 99 // End 2.goroutin // 1.goroutin 97 // End 1.goroutin // 3.goroutin 95 // End 3.goroutin }
チャネル + go routin + for
package main import "fmt" func main() { ch1 := make(chan int, 3) ch1 <- 1 ch1 <- 2 ch1 <- 3 close(ch1) for i := range ch1 { fmt.Println(i) } // 1 // 2 // 3 }
チャンネル + select その1
-
package main import "fmt" func main() { ch1 := make(chan int, 2) ch2 := make(chan string, 2) ch2 <- "A" select { case v1 := <-ch1: fmt.Println(v1 + 1000) case v2 := <-ch2: fmt.Println(v2 + "!?") } // A!? }
その2
func main() { ch1 := make(chan int, 2) ch2 := make(chan string, 2) ch2 <- "A" ch1 <- 1 ch2 <- "B" ch1 <- 2 select { case v1 := <-ch1: fmt.Println(v1 + 1000) case v2 := <-ch2: fmt.Println(v2 + "!?") } // ランダムになる // kanehiroyuu@MacBook-Pro-2 golang-webgosql % go run channelSelect.go // A!? // kanehiroyuu@MacBook-Pro-2 golang-webgosql % go run channelSelect.go // A!? // kanehiroyuu@MacBook-Pro-2 golang-webgosql % go run channelSelect.go // 1001 // kanehiroyuu@MacBook-Pro-2 golang-webgosql % go run channelSelect.go // 1001 // kanehiroyuu@MacBook-Pro-2 golang-webgosql % go run channelSelect.go // A!? }
その3
package main import "fmt" func main() { ch3 := make(chan int,) ch4 := make(chan int) ch5 := make(chan int) // reciever go func() { for { i := <-ch3 ch4 <- i * 2 } }() go func() { for { i2 := <-ch4 ch5 <- i2 -1 } }() n := 0 L: for { select { case ch3 <- n: n++ case i3 := <-ch5: fmt.Println("recieved", i3) default: if n > 100 { break L } } } } // kanehiroyuu@MacBook-Pro-2 golang-webgosql % go run channelSelect2.go // recieved -1 // recieved 1 // recieved 3 // recieved 5 // recieved 7 // recieved 9 // recieved 11 // recieved 13 // recieved 15 // ... // recieved 183 // recieved 185 // recieved 187 // recieved 189 // recieved 191 // recieved 193 // recieved 195 // recieved 197
-
-
-
- ポインタ
package main import "fmt" func Double(i int) { i = i * 2 } func Doublev2(i *int) { *i = *i * 2 } func Doublev3(s []int) { for i, v := range s { s[i] = v * 2 } } func main() { var n int = 100 fmt.Println(n) // 100 fmt.Println(&n) // 0xc00012a008 Double(n) fmt.Println(n) // 100 var p *int = &n // &変数 ... ポインタ fmt.Println(p) // 0xc000132008 fmt.Println(*p) // 100 ... *をつけると実体 *p = 300 // *pで実体を指定して代入 fmt.Println(n) // 300 n = 200 fmt.Println(*p) // 200 Doublev2(&n) fmt.Println(n) // 400 // スライスは参照型なので関数先で自動的に値が参照渡しされてる var sl []int = []int{1, 2, 3} Doublev3(sl) fmt.Println(sl) // [2 4 6] }
-
- CRUD操作 … 掲示板アプリなどで
- JSON操作
- フレームワーク
Echoの可能性が高い
フレームワークなしって可能性もある - Redisにレスポンスキャッシュ, 更新時にキャッシュクリア & キャッシュ
- init() … main()より先に呼ばれる関数
package main import ( "fmt" ) func init() { fmt.Println("init()") } func main() { fmt.Println("main()") // init() // main() }
- ジョブ・キュー
ミドルウェア側のチューニング
- 画像やCSSといった静的ファイルの圧縮
- メモリの最適化
- プロセス、CPUといったパラメータチューニング
- MySQLのINDEXを貼る
Nginx
ルールが参照系だけの場合
- 可能な限りHTTPレスポンスはNginx側でキャッシュする
キャッシュ is キング。 - ログインが存在するアプリの場合
・ページキャッシュ
・セッションキーがある場合はキャッシュしないようにする
・おそらくルール上利用は難しい
・オブジェクトキャッシュ
・RedisによるNoSQL実装をする必要がある - http2の利用
・listen 443 ssl http2;
・listen 80 http2; - CPU設定 autoで自動設定
worker_processes auto;
- https://worklog.be/archives/3222#cache_pathconf
- https://www.linuxcapable.com/set-up-nginx-fastcgi-cache-on-ubuntu-20-04/
user www www; pid /var/run/nginx.pid; ## auto を指定すれば最大限利用できる CPU コア数を勝手に設定してくれる ## 指定したいならサーバの CPU コア数以下で設定する worker_processes auto; worker_rlimit_nofile 4096; events { use epoll; multi_accept on; worker_connections 1024; } http { include mime.types; default_type text/plain; charset utf-8; sendfile on; tcp_nopush on; tcp_nodelay on; server_tokens off; keepalive_requests 100; keepalive_timeout 3; server_names_hash_bucket_size 64; types_hash_max_size 2048; client_body_buffer_size 64k; client_body_temp_path /home/www/tmp/client_body_temp 1 2; ## gzip圧縮を有効化 gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/x-javascript application/xml application/rss+xml application/atom+xml image/svg+xml; ## SSL周り ssl_session_timeout 30m; ssl_session_cache shared:SSL:10m; ssl_session_tickets off; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_dhparam /etc/nginx/ssl/dhparam.pem; ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256; ## fastcgi fastcgi_buffers 8 64k; fastcgi_buffer_size 64k; fastcgi_connect_timeout 60; fastcgi_send_timeout 60; fastcgi_read_timeout 300; ## proxy (WordPressだけだったらここは不要) proxy_connect_timeout 60; proxy_send_timeout 60; proxy_read_timeout 120; proxy_http_version 1.1; proxy_cache_bypass $http_upgrade; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; proxy_temp_path /home/www/tmp; ## cache_pathについては別ファイルで設定する include conf.d/cache_path.conf; ## デフォルトのサーバ server { listen *:80 default_server; root /home/www/htdocs; access_log /var/log/nginx-access.log combined; error_log /var/log/nginx-error.log warn; index index.html; include conf.d/common.conf; } ## バーチャルドメインの設定 include conf.d/virtual.conf; }
MySQL
- INDEXを貼る
・WHERE = {キー}
・ORDER BY {キー}
このキーに対してINDEXを貼る - EXPLAINによるALL解析EXPLAIN SELECT xxxx
mysql > EXPLAIN select * from sampledb.sampletable ;
typeにALLやindexがあったらINDEXを貼った方が良い。
indexの場合は、
typeがrefになるようにwhereの位置など変更してINDEXが効くようにクエリを改良する。
ただし、
一覧など全データを取ってきてという処理であれば、ALLは仕様がない場合もある。
- innodb_buffer_pool_size … GOの処理に割り当てる以外のメモリの8割はここに充てる
- query_cache_size=64M