- 「ボトルネックの特定は外側から順番に」に従って、外形監視からする。
- プロファイラーは何がいいかわからんので色々使ってみる
Memos
ログイン後 linux user を isucon に切り替える
ssh
sudo su isucon
cd
benchmark tool は結構オプションが色々いける
./bench_linux_amd64 run --target https://pipe.u.isucon.dev --nameserver 35.72.35.148 --enable-ssl
を参考に VSCode でログイン後ユーザーを変えてリモート接続する https://qiita.com/craymaru/items/f126597ed91940226805
# Enable Kerb Forwarding
Host isucon13
HostName xxxx
IdentityFile xxx.pem
User ubuntu
RemoteCommand sudo su ubuntu
sudo service isupipe-go restart
sudo service isupipe-go status
ポートフォワード
ssh -i "xxx.pem" ubuntu@35.72.35.148 -fNL 1080:localhost:1080
unit file 確認
sudo cat /etc/systemd/system/isupipe-go.service
直接バイナリ実行してる
ExecStart=/home/isucon/webapp/go/isupipe
profiler 導入
pprof
質問と応答
Go でプロファイラー(例: `pprof`)を差し込む場合、通常は `main` 関数にプロファイリングの設定や開始・停止を記述しますが、それがプロファイリングされるコードの範囲を制限するものではありません。
実際には、`main` 関数にプロファイリングの開始を記述することで、プログラム全体でのプロファイリングが可能です。
基本トップレベルに書くぽい
"net/http/pprof"
"runtime"
runtime.SetBlockProfileRate(1)
runtime.SetMutexProfileFraction(1)
go func() {
log.Fatal(http.ListenAndServe("localhost:6060", nil))
}()
go build -o isupipe
ls --full-time
sudo service isupipe-go restart
sudo service isupipe-go status
ps aux | grep "isupipe"
isucon 5265 3.1 0.3 1825708 12992 ? Ssl 00:53 0:25 /home/isucon/webapp/go/isupipe
lsof -c isupipe
isupipe 5265 isucon 7u IPv6 43103 0t0 TCP *:8080 (LISTEN)
isupipe 5265 isucon 25u IPv4 54197 0t0 TCP 127.0.0.1:61316->127.0.0.1:3306 (ESTABLISHED)
プロファイリングツール起動(こいつは WEB UI も提供してくれる)
go tool pprof -seconds 60 -http=localhost:1080 http://localhost:6060/debug/pprof/profile
一度保存したファイルは以下の感じで開ける
go tool pprof -http=localhost:1080 /home/isucon/pprof/pprof.isupipe.samples.cpu.001.pb.gz
うまくとれてないっぽい
profiling してる間に裏で benchmarker 実行
go tool pprof -seconds 60 -http=localhost:1080 http://localhost:6060/debug/pprof/profile
多そうなやつ
- getLivestreamStatisticsHandler
- searchLivestreamsHandler
getLivestreamStatisticsHandler
stats_handler.go を見ていく
sqlx.GetContext が多い。多分SQL
if err := tx.GetContext(ctx, &livestream, "SELECT * FROM livestreams WHERE id = ?", livestreamID); err != nil {
みていく
how columns from livestreams;
+---------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+--------------+------+-----+---------+----------------+
| id | bigint | NO | PRI | NULL | auto_increment |
| user_id | bigint | NO | | NULL | |
| title | varchar(255) | NO | | NULL | |
| description | text | NO | | NULL | |
| playlist_url | varchar(255) | NO | | NULL | |
| thumbnail_url | varchar(255) | NO | | NULL | |
| start_at | bigint | NO | | NULL | |
| end_at | bigint | NO | | NULL | |
+---------------+--------------+------+-----+---------+----------------+
show index from livestreams;
まずは SELECT 系しかしてないので tx で transaction 貼ってるところを止める
次に、なんか大量に Get してるところを一回の SQL でよんできて構造体に mapping する
// // Rank 以外の Stats を算出
var stats LivestreamStatistics
if err := dbConn.GetContext(ctx, &stats, `
SELECT
COUNT(DISTINCT r.id) AS total_reactions,
COUNT(DISTINCT h.user_id) AS viewers_count,
COALESCE(MAX(l2.tip), 0) AS max_tip,
COUNT(DISTINCT lr.id) AS total_reports
FROM livestreams l
LEFT JOIN reactions r ON l.id = r.livestream_id
LEFT JOIN livecomments l2 ON l.id = l2.livestream_id
LEFT JOIN livestream_viewers_history h ON h.livestream_id = l.id
LEFT JOIN livecomment_reports lr ON lr.livestream_id = l.id
WHERE l.id = ?`, livestreamID); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get livestream stats: "+err.Error())
}
Conn pooling もしてみる
dbConn = conn
dbConn.SetMaxOpenConns(10)
dbConn.SetMaxIdleConns(5)
これ参考に DB に index 貼ってく
SELECT
COUNT(DISTINCT r.id) AS total_reactions,
COUNT(DISTINCT h.user_id) AS viewers_count,
COALESCE(MAX(l2.tip), 0) AS max_tip,
COUNT(DISTINCT lr.id) AS total_reports
FROM livestreams l
LEFT JOIN reactions r ON l.id = r.livestream_id
LEFT JOIN livecomments l2 ON l.id = l2.livestream_id
LEFT JOIN livestream_viewers_history h ON h.livestream_id = l.id
LEFT JOIN livecomment_reports lr ON lr.livestream_id = l.id
- r.livestream_id
alter table reactions add index livestream_id_idx (livestream_id);
show columns from livestream_viewers_history;
+---------------+--------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+--------+------+-----+---------+----------------+
| id | bigint | NO | PRI | NULL | auto_increment |
| user_id | bigint | NO | | NULL | |
| livestream_id | bigint | NO | | NULL | |
| created_at | bigint | NO | | NULL | |
+---------------+--------+------+-----+---------+----------------+
show index from livestream_viewers_history;
- livestream_id
alter table livestream_viewers_history add index livestream_id_idx (livestream_id);
alter table livecomment_reports add index livestream_id_idx (livestream_id);
alter table livecomments add index livestream_id_idx (livestream_id);
Statのクエリ N+1 解決がうまくいかんな
curl https://pipe.u.isucon.dev/api/livestream/7496/statistics \
--resolve pipe.u.isucon.dev:443:35.72.35.148
これでいけた。DB側でソートしてから返すことでフロントでのソートを短縮。mysqlはクエリキャッシュとかもあるはずだからまあ良さげちゃうか?
query := `
SELECT
l.id,
COALESCE(SUM(r.reaction_count), 0) AS total_reactions,
COALESCE(SUM(lc.tip), 0) AS total_tips,
COALESCE(SUM(r.reaction_count), 0) + COALESCE(SUM(lc.tip), 0) AS score
FROM
livestreams l
LEFT JOIN
(SELECT livestream_id, COUNT(*) AS reaction_count
FROM reactions
GROUP BY livestream_id) r ON l.id = r.livestream_id
LEFT JOIN
livecomments lc ON l.id = lc.livestream_id
GROUP BY
l.id
ORDER BY
score ASC,
l.id ASC;
`
今は 23.04% なんか減った感じはする
なんか遅いな〜と思ったら、一回のプロファイルではでかく出てる Stat はそこまで呼び出し多くないから、他を調査しないといけないくさい
pprofile は単一リクエストのプロファイルは取れるが、全体となると別が良さそうか
Icon対応
user_handler の以下の部分はだいぶ厳しいと思われる
iconHash := sha256.Sum256(image)
とりあえず no Image の時は固定値を返すとして、DBに値も保存しておきたいので、Icon作成時に保存するのが良さそうか。
Obserbability
https://trap.jp/post/2046/ ベンチマーカー側が cookie 保存するのでトラッキングID仕込んでユーザー遷移を追う感じっぽい
これは自作するのは大変そうだけど、正味 observability の出来が一番影響しそうなので他の人のを参考にしてみる
mazrean/isucrud: ISUCON用DBに対するCRUD操作周りのコールフロー可視化ツール https://github.com/mazrean/isucrud
isucrud .
こういうのができた すごいツールだ
NodeGarph ツール考える
ユーザーリクエストのトレース、分散トレーシングは必須 トレースのためには trace ID が必要
1.AWS Distro for OpenTelemetryを試してみた https://zenn.dev/kobayashi_m42/articles/try-aws-distro-for-opentelemetry
1.Trace Context https://www.w3.org/TR/trace-context/
The traceparent header represents the incoming request in a tracing system in a common format, understood by all vendors. Here’s an example of a traceparent header.
The `traceparent` HTTP header field identifies the incoming request in a tracing system. It has four fields:
- `version`
- `trace-id`
- `parent-id`
- `trace-flags`
これ自作はキッツイから library が欲しい
trace golang node graph 今だと世の中的には Graphana で可視化一択っぽいか..?
参考実装
https://gammalab.net/blog/43uqjmpr8qx/
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
"go.opentelemetry.io/otel/attribute"
install https://opentelemetry.io/docs/languages/go/getting-started/
go get "go.opentelemetry.io/otel" \
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" \
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace" \
"go.opentelemetry.io/otel/exporters/stdout/stdoutlog" \
"go.opentelemetry.io/otel/sdk/log" \
"go.opentelemetry.io/otel/log/global" \
"go.opentelemetry.io/otel/propagation" \
"go.opentelemetry.io/otel/sdk/metric" \
"go.opentelemetry.io/otel/sdk/resource" \
"go.opentelemetry.io/otel/sdk/trace" \
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"\
"go.opentelemetry.io/contrib/bridges/otelslog"
これがいいらしい https://pkg.go.dev/go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho
go get go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho
参考実装はできたけど、これ exporter でローカル graphana とか別の graphana に飛ばさないとダメな気がする
func main() {
runtime.SetBlockProfileRate(1)
runtime.SetMutexProfileFraction(1)
go func() {
log.Fatal(http.ListenAndServe("localhost:6060", nil))
}()
e := echo.New()
e.Debug = true
e.Logger.SetLevel(echolog.DEBUG)
e.Use(middleware.Logger())
cookieStore := sessions.NewCookieStore(secret)
cookieStore.Options.Domain = "*.u.isucon.dev"
e.Use(session.Middleware(cookieStore))
// e.Use(middleware.Recover())
// OTEL // OTEL// OTEL// OTEL
// tracerの初期化
cleanup, err := setupTraceProvider("example-service", "1.0.0")
if err != nil {
panic(err)
}
defer cleanup()
// /metrics と /health は計測しないようにする
skipper := otelecho.WithSkipper(
func(c echo.Context) bool {
return c.Path() == "/metrics" || c.Path() == "/health"
},
)
e.Use(otelecho.Middleware("api", skipper))
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Skipper: func(c echo.Context) bool {
return c.Path() == "/metrics" || c.Path() == "/health"
},
Format: `{"time":"${time_rfc3339_nano}",${custom},"remote_ip":"${remote_ip}",` +
`"host":"${host}","method":"${method}","uri":"${uri}","status":${status},` +
`"error":"${error}","latency":${latency},"latency_human":"${latency_human}",` +
`"bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n",
CustomTagFunc: func(c echo.Context, buf *bytes.Buffer) (int, error) {
span := trace.SpanFromContext(c.Request().Context())
buf.WriteString(fmt.Sprintf("\"%s\":\"%s\"", "traceID", span.SpanContext().TraceID().String()))
buf.WriteString(fmt.Sprintf(",\"%s\":\"%s\"", "spanID", span.SpanContext().SpanID().String()))
return 0, nil
},
}))
// OTEL// OTEL// OTEL// OTEL
// 初期化
e.POST("/api/initialize", initializeHandler)
// top
e.GET("/api/tag", getTagHandler)
e.GET("/api/user/:username/theme", getStreamerThemeHandler)
// livestream
// reserve livestream
e.POST("/api/livestream/reservation", reserveLivestreamHandler)
// list livestream
e.GET("/api/livestream/search", searchLivestreamsHandler)
e.GET("/api/livestream", getMyLivestreamsHandler)
e.GET("/api/user/:username/livestream", getUserLivestreamsHandler)
// get livestream
e.GET("/api/livestream/:livestream_id", getLivestreamHandler)
// get polling livecomment timeline
e.GET("/api/livestream/:livestream_id/livecomment", getLivecommentsHandler)
// ライブコメント投稿
e.POST("/api/livestream/:livestream_id/livecomment", postLivecommentHandler)
e.POST("/api/livestream/:livestream_id/reaction", postReactionHandler)
e.GET("/api/livestream/:livestream_id/reaction", getReactionsHandler)
// (配信者向け)ライブコメントの報告一覧取得API
e.GET("/api/livestream/:livestream_id/report", getLivecommentReportsHandler)
e.GET("/api/livestream/:livestream_id/ngwords", getNgwords)
// ライブコメント報告
e.POST("/api/livestream/:livestream_id/livecomment/:livecomment_id/report", reportLivecommentHandler)
// 配信者によるモデレーション (NGワード登録)
e.POST("/api/livestream/:livestream_id/moderate", moderateHandler)
// livestream_viewersにINSERTするため必要
// ユーザ視聴開始 (viewer)
e.POST("/api/livestream/:livestream_id/enter", enterLivestreamHandler)
// ユーザ視聴終了 (viewer)
e.DELETE("/api/livestream/:livestream_id/exit", exitLivestreamHandler)
// user
e.POST("/api/register", registerHandler)
e.POST("/api/login", loginHandler)
e.GET("/api/user/me", getMeHandler)
// フロントエンドで、配信予約のコラボレーターを指定する際に必要
e.GET("/api/user/:username", getUserHandler)
e.GET("/api/user/:username/statistics", getUserStatisticsHandler)
e.GET("/api/user/:username/icon", getIconHandler)
e.POST("/api/icon", postIconHandler)
// stats
// ライブ配信統計情報
e.GET("/api/livestream/:livestream_id/statistics", getLivestreamStatisticsHandler)
// 課金情報
e.GET("/api/payment", GetPaymentResult)
e.HTTPErrorHandler = errorResponseHandler
// DB接続
conn, err := connectDB(e.Logger)
if err != nil {
e.Logger.Errorf("failed to connect db: %v", err)
os.Exit(1)
}
defer conn.Close()
dbConn = conn
dbConn.SetMaxOpenConns(10)
dbConn.SetMaxIdleConns(5)
subdomainAddr, ok := os.LookupEnv(powerDNSSubdomainAddressEnvKey)
if !ok {
e.Logger.Errorf("environ %s must be provided", powerDNSSubdomainAddressEnvKey)
os.Exit(1)
}
powerDNSSubdomainAddress = subdomainAddr
// HTTPサーバ起動
listenAddr := net.JoinHostPort("", strconv.Itoa(listenPort))
if err := e.Start(listenAddr); err != nil {
e.Logger.Errorf("failed to start HTTP server: %v", err)
os.Exit(1)
}
}
type ErrorResponse struct {
Error string `json:"error"`
}
func errorResponseHandler(err error, c echo.Context) {
c.Logger().Errorf("error at %s: %+v", c.Path(), err)
if he, ok := err.(*echo.HTTPError); ok {
if e := c.JSON(he.Code, &ErrorResponse{Error: err.Error()}); e != nil {
c.Logger().Errorf("%+v", e)
}
return
}
if e := c.JSON(http.StatusInternalServerError, &ErrorResponse{Error: err.Error()}); e != nil {
c.Logger().Errorf("%+v", e)
}
}
func setupTraceProvider(serviceName string, serviceVersion string) (func(), error) {
// otelのライブラリではexporterという枠組みで計測した情報をどこに送信するかを設定できる
// 今回はとりあえず標準出力に出してみる
exporter, err := stdouttrace.New(
stdouttrace.WithPrettyPrint(),
stdouttrace.WithWriter(os.Stderr),
)
if err != nil {
return nil, err
}
// ここでresourceを設定することで、全てのspanに共通のメタデータを設定できる
resource := resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(serviceName),
semconv.ServiceVersionKey.String(serviceVersion),
)
// 実際に計測するためのtracerProviderを作成する
tracerProvider := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithSampler(sdktrace.AlwaysSample()),
sdktrace.WithResource(resource),
)
otel.SetTracerProvider(tracerProvider)
// tracerProviderを終了するための関数を返す
cleanup := func() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tracerProvider.Shutdown(ctx)
}
return cleanup, nil
}
graphana install
https://grafana.com/docs/grafana/latest/setup-grafana/installation/debian/
sudo apt-get install -y apt-transport-https software-properties-common wget
run
sudo /bin/systemctl start grafana-server
VSCode で portforwading するとすぐ見れる
sudo cat /etc/grafana/grafana.ini
initial user は admin, admin で入れる
ログインできた
otel から graphana に飛ばす
あ〜これはやめてログをファイルに出して、その読み取りは自作した方がいいかも… Grafana の依存関係多いし
file exporter 設定必要そう
alpha だけどこれ使うか https://pkg.go.dev/github.com/open-telemetry/opentelemetry-collector-contrib/exporter/fileexporter
github.com/open-telemetry/opentelemetry-collector-contrib/exporter/fileexporter
go get github.com/open-telemetry/opentelemetry-collector-contrib/exporter/fileexporter
curl https://pipe.u.isucon.dev/ --resolve pipe.u.isucon.dev:443:35.72.35.148
作った https://github.com/mikanbox/isuexporter
こいつを読み込む
// tracerの初期化
cleanup, err := isuexporter.TraceFileProvider("output.json", "isucon13", "0.0.0")
if err != nil {
panic(err)
}
defer cleanup()