新卒研修の締めくくりに社内ISUCONに挑戦しました。 | Wantedly Engineer Blog
はじめまして、今年新卒で入社したエンジニアの富岡です。先週のGWの合間の5/2に行った社内ISUCONの様子をお伝えします。ISUCONとはISUCON は「Iikanjini (良い感じに) ...
https://www.wantedly.com/companies/wantedly/post_articles/61666
はじめまして.今年新卒でWantedlyに入社した小林(@kobayang)です.GW合間の5/1に新卒研修の一環として,Wantedly内部で社内ISUCONを行いました.この記事はその振り返りになります.
昨年からWantedlyでは新卒研修の一貫としてISUCONを実施しており,今年も同様に実施されました.
ISUCONとは,「Iikanjini (良い感じに) Speed Up Contest」の略で、2~3人のチームで課題として与えられたウェブアプリケーションをできる限り高速化し、そのスコアを競うコンテストです。
昨年は飛び入りで参加したCTOの川崎さんが約9万点差というぶっちぎりのスコアで勝利しました.しかし,今年はISUCONの本選出場者が3人もおり,僕ら新卒としては「CTOに一泡吹かせる」という腹づもりでISUCONに臨みました.
今年の社内ISUCONに参加したチームはCTO+内定者チームと新卒チームが4組で合計5組でした.使用言語は,僕らのチームはGoで,Rubyが3チーム,Rustでフルスクラッチ(やばい)したチームが1チームという構成でした.
僕は田中くん(@bgpat)と2人での参加となりました.@bgpatはISUCON6の学生部門優勝メンバーです.これは気合が入ります.基本的には,@bgpatの指示をうけつつ実装を行いました.
今年の社内ISUCONの問題は,ISHOCON1(https://github.com/showwin/ISHOCON1)でした.
テーマは「爆買いECサイト」です。 爆買いに負けずより多くのレスポンスを返せるようにECサイトをチューニングしましょう。
とのことで,ベンチマークでめちゃくちゃリクエストが走ることが予想されます!
トップページ
また,複数台サーバ(Instance Type: c4.large * 3台)が競技用に与えられました.
CTOからマニュアルやレギュレーションなどの説明があったあと,11時ごろからチームに分かれての社内ISUCONが始まりました.
@bgpatがNetdata, Kataribeなどの計測ツールを入れてくれて,その間にNginxやMySQLのログを吐くように設定を少しいじりました.
Netdataは初めて使ったのですが,とても良かったです.CPUやメモリやネットワークの帯域が全部リアルタイムにGUIで見れるし,何よりカッコイイ!
計測ツールを回しつつベンチマークを実行してみると,初期スコアは100程度でした.Netdataを見て,MySQLへのCPU使用率が高いことを確認しました.それと同時にSlowQueryのログを見て,遅いクエリをいくつか見つけました.とりあえず一番重そうなテーブルにインデックスを貼りました.
また,DB Schemaを確認してデータが一番大きなテーブルでも30MB程度しかないことが分かりました.
遅いクエリがあったテーブルにINDEX貼り終え.再度ベンチを回すとスコアが100から2000くらいまで上がりました.でも思ったより上がらなかったといった感じです.
そろそろアプリの方の実装を見ていくかーというところでお昼になりました.
お昼ごはんは会社前の売店で買いました.
12時前の計測結果で,@izuminと@kdnkのRubyチームが15000点でトップに躍り出ていました.
MySQLの使用率が高いし,データもそれほど多くないことが分かったので,オンメモリで行くことに決めました.このあたりで,オンメモリだしGoかなってことでGoで書くことが決まりました.
遅いクエリがある場所やN+1がある場所を重点的にオンメモリ実装を行いました.
var products = map[int]*Product
func setProduct(p Product) {
// Sync.Mutex
productsMu.Lock()
products[p.ID] = &p
productsMu.Unlock()
}
func loadProducts() {
products = make(map[int]*Product)
rows, err := db.Query("SELECT * FROM products")
if err != nil {
fmt.Fprintf(os.Stderr, "getProducts: %v\n", err)
return
}
for rows.Next() {
var p Product
rows.Scan(&p.ID, &p.Name, &p.Description, &p.ImagePath, &p.Price, &p.CreatedAt)
setProduct(p)
}
}
こんな感じで初期化時にデータをメモリにロードします.
注意としてmapに代入するときはsync.Mutex
を使う必要があるかを考える必要があります.
このおかげで遅いクエリがほぼほぼなくなり,MySQLに奪われていたCPUもだいぶ落ち着いてきたので,ベンチマーカーのWorkLoadを上げることができるようになりました.
ここで,Scoreが20000ほどまで上がりました.メモリつよい.CTOもっとつよい
今回は複数台構成だったので,複数台を使うならば,メモリにあるデータの整合性を取る必要がありました.そこで,参考実装で使用されていたGoのWebフレームワークであるginのBindJSON
を使って,POSTのBindを@bgpatに教えてもらいながら実装しました.
受信部分
r.POST("/push/histories", func(c *gin.Context) {
var history History
c.BindJSON(&history)
setHistory(history)
c.JSON(http.StatusOK, history)
})
送信部分
// BuyProduct : buy product
func (u *User) BuyProduct(pid string) {
productID, _ := strconv.Atoi(pid)
createdAt := time.Now()
res, _ := db.Exec(
"INSERT INTO histories (product_id, user_id, created_at) VALUES (?, ?, ?)",
pid, u.ID, createdAt)
id, err := res.LastInsertId()
if err != nil {
panic(err.Error())
}
h := History{
ID: int(id),
ProductID: productID,
UserID: u.ID,
CreatedAt: createdAt.Format("2006-01-02 15:04:05"),
}
setHistory(h)
go func() {
for _, s := range os.Args[1:] {
// ここでRequestを他サーバに投げる
go gorequest.New().Post(s + "/push/histories").Send(h).End()
}
}()
}
データを変数に入れる箇所で,指定した他のサーバーにもリクエストを投げ,リクエストが投げられたサーバーもリクエストをBindしてデータを入れる,という感じです.ナイーブな実装ですが,ISUCON問題がサーバ複数台でオンメモリに挑戦するときは使ってみようと思いました.
上記の実装を行ったあと,ようやく複数台構成に変えました.ここで5時間ちょっとが経過.
3台のサーバーは全てwebサーバーとして受けることにしました.(便宜上web1, web2, web3サーバーと呼びます)
—> Nginx(web1) —> app(web1) <--> MySQL(web1)
—> Nginx(web2) --> app(web2) <--> MySQL(web2)
—> Nginx(web3) --> app(web3) <--> MySQL(web3)
すごくシンプルで,web1サーバへのリクエストをNginxでうけて,それを他のサーバにリダイレクトすることで,負荷分散を行います.
(注: 終わってから気付きましたが,データの整合性を正しく取るためにMySQLサーバへの接続は全てweb1で行うべきでした.)
またこの間に,web1サーバで画像とCSSを静的配信をするようにNginxの設定をしました.
これで,Scoreが40000点ほどまで上がりました.
Netdataを見ると,レスポンスが早くなった代わりに,ネットワークの帯域が悲鳴を上げていました.
画像でかなり帯域食ってるらしい.
ブラウザにキャッシュさせるようにしたら良くなるかな?と思ってキャッシュのヘッダを付けたけど
キャッシュしてくれてなさそうだったのでネットワークの負荷を下げることを考えます.
ここで画像ファイルを見たら5つしかファイルがなかったので,
一番軽い画像だけweb1サーバで受け取り,あとの画像を他サーバに振り分けるようにしました
...
location = /images/image1.jpg {
return 301 http://10.0.0.217/images/image1.jpg;
}
location = /images/image2.jpg {
return 301 http://10.0.0.50/images/image2.jpg;
}
location = /images/image3.jpg {
return 301 http://10.0.0.50/images/image3.jpg;
}
location = /images/image4.jpg {
return 301 http://10.0.0.217/images/image4.jpg;
}
神Config.
これで帯域問題が多少改善されて,ベンチのWorkLoadをさらに上げることができました.
この施策で,スコアが最高72000くらいまで上がりました.
このあたりで残り30分です.
このあと,再起動テストをしたり,細かいチューニングをしたりして,結局最終的なスコアは少し下がって 69355 になりました.
最終結果です.
結果としては,CTOチームが10万点を叩き出して優勝,僕らのチームは2位でした.残念ながら新卒の勝利は来年に持ち越しとなってしまいました.CTOには負けてしまいましたが,新卒チームでは1位と奮闘しました.また,競技のスコアとは別にRustチーム(@agatan, @qnighy)が実装に成功していたのはすごい.
優勝は叶いませんでしたが,ISUCONは一番学びが多い人が優勝なので実質優勝は僕です
ISUCON経験者の@bgpatに教えてもらいながら,社内ISUCONを行いました.
キリキリすることなく,ラフな雰囲気でISUCONを楽しむことができて,とても楽しかったです.
普段は業務でフロント側を書いているので,こういうふうに普段触らないNginxやMySQLのチューニングだったり,計測してどこか悪いかを特定して施策を打つところだったりとか,普段インフラをやってる@bgpatとやることで学ぶことが多かったです.
特に「おそらくここが悪いからこの部分を修正しよう」ではなく,「計測した結果だとこの部分が遅いから,ここの部分だけ修正しよう」といった計測と施策の繰り返しにより実際に結果が良くなっていくことがとても気持ち良いものだと知りました."推測ではなく計測せよ"とは良く言われますが,実際に,計測ベースの実装ができたのはとても大きな学びでした.
最後に,ISHOCONの問題作成者の@showwinさん,複数台への使用変更とメンバー全員分のサーバー環境を用意していただいたインフラの坂部さん,ありがとうございました!そして分からないことだらけな僕を,ほぼほぼペアプロみたいになりながら親身に教えてくれた田中くん,本当にありがとう!
去年はISUCONの予選に初めて参加して結果惨敗だった.今年こそは本戦に行くぞ!