pixiv private isucon 2016 攻略 (2/5)
アプリを読む
ソースコードを読みながらだいたいの構成を把握していきます。
- フレームワークは goji を使っている。 goji のサイトを見ると同じ名前で新しいフレームワークを作り直したみたいだけど、これは古い方を使ってるみたいです。
- セッションは Gorilla のライブラリに memcached store を組み合わせている
- テンプレートをレンダリングのたびにコンパイルしてるのが重そう。
- コメントのユーザーをいちいちユーザーテーブルから引っ張ってるのも JOIN かキャッシュが効きそう。
- 画像データを MySQL に突っ込んでるのは、ファイルに出す必要がありそう。(ファイルなら数十KBずつ読んでは返せるけれど、DBだと簡単な方法だと一度に全体をメモリに読んでしまうので、並列にアクセスが来たときにメモリが足りなくなります。)
読んでいると改善したいポイントがたくさん出てくるものの、いきなりあちこち改造しだすとバグったときに困ります。はじめは基礎的な設定とボトルネックに絞ったチューニングです。
最初にDBのコネクションプール設定できてないので設定しておきましょう。2コアなんでそんなにたくさんコネクションあっても仕方ないハズ。とりあえずちょい多めの8にしておきます。
+ db.SetMaxOpenConns(8)
+ db.SetMaxIdleConns(8)
defer db.Close()
アクセスログを読む
事前に用意しておいたアクセスログ集計ツール(自作してもいいですし、過去のいろいろな方のISUCONの記事を探してみれば良いツールが紹介されているはずです)を、アクセスログやアプリのURLルーティングの設定を参考にカスタマイズしましょう。
僕の場合は自作スクリプトで、次のような集計をしました。
Request by count 8028 GET /image/* 644 GET / 551 GET /js/jquery-2.2.0.js 551 GET /css/style.css 551 GET /js/jquery.timeago.ja.js 551 GET /favicon.ico 551 GET /js/jquery.timeago.js 551 GET /js/main.js 450 POST /login 299 GET /posts/* 235 GET /@user 130 POST / 98 GET /login 90 POST /comment 79 POST /register 78 GET /admin/banned 56 GET /posts?max_created_at= 50 GET /logout 1 GET /initialize Request by total time (total avg [sec]) 198.379 0.308041925466 GET / 168.858 0.021033632287 GET /image/* 47.605 0.202574468085 GET /@user 34.491 0.0766466666667 POST /login 30.444 0.543642857143 GET /posts?max_created_at= 15.095 0.0504849498328 GET /posts/* 7.068 0.0894683544304 POST /register 6.7 0.0515384615385 POST / 6.51 0.0118148820327 GET /favicon.ico 6.483 0.0117658802178 GET /js/jquery-2.2.0.js 5.784 0.010497277677 GET /js/jquery.timeago.js 5.213 0.0094609800363 GET /js/jquery.timeago.ja.js 5.095 0.00924682395644 GET /css/style.css 4.997 0.00906896551724 GET /js/main.js 2.478 0.0275333333333 POST /comment 2.257 0.0230306122449 GET /login 1.314 0.0168461538462 GET /admin/banned 1.131 0.02262 GET /logout 0.061 0.061 GET /initialize Request by out bytes (total avg) 2485959550 309661 GET /image/* 142499069 258619 GET /js/jquery-2.2.0.js 12206011 18953 GET / 4287247 18243 GET /@user 4105501 7451 GET /js/jquery.timeago.js 1912146 34145 GET /posts?max_created_at= 1108574 3707 GET /posts/* 980229 1779 GET /css/style.css 487635 885 GET /js/main.js 357048 648 GET /js/jquery.timeago.ja.js 195356 434 POST /login 177772 1814 GET /login 149872 272 GET /favicon.ico 34286 434 POST /register 23530 181 POST / 22200 444 GET /logout 16833 187 POST /comment 13104 168 GET /admin/banned 161 161 GET /initialize
これで考えるのは、主に次の2点です。
- トップページと
/image/に掛かる時間が一番重い。特にトップページを最初にチューニングするのが良さそう。 - 帯域は
/image/が支配的。帯域ネックになるようだったら、画像は再圧縮しにくいので、ヘッダーを適切に設定すればクライアントに "304 Not Modified" を返せるようになって大きなブレークスルーがあるかもしれない。
pprof
とりあえず app の CPU が重いので、CPUプロファイルから始めましょう。 pprof の基本的な使い方は、以前に Blog を書いたのでそれを参照してください。
diff --git a/app.go b/app.go
index 861ff03..eb52f47 100644
--- a/app.go
+++ b/app.go
@@ -9,6 +9,7 @@ import (
"io/ioutil"
"log"
"net/http"
+ _ "net/http/pprof"
"net/url"
"os"
"os/exec"
@@ -826,6 +827,8 @@ func main() {
+
+ db.SetMaxOpenConns(8)
+ db.SetMaxIdleConns(8)
defer db.Close()
+ go http.ListenAndServe(":3000", nil)
+
goji.Get("/initialize", getInitialize)
goji.Get("/login", getLogin)
goji.Post("/login", postLogin)
$ go tool pprof app http://localhost:3000/debug/pprof/profile
Fetching profile from http://localhost:3000/debug/pprof/profile
Please wait... (30s)
Saved profile in /home/isucon/pprof/pprof.localhost:3000.samples.cpu.001.pb.gz
Entering interactive mode (type "help" for commands)
(pprof) top 10
14440ms of 29650ms total (48.70%)
Dropped 546 nodes (cum <= 148.25ms)
Showing top 10 nodes out of 214 (cum >= 7370ms)
flat flat% sum% cum cum%
2270ms 7.66% 7.66% 8650ms 29.17% runtime.mallocgc
2220ms 7.49% 15.14% 2220ms 7.49% runtime.memclr
2150ms 7.25% 22.39% 2840ms 9.58% runtime.scanobject
1930ms 6.51% 28.90% 2060ms 6.95% syscall.Syscall
1280ms 4.32% 33.22% 1280ms 4.32% runtime.heapBitsSetType
1240ms 4.18% 37.40% 1270ms 4.28% runtime.(*mspan).sweep.func1
1000ms 3.37% 40.78% 1000ms 3.37% runtime.memmove
930ms 3.14% 43.91% 5070ms 17.10% database/sql.convertAssign
750ms 2.53% 46.44% 2240ms 7.55% time.parse
670ms 2.26% 48.70% 7370ms 24.86% github.com/go-sql-driver/mysql.(*textRows).readRow
メモリ管理が重いですね。あと MySQL から受け取った結果をパースしてる部分が重い。
メモリ管理が重い場合、メモリのアロケートしてる部分を見つけて潰していく方法もありますが、もっと高いレイヤーでのアプリのチューニングの方が先です。この場合 -cum オプションを使ってトップダウンにプロファイルを見ます。
(pprof) top -cum 40
10.25s of 29.65s total (34.57%)
Dropped 546 nodes (cum <= 0.15s)
Showing top 40 nodes out of 214 (cum >= 2.22s)
flat flat% sum% cum cum%
0 0% 0% 28.99s 97.77% runtime.goexit
0 0% 0% 25.69s 86.64% net/http.(*conn).serve
0 0% 0% 25.40s 85.67% net/http.(*ServeMux).ServeHTTP
...
0 0% 0% 25.08s 84.59% github.com/zenazn/goji/web/middleware.Recoverer.func1
0 0% 0% 22.42s 75.62% github.com/zenazn/goji/web.netHTTPHandlerFuncWrap.ServeHTTPC
0 0% 0% 20.61s 69.51% main.getIndex
0 0% 0% 20.26s 68.33% github.com/jmoiron/sqlx.(*DB).Select
0 0% 0% 20.26s 68.33% github.com/jmoiron/sqlx.Select
...
上位にいるリクエストハンドラーは getIndex だけです。ここに集中しましょう。
ソースコードの行単位のプロファイルを見るにはデバッグ情報が必要なので、 pprof 起動時にアプリのバイナリを指定しておく必要があります。
バイナリを指定し忘れて URL だけでプロファイルを取ってしまった場合も、起動したときの "Saved profile in ..." に表示されているパスにプロファイル結果が保存されているので、再度プロファイリングしなくても
go tool pprof app <プロファイル結果のパス> で大丈夫です。
isucon:~/private_isu/webapp/golang$ go tool pprof app /home/isucon/pprof/pprof.localhost:3000.samples.cpu.001.pb.gz
Entering interactive mode (type "help" for commands)
(pprof) list main.getIndex
Total: 29.65s
ROUTINE ======================== main.getIndex in /home/isucon/private_isu/webapp/golang/app.go
0 20.61s (flat, cum) 69.51% of Total
. . 388:
. . 389: http.Redirect(w, r, "/", http.StatusFound)
. . 390:}
. . 391:
. . 392:func getIndex(w http.ResponseWriter, r *http.Request) {
. 120ms 393: me := getSessionUser(r)
. . 394:
. . 395: results := []Post{}
. . 396:
. 18.39s 397: err := db.Select(&results, "SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` ORDER BY `created_at` DESC")
. . 398: if err != nil {
. . 399: fmt.Println(err)
. . 400: return
. . 401: }
. . 402:
. 1.34s 403: posts, merr := makePosts(results, getCSRFToken(r), false)
. . 404: if merr != nil {
. . 405: fmt.Println(merr)
. . 406: return
. . 407: }
. . 408:
. . 409: fmap := template.FuncMap{
. . 410: "imageURL": imageURL,
. . 411: }
. . 412:
. . 413: template.Must(template.New("layout.html").Funcs(fmap).ParseFiles(
. . 414: getTemplPath("layout.html"),
. . 415: getTemplPath("index.html"),
. . 416: getTemplPath("posts.html"),
. . 417: getTemplPath("post.html"),
. 70ms 418: )).Execute(w, struct {
. . 419: Posts []Post
. . 420: Me User
. . 421: CSRFToken string
. . 422: Flash string
. 690ms 423: }{posts, me, getCSRFToken(r), getFlash(w, r, "notice")})
. . 424:}
. . 425:
. . 426:func getAccountName(c web.C, w http.ResponseWriter, r *http.Request) {
. . 427: user := User{}
. . 428: uerr := db.Get(&user, "SELECT * FROM `users` WHERE `account_name` = ? AND `del_flg` = 0", c.URLParams["accountName"])
前回の記事の最後で myprofiler で見つけた、 posts からの LIMIT なしクエリが重い問題ですね。MySQL 側だけでなく、その大量の結果を全件取得してパースしてる app 側でも一番重くなっているようです。
このクエリの結果を利用している makePosts() の中を見てみると、ループの終端にこんな if 文があります。
if len(posts) >= postsPerPage {
break
}
この手前に continue 文もないので、このまま postsPerPage で LIMIT しましょう。 (注: これが確認不足で後でハマります)
diff --git a/app.go b/app.go
index eb52f47..3dd9804 100644
--- a/app.go
+++ b/app.go
@@ -394,7 +394,7 @@ func getIndex(w http.ResponseWriter, r *http.Request) {
results := []Post{}
- err := db.Select(&results, "SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` ORDER BY `created_at` DESC")
+ err := db.Select(&results, "SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` ORDER BY `created_at` DESC LIMIT 20")
if err != nil {
fmt.Println(err)
return
さて、計測です。本当は計測とプロファイルは別にした方がいいですが、時間が勿体無いのでプロファイルしながら計測です。
スコア:
{"pass":true,"score":12848,"success":22272,"fail":1201,"messages":["1ページに表示される画像の数が足りません (GET /)","リダイレクト先URLが正しくありません: expected '^/$', got '/login' (GET /login)"]}
あらら。エラーがでてきました。アプリのログを見ると、
fork/exec /bin/bash: cannot allocate memory
メモリが足りなくて fork に失敗してました。
top:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 11931 isucon 20 0 406248 183372 9052 S 74.8 17.9 0:19.55 app 720 mysql 20 0 1226416 215076 10404 S 65.5 21.0 3:24.11 mysqld
CPU 使用率は app と mysql がだいたい一緒ですね。足しても200にだいぶ届かないので、CPU以外の部分がネックにりはじめていそうです。
myprofiler:
17 SELECT * FROM `posts` WHERE `id` = N 12 SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` WHERE `created_at` <= S ORDER BY `created_at` DESC 4 SELECT * FROM `users` WHERE `id` = N 3 SELECT * FROM `users` WHERE `id` = ?
posts から id 指定で1レコード取得が重くなりました。 N+1 かな?
外部プロセス呼び出しをやめる
fork に失敗するのは、多分(スワップがないせいで?)オーバーコミットがオフになっていて、fork した瞬間に app と同じだけのメモリを持ってるもう1プロセスを作れないからだと思います。
CPU使用率が2コアなのに200%に全然届かないのも fork のせい (Go のランタイムの都合上 fork するためには全 goroutine を一旦止めないといけなくて待ち時間が発生する) かもしれないので、メモリ使用量削減のために画像ファイルをDBから切り出すよりも外部プロセス呼び出しをやめる方を優先しましょう。
外部プロセスを読んでるところを見ると、 openssl dgst -sha512 コマンドで sha512 ダイジェストを計算しているようです。 Go で sha512 を計算する方法を調べて、同じ出力をするプログラムを書いてみます。
$ cat t.go < app.go
package main
import (
"crypto/sha512"
"fmt"
"io/ioutil"
"os"
)
func main() {
data, _ := ioutil.ReadAll(os.Stdin)
fmt.Printf("%x\n", sha512.Sum512(data))
}
isucon@ip-10-0-1-202:~/private_isu/webapp/golang$ go run t.go < t.go
b252b55576a4f4e321d5761b6dc980b8ee41fd9767765c785861a73b736f6a59aa7f40f7ba0d92c508dea32a20bc800e0198056c3dc8b33d5ac445f27b163d17
isucon@ip-10-0-1-202:~/private_isu/webapp/golang$ openssl dgst -sha512 < t.go
(stdin)= b252b55576a4f4e321d5761b6dc980b8ee41fd9767765c785861a73b736f6a59aa7f40f7ba0d92c508dea32a20bc800e0198056c3dc8b33d5ac445f27b163d17
これを参考に app.go を書き換えます。
@@ -125,14 +125,7 @@ func escapeshellarg(arg string) string {
}
func digest(src string) string {
- // opensslのバージョンによっては (stdin)= というのがつくので取る
- out, err := exec.Command("/bin/bash", "-c", `printf "%s" `+escapeshellarg(src)+` | openssl dgst -sha512 | sed 's/^.*= //'`).Output()
- if err != nil {
- fmt.Println(err)
- return ""
- }
-
- return strings.TrimSuffix(string(out), "\n")
+ return fmt.Sprintf("%x", sha512.Sum512([]byte(src)))
}
さて、プロファイル&計測です。アプリのログには fork エラーが消えました。
スコア:
{"pass":true,"score":26755,"success":24050,"fail":119,"messages":["1ページに表示される画像の数が足りません (GET /)"]}
うーん、まだエラー出てますね。これは LIMIT 足したのがマズいかな?
top:
Tasks: 84 total, 1 running, 83 sleeping, 0 stopped, 0 zombie %Cpu(s): 54.2 us, 20.7 sy, 0.0 ni, 15.0 id, 1.4 wa, 0.0 hi, 8.3 si, 0.4 st KiB Mem: 1022972 total, 958404 used, 64568 free, 18292 buffers KiB Swap: 0 total, 0 used, 0 free. 425600 cached Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 720 mysql 20 0 1357488 210336 4476 S 78.6 20.6 5:19.05 mysqld 763 isucon 20 0 619612 198632 9120 S 70.2 19.4 0:13.54 app 2904 www-data 20 0 92028 4088 1928 S 11.3 0.4 0:29.40 nginx 2572 isucon 20 0 34344 11148 2160 S 7.3 1.1 0:28.48 tmux
idle が 15% まで下がってます。狙い通り。 あと、 tmux が 7% まで来ました。 Goji のログがリッチすぎるので、最後には切らないといけませんね。
myprofiler:
## 2016-05-25 20:18:44.06 +0900 13 SELECT * FROM `posts` WHERE `id` = N 9 SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` WHERE `created_at` <= S ORDER BY `created_at` DESC 9 SELECT * FROM `comments` WHERE `post_id` = N ORDER BY `created_at` DESC LIMIT N 6 SELECT * FROM `users` WHERE `id` = ? 6 SELECT `id` FROM `posts` WHERE `user_id` = N 4 SELECT * FROM `users` WHERE `id` = N 4 SELECT COUNT(*) AS `count` FROM `comments` WHERE `post_id` = ? 2 SELECT COUNT(*) AS `count` FROM `comments` WHERE `post_id` = N 2 SELECT * FROM `comments` WHERE `post_id` = ? ORDER BY `created_at` DESC LIMIT N 2 SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` WHERE `user_id` = N ORDER BY `created_at` DESC
バグ修正
makePosts() を見直してみると、投稿したユーザーが論理削除されていたら配列への追加をしないようなコードになっていました。
(Go だと early return (continue) イディオムを使うことが多いのでこの部分を見逃していました)
if p.User.DelFlg == 0 {
posts = append(posts, p)
}
SELECT の段階で強引に JOIN してこの論理削除のフィルタリングを実行してしまいます。
@@ -394,7 +387,7 @@ func getIndex(w http.ResponseWriter, r *http.Request) {
results := []Post{}
- err := db.Select(&results, "SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` ORDER BY `created_at` DESC LIMIT 20")
+ err := db.Select(&results, "SELECT posts.`id`, `user_id`, `body`, `mime`, posts.`created_at` FROM `posts` INNER JOIN `users` ON posts.user_id=users.id WHERE users.del_flg = 0 ORDER BY `created_at` DESC LIMIT 20")
if err != nil {
fmt.Println(err)
return
スコア:
{"pass":true,"score":27399,"success":23615,"fail":0,"messages":[]}
今度はちゃんと通りました。
top:
Tasks: 83 total, 4 running, 79 sleeping, 0 stopped, 0 zombie %Cpu(s): 53.9 us, 23.6 sy, 0.0 ni, 9.8 id, 4.2 wa, 0.0 hi, 8.2 si, 0.2 st KiB Mem: 1022972 total, 948340 used, 74632 free, 12604 buffers KiB Swap: 0 total, 0 used, 0 free. 486056 cached Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 720 mysql 20 0 1359224 209144 4576 S 85.9 20.4 6:06.96 mysqld 910 isucon 20 0 487224 139892 8884 S 67.9 13.7 0:09.58 app 2904 www-data 20 0 91852 2828 844 R 10.7 0.3 0:35.40 nginx 2572 isucon 20 0 34472 10100 928 S 6.7 1.0 0:33.64 tmux
ちょっと mysql が重いですね。
myprofiler:
15 SELECT * FROM `posts` WHERE `id` = N 7 SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` WHERE `created_at` <= S ORDER BY `created_at` DESC 6 SELECT * FROM `comments` WHERE `post_id` = N ORDER BY `created_at` DESC LIMIT N 5 SELECT * FROM `users` WHERE `id` = ? 5 INSERT INTO `comments` (`post_id`, `user_id`, `comment`) VALUES (?,?,?) 5 SELECT COUNT(*) AS `count` FROM `comments` WHERE `post_id` = N 5 SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` WHERE `user_id` = N ORDER BY `created_at` DESC 4 INSERT INTO `posts` (`user_id`, `mime`, `imgdata`, `body`) VALUES (?,?,?,?) 4 INSERT INTO `users` (`account_name`, `passhash`) VALUES (?,?) 3 SELECT * FROM `users` WHERE `id` = N
JOIN を足したけど、それでもまだ post を id で引くのが重いみたいです。
pprof top40 -cum:
...
0 0% 0.38% 13.88s 76.18% github.com/zenazn/goji/web/middleware.AutomaticOptions.func1
0.01s 0.055% 0.44% 8.85s 48.57% github.com/zenazn/goji/web.netHTTPHandlerFuncWrap.ServeHTTPC
0 0% 0.44% 6.11s 33.53% main.getIndex
0.02s 0.11% 0.55% 4.92s 27.00% github.com/jmoiron/sqlx.(*DB).Get
0.01s 0.055% 0.6% 4.90s 26.89% github.com/jmoiron/sqlx.Get
0 0% 0.6% 4.78s 26.23% github.com/zenazn/goji/web.handlerFuncWrap.ServeHTTPC
0.02s 0.11% 0.71% 4.65s 25.52% main.makePosts
3.82s 20.97% 21.68% 4.06s 22.28% syscall.Syscall
...
pprof list makePosts:
ROUTINE ======================== main.makePosts in /home/isucon/private_isu/webapp/golang/app.go
20ms 4.65s (flat, cum) 25.52% of Total
. . 173:}
. . 174:
. . 175:func makePosts(results []Post, CSRFToken string, allComments bool) ([]Post, error) {
. . 176: var posts []Post
. . 177:
10ms 10ms 178: for _, p := range results {
. 1.05s 179: err := db.Get(&p.CommentCount, "SELECT COUNT(*) AS `count` FROM `comments` WHERE `post_id` = ?", p.ID)
. . 180: if err != nil {
. . 181: return nil, err
. . 182: }
. . 183:
. . 184: query := "SELECT * FROM `comments` WHERE `post_id` = ? ORDER BY `created_at` DESC"
. . 185: if !allComments {
. 20ms 186: query += " LIMIT 3"
. . 187: }
. . 188: var comments []Comment
. 1.38s 189: cerr := db.Select(&comments, query, p.ID)
. . 190: if cerr != nil {
. . 191: return nil, cerr
. . 192: }
. . 193:
. . 194: for i := 0; i < len(comments); i++ {
. 770ms 195: uerr := db.Get(&comments[i].User, "SELECT * FROM `users` WHERE `id` = ?", comments[i].UserID)
. . 196: if uerr != nil {
. . 197: return nil, uerr
. . 198: }
. . 199: }
. . 200:
. . 201: // reverse
. . 202: for i, j := 0, len(comments)-1; i < j; i, j = i+1, j-1 {
. . 203: comments[i], comments[j] = comments[j], comments[i]
. . 204: }
. . 205:
10ms 10ms 206: p.Comments = comments
. . 207:
. 1.35s 208: perr := db.Get(&p.User, "SELECT * FROM `users` WHERE `id` = ?", p.UserID)
. . 209: if perr != nil {
. . 210: return nil, perr
. . 211: }
. . 212:
. . 213: p.CSRFToken = CSRFToken
. . 214:
. . 215: if p.User.DelFlg == 0 {
. 60ms 216: posts = append(posts, p)
. . 217: }
. . 218: if len(posts) >= postsPerPage {
. . 219: break
. . 220: }
. . 221: }
時間のかかってるクエリがコメント取得を除いて1レコード取得するだけのクエリなので、簡単なクエリを大量に投げてるのが問題で重いのでしょう。
プレースホルダがあるクエリは、デフォルトでは prepared statement を使い捨てにしている (prepare, execute, close の3コマンド) ので、明示的に prepared statement を再利用するコードを書くか、 prepared statement を使わないようにするのがいいです。
最高性能を目指すなら再利用の方がいいですが、改修が簡単なのは prepared statement を使わない方です。 dsn に interpolateParams=true を足すだけ! (僕が実装した機能です。 参考)
diff:
@@ -805,7 +805,7 @@ func main() {
}
dsn := fmt.Sprintf(
- "%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=Local",
+ "%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=Local&interpolateParams=true",
user,
password,
host,
スコア:
{"pass":true,"score":30076,"success":25687,"fail":0,"messages":[]}
30k 点に到達。
top:
Tasks: 83 total, 5 running, 78 sleeping, 0 stopped, 0 zombie %Cpu(s): 63.1 us, 17.4 sy, 0.0 ni, 8.5 id, 3.4 wa, 0.0 hi, 7.4 si, 0.2 st KiB Mem: 1022972 total, 960140 used, 62832 free, 13192 buffers KiB Swap: 0 total, 0 used, 0 free. 472760 cached Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 720 mysql 20 0 1359224 209132 4128 S 79.7 20.4 6:56.71 mysqld 1010 isucon 20 0 514368 157192 8888 S 66.8 15.4 0:08.93 app 2904 www-data 20 0 91876 2824 844 R 11.3 0.3 0:41.86 nginx 1015 isucon 20 0 21176 18648 3444 R 8.3 1.8 0:01.31 myprofiler 2572 isucon 20 0 34920 10472 916 S 7.3 1.0 0:38.28 tmux
myprofiler:
18 SELECT * FROM `posts` WHERE `id` = N 11 INSERT INTO `posts` (`user_id`, `mime`, `imgdata`, `body`) VALUES (N,S,_binaryS,S) 9 SELECT * FROM `users` WHERE `id` = N 7 SELECT * FROM `comments` WHERE `post_id` = N ORDER BY `created_at` DESC LIMIT N 6 SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` WHERE `user_id` = N ORDER BY `created_at` DESC 5 SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` WHERE `created_at` <= S ORDER BY `created_at` DESC 4 SELECT COUNT(*) AS `count` FROM `comments` WHERE `post_id` = N 2 INSERT INTO `users` (`account_name`, `passhash`) VALUES (S,S) 1 SELECT * FROM `users` WHERE `account_name` = S AND `del_flg` = N 1 INSERT INTO `comments` (`post_id`, `user_id`, `comment`) VALUES (N,N,S)
pprof:
(pprof) top40 -cum
4.23s of 18.58s total (22.77%)
Dropped 583 nodes (cum <= 0.09s)
Showing top 40 nodes out of 317 (cum >= 2.07s)
flat flat% sum% cum cum%
0 0% 0% 17.10s 92.03% runtime.goexit
...
0 0% 0.22% 8.84s 47.58% github.com/zenazn/goji/web.netHTTPHandlerFuncWrap.ServeHTTPC
0 0% 0.22% 5.66s 30.46% main.getIndex
0 0% 0.22% 4.90s 26.37% github.com/zenazn/goji/web.handlerFuncWrap.ServeHTTPC
0 0% 0.22% 4.04s 21.74% github.com/jmoiron/sqlx.(*DB).Get
0.04s 0.22% 0.43% 4.04s 21.74% github.com/jmoiron/sqlx.Get
0 0% 0.43% 3.73s 20.08% html/template.(*Template).Execute
0.03s 0.16% 0.59% 3.50s 18.84% main.makePosts
0 0% 0.59% 3.32s 17.87% github.com/jmoiron/sqlx.(*DB).Select
0 0% 0.59% 3.32s 17.87% github.com/jmoiron/sqlx.Select
0.93s 5.01% 5.60% 3.23s 17.38% runtime.mallocgc
0 0% 5.60% 3.12s 16.79% text/template.(*Template).Execute
0.08s 0.43% 6.03% 3.12s 16.79% text/template.(*state).walk
0.01s 0.054% 6.08% 3.07s 16.52% text/template.(*state).walkTemplate
0 0% 6.08% 3.02s 16.25% text/template.(*state).walkRange
0 0% 6.08% 3.02s 16.25% text/template.(*state).walkRange.func1
0.13s 0.7% 6.78% 2.96s 15.93% runtime.systemstack
そろそろテンプレートの重さが上位に見え始めましたが、まだDBの方が重い。
まとめ
スコア: 4745 (初期状態) -> 14208 (前回) -> 30076 (今回)
さて、そろそろプロファイルで重いところを潰すチューニングは、乾いた雑巾絞りに近づいてきました。 次回はもっとドラスティックなチューニングに移っていきたいと思います。
今回は pprof や myprofiler を使ったチューニングの紹介の意図があったのでちょっとやりすぎてしまいました。 実際にはもっと早めに大改造を必要とするようなチューニングを考えて実行しないと、後半に時間が足りなくてジリ貧になってしまうので気をつけましょう。
@methane