- 投稿日:2020-08-10T23:17:17+09:00
Go言語アプリのDockerイメージをscratchで軽量化してみた
こんばんは、ねじねじおです。
Go言語で書いたアプリの Docker イメージを軽量化するには、マルチステージビルドを使って scratch をベースに構築するのがよいと聞いて、やってみました。
準備
まず、サンプルとして小さな Web API を echo で作ります。
$ mkdir app $ cd app $ go mod init example.com/example $ go get github.com/labstack/echo/v4下記のディレクトリ構成で server.go と Dockerfie を追加します。
-example |-app │-Dockerfile │-go.mod │-go.sum |-server.goserver.gopackage main import ( "net/http" "time" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) func main() { // Echo instance e := echo.New() // Middleware e.Use(middleware.Logger()) e.Use(middleware.Recover()) // Routes e.GET("/", func (c echo.Context) error { return c.JSON(http.StatusOK, []string{ "Hello, World!", time.Now().Format(time.RFC3339), }) }) // Start server e.Logger.Fatal(e.Start(":1323")) }DockerfileFROM golang:1.14.7 WORKDIR /go/src/app COPY ./ ./ RUN go mod download RUN go build -o ./server ./server.goビルドして実行してみます。
$ docker build ./ -t example $ docker run -e TZ=Asia/Tokyo -p 1323:1323 example ./server動作確認。
$ curl http://localhost:1323/ ["Hello, World!","2020-08-10T19:51:15+09:00"]成功!!
では、イメージサイズを確認してみます。
$ docker images example REPOSITORY TAG IMAGE ID CREATED SIZE example latest 5ef805ca57ec 2 minutes ago 908MB
908MB って、でかいのかな?
基準がわからないので、レイヤーを確認します。$ docker history example IMAGE CREATED CREATED BY SIZE COMMENT 5ef805ca57ec 2 minutes ago /bin/sh -c go build -o ./server ./server.go 20.5MB 13249ee42cff 2 minutes ago /bin/sh -c go mod download 77.3MB a3e0d3e82f45 3 minutes ago /bin/sh -c #(nop) COPY dir:cccd1ec30e0093efe… 4.96kB aa1a8385bb42 44 hours ago /bin/sh -c #(nop) WORKDIR /go/src/app 0B baaca3151cdb 3 days ago /bin/sh -c #(nop) WORKDIR /go 0B ... 省略 ...マルチステージビルドを使うと、go mod download と go build の領域を節約できそうです。
マルチステージビルドを使う
Dockerfileを変更します。
builderステージでコンパイル、できたバイナリファイルをコピーしてproductionステージを作ります。FROM golang:1.14.7 AS base WORKDIR /go/src/app FROM base AS builder COPY ./ ./ RUN go mod download RUN go build -o ./server ./server.go FROM base AS production COPY --from=builder /go/src/app/server ./ビルドします。
$ docker build ./ -t exampleイメージのサイズとレイヤーを確認。
$ docker images example REPOSITORY TAG IMAGE ID CREATED SIZE example latest 62b3a8a02af3 3 minutes ago 822MB $ docker history example IMAGE CREATED CREATED BY SIZE COMMENT 62b3a8a02af3 3 minutes ago /bin/sh -c #(nop) COPY file:3d74d9674cd3943c… 12.2MB aa1a8385bb42 44 hours ago /bin/sh -c #(nop) WORKDIR /go/src/app 0B baaca3151cdb 3 days ago /bin/sh -c #(nop) WORKDIR /go 0B ... 省略 ...イメージサイズは、822MB。少し小さくなりましたね。
動作確認です。
$ docker run -e TZ=Asia/Tokyo -p 1323:1323 example ./server $ curl http://localhost:1323/ ["Hello, World!","2020-08-10T19:58:28+09:00"]成功!
しかし、先ほどの docker history の出力をみると、バイナリをCOPYしてくるレイヤーは、たかだか 12.2MB 。
対して、イメージ全体のサイズは、822MB。
ベースのイメージが大きいのですね。scratch をベースに最終イメージを構築する
Goの実行環境にはGoは不要なので、ミニマムに scratch をベースに最終イメージを構築してみます。
失敗 その1
DockerfileFROM golang:1.14.7 AS base WORKDIR /go/src/app FROM base AS builder COPY ./ ./ RUN go mod download RUN go build -o ./server ./server.go FROM scratch AS production WORKDIR /go/bin COPY --from=builder /go/src/app/server ./ビルドします。
$ docker build ./ -t exampleイメージのサイズとレイヤーを確認。
$ docker images example REPOSITORY TAG IMAGE ID CREATED SIZE example latest e1ba3f7f81f0 About a minute ago 12.2MB $ docker history example IMAGE CREATED CREATED BY SIZE COMMENT e1ba3f7f81f0 2 minutes ago /bin/sh -c #(nop) COPY file:3d74d9674cd3943c… 12.2MB 06d1e3bea55e 12 minutes ago /bin/sh -c #(nop) WORKDIR /go/bin 0Bイメージサイズは、 12.2MB 。
劇的に小さくなりました!!動作確認です。
$ docker run -e TZ=Asia/Tokyo -p 1323:1323 example ./server standard_init_linux.go:211: exec user process caused "no such file or directory"エラーが発生して、起動できない。
いろいろ調べて見ると、builder と production で実行環境が異なるので、コンパイル時に CGO を無効にする必要がありました。失敗 その2
コンパイル時に CGO を無効にするように変更して再チャレンジです。
DockerfileFROM golang:1.14.7 AS base WORKDIR /go/src/app FROM base AS builder COPY ./ ./ RUN go mod download RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./server ./server.go FROM scratch AS production WORKDIR /go/bin COPY --from=builder /go/src/app/server ./ビルドして実行します。
$ docker build ./ -t example $ docker run -e TZ=Asia/Tokyo -p 1323:1323 example ./server起動しました!
動作確認です。$ curl http://localhost:1323/ ["Hello, World!","2020-08-10T11:53:39Z"]成功!
いや、否。
タイムゾーンが無視されている。。
タイムゾーンは、 アジアの都市、東京 です。"+09:00" です。そして成功へ
こちらのサイトに答えが書いてありました。
Using local time in a Golang Docker container built from Scratch今回は、builder ステージがから /usr/share/zoneinfo をコピーする方法を採用しました。
DockerfileFROM golang:1.14.7 AS base WORKDIR /go/src/app FROM base AS builder COPY ./ ./ RUN go mod download RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./server ./server.go FROM scratch AS production WORKDIR /go/bin COPY --from=builder /go/src/app/server ./ COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfoビルドして実行します。
$ docker build ./ -t example $ docker run -e TZ=Asia/Tokyo -p 1323:1323 example ./server動作確認です。
$ curl http://localhost:1323/ ["Hello, World!","2020-08-10T21:11:29+09:00"]タイムゾーンが反映されています!
イメージのサイズを確認します。
$ docker images example REPOSITORY TAG IMAGE ID CREATED SIZE example latest e673e01668c6 14 minutes ago 13.3MB $ docker history example IMAGE CREATED CREATED BY SIZE COMMENT e673e01668c6 14 minutes ago /bin/sh -c #(nop) COPY dir:b39c255c3688d7205… 1.21MB 3865c579a2d0 25 minutes ago /bin/sh -c #(nop) COPY file:bafd1cb7f8670973… 12.1MB 06d1e3bea55e 34 minutes ago /bin/sh -c #(nop) WORKDIR /go/bin 0B13.3 MB。
OK。
ねじねじおでした。
- 投稿日:2020-08-10T18:40:13+09:00
Goを使ってコマンドプロンプト(cmd)のコマンドを実行
GoはPythonみたいに使えてしかも環境に合わせたバイナリを作成してくれるやばい言語
最近Goにはまっていまして単純なことをしようとしてもちょっと詰まったりしたので
備忘録としてここに残しておきます。Goを使ってコマンドプロンプト(cmd)のコマンドを実行
以下で実行できます。あらかじめcmdを「cmd /C」しておくところがポイントです。
package main import ( "fmt" "os/exec" "time" "log" ) func main() { out, err := exec.Command("cmd", "/C", "echo hello > test.txt").Output() if err != nil { log.Fatal(err) }else{ fmt.Println(out) } }改訂履歴
- 2020/8/10 新規作成
- 投稿日:2020-08-10T18:36:04+09:00
Goのfyneで日本語を文字化けさせない手っ取り早い方法
fyneでサンプルアプリを動かそうとしたら文字化けに遭遇
GOについて暇さえあれば勉強している私です。将来的にはGUIのアプリで設定ファイルを作成するものを製作し会社のサービスとして提供する予定です。
そのためによいGUIのライブラリはないかと探していたところfyneを見つけました
早速以下のようなサンプルを以下の環境で作ったところフォントが文字化けしました。
- OS:Windows10
- Go:go version go1.14.7 windows/amd64
package main import ( "fyne.io/fyne/widget" "fyne.io/fyne/app" ) func main() { app := app.New() w := app.NewWindow("こんにちは") w.SetContent(widget.NewVBox( widget.NewLabel("こんにちは Fyne!"), widget.NewButton("Quit", func() { app.Quit() }), )) w.ShowAndRun() }解決方法としては環境変数FYNE_FONTを設定すればいいそうです。
以下のように設定しました。
set FYNE_FONT=C:\Windows\Fonts\meiryo.ttcこれで普通に文字化けせずに表示されます。
Goはやっぱりすごいです。Flutterも覚えればデスクトップ、モバイル共に環境に左右されないアプリ製作がはかどりそうです。
改訂履歴
- 2020/8/10 新規作成
- 投稿日:2020-08-10T18:07:30+09:00
Github ActionsでGoのCI環境を作成する
はじめに
みなさんCIしてますか?
ちなみに僕はCIは大好きですが、それの環境構築をするのは大嫌いです。
毎回細かい設定をしたり、ごちゃごちゃといろんなことをやらなければいけないのは本当に憂鬱です。
ということで今回は新しくサービスを作成するときに必要であろうGoのアプリケーションのCIをGithub Actionsで簡単にやりたいと思います!Github Actionsについて簡単に、、、
Github ActionsはGitHubが2019年11月に正式に公開したGitHub上のリポジトリやイシューに対するさまざまな操作をトリガーとしてあらかじめ定義しておいた処理を実行できる機能です。
これまでの
- Circle CI
- Jenkins
の兄弟みたいな物だと思ってもらえるといいと思います!
またGithub Actionsは
.github/workflows/
配下にymlファイルを作成することで処理を実行してくれます├── .github │ └── workflows │ └── ci.yml今回のゴール
今回はあくまで最低限のci環境を構築するだけなのでやることは
- buildできる
- lintを通す
- testを通す
この3つだけです!
もちろんこのあとDockerHubにあげたり、リリースまで行ったりと、やれることはたくさんあると思いますがいったん今回は省略です。Actionを実行するファイルの実装
ci.ymlname: go-ci on: [push] jobs: # 共通処理 setup: runs-on: ubuntu-latest steps: - name: set up uses: actions/setup-go@v2 with: go-version: ^1.13 id: go - name: check out uses: actions/checkout@v2 # 処理を高速化するために環境をキャッシュ - name: Cache uses: actions/cache@v2.1.0 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- # buildテスト build: needs: setup runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: build run: go build ./... # testを通す test: needs: setup runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: test run: go test ./... -v # lintを通す lint: needs: setup runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: golangci-lint uses: golangci/golangci-lint-action@v1 with: version: v1.29パッと見かなりいろんなパラメータがあって0から全てを作っていくのはとても難しそうなイメージがありますが、、、
実はそんなことありません。Github Actionsは既にかなりの数のテンプレートが存在するので基本的にそれらのテンプレを組み合わせるだけです。
(上記のコードもほぼそれらのテンプレートをそのまま使っています。)具体的には、、、
# 共通処理
-> setup-go
# 処理を高速化するために環境をキャッシュ
-> actions/cache
# lintを通す
-> golangci/golangci-lint-actionを参考にしています。
(build, test処理はGoの標準コマンドを活用しているので省略します)上記actionsでやってることとしては、、、
# 共通処理
となってるところでgoの環境のセットアップ
↓
共通処理終了後build
,test
,lint
の処理を並行で実行というような感じです
最終的にプルリクエストを出したときにこのようにワークフローが回っていることが確認できれば成功です!おわりに
Github Actionsがリリースされてからまだ日が浅いこともありベストプラクティスが確立されてないところや、OSSのスター数がどれも少ないところなど多少不安な点もありつつもCIをもGithub内で完結できるのはとても魅力的ですし、他のCIツールを使ったことがある方であれば学習コストもほとんどかからないと思うので、1度は触った方がいいのかなと思っています。
- 投稿日:2020-08-10T15:29:01+09:00
Visual Studio Code + WSL2でGo開発環境構築
WindowsからTeraTerm等のターミナルソフトを使って、Linuxサーバにssh接続し、Linux上で開発をすることは、一般的に行っていると思う。しかし、Windows用のVS CodeのIntellisenseやdebug機能を使いたい!と思うこともよくある。そんなことが、WSL2を使うことで簡単にできちゃうので、紹介してみる。
インストール
Remote Development Extention
VS Codeの画面左側の
Extensions(Ctrl + Shift + X)
メニューで、Remote Development
を検索し、インストール。とっても簡単。WSL2 + Linux Distribution
ググればわかりやすいページがたくさんヒットするので、頑張りましょう。なお、私も書いてたりします。
Windows 10でLinuxを使う(WSL2)Go tools
WSL2上のLinuxにGo Toolsをインストールする。Distributionで用意しているパッケージでもいいかもしれないが、私は何となく最新版をインストールした。Getting Started - The Go Programming Languageを見れば、簡単にインストールできる。
インストール後、$HOME/.profile
に以下のPATH設定をお忘れなく。GOPATH="$HOME/go" PATH="$PATH:/usr/local/go/bin:$GOPATH/bin"Go on Linux on WSL2
モジュール初期化
Linux上で、以下のコマンドを実行し、モジュールの初期化を行う。
mkdir hello cd hello go mod init helloVS Code起動
上記で作成した
hello
ディレクトリで、以下のコマンドを実行。Windows上でVS Codeがhello
ディレクトリをオープンした状態で起動する。code .VS Codeの画面左下に
WSL: XXXXX
と表示されていれば成功。Go Extentionのインストール
VS Codeで、
hello.go
を作成すると、Go Extention
のインストールを勧められるので、Install
をクリック。インストール完了後、
Reload Required
をクリック。この後、ソースコードの編集、実行時に
Update
やInstall
の問い合わせが何回かあるが、基本的に言われるがままに実行する。Go build
ソースの編集後、
Ctrl + F5
で、コマンドパレットが開くのでGo
を選択。hello.gopackage main import ( "fmt" ) func main() { msg := "Hello, Wold!" fmt.Println(msg) }下記のように、
DEBUG CONSOLE
にHello, World!
と表示されればOK。
Ctrl + F5
を実行するたびに、Go
を選択するのは面倒なので、画面左のRun(Ctrl + Shift + D)
メニューをクリックし、create a launch.json file
をクリックし、コマンドパレットからGo
を選択。
launch.json
ファイルが開くが、編集は不要なので、そのまま閉じる。これで、Ctrl + F5
実行時の問い合わせがなくなる。Debug
F5
キーによりデバッグもできる。ソースコードの行番号の左側をクリックすることでブレークポイントを設定。F5
キーを実行すると、デバッグ開始しブレークポイントで処理が止まる。あとは、Continue(F5)
、Step Over(F10)
、Step Into(F11)
等で遊びましょう。リンク
- 投稿日:2020-08-10T14:15:31+09:00
AzureでWebアプリを稼働させる「App Service」とGo
Azureには、Webアプリを稼働させるPaaSとして「App Service」が用意されています。
通常は、WebアプリをWindows上でネイティブに稼働させる「App Service(App Service on Windows)」となります。
WebアプリをDocker化してLinuxコンテナーとして稼働させる「Web App for Containers(App Service on Linux)」も用意されています。
当初、Goは「App Service」にて実験サポートされていたのですが、2017年12月に対象外となりました(T_T)
そのため、現在Goで作成したWebアプリは「Web App for Containers」で稼働させることになっています。
これでDockerを学ぶ理由ができたので、よしとしま、しょうか、ねぇ...q@w@p
- 投稿日:2020-08-10T09:43:19+09:00
AtCoder Grand Contest初挑戦
8/9(日)、初のAtCoder Grand Contestを挑戦いたしました。
勉強中のgo言語を利用して、「C - Product Modulo」だけを実施したが、どうしても、実行時間が2秒以内に抑えず(実績:2205 ms)、初挑戦が失敗した。
package main import ( "fmt" ) func main() { var n int fmt.Scanf("%d", &n) slice := make([]int,n,n) for i := 0; i < n; i++ { fmt.Scanf("%d", &slice[i]) } total:=0 for i:=0;i<n-1;i++ { if slice[i] > 0 { for j:=1+i;j<n;j++ { if slice[j] >0 { total += slice[i]*slice[j]%200003 } } } } fmt.Println(total) }処理時間がオーバーする原因はforループの利用が多かったかなと思いますが、解決策は分からないです。
- 投稿日:2020-08-10T01:41:31+09:00
Go言語のhttp.Handle、http.HandleFuncを雑に理解する
はじめに
最近Goに入門しました。
http.Handle
、http.HandleFunc
、http.Handler
、http.HandlerFunc
についてまとめられている記事は溢れていますが、結局何パターンの書き方があるのかまとまっている記事がなかったので、まとめておきます。対象は、筆者と同じぐらいのGo言語初心者を想定しています。実装
下の5パターンが考えられる全てだと結論付けました。他の方法を知っている方がいらっしゃいましたら教えていただきたいです。
package main import "net/http" type one struct { } func (h *one) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte("one\n")) } func two(w http.ResponseWriter, r *http.Request) { w.Write([]byte("two\n")) } func three(w http.ResponseWriter, r *http.Request) { w.Write([]byte("three\n")) } func main() { http.Handle("/one", &one{}) http.Handle("/two", http.HandlerFunc(two)) // http.Handle("/two", two) <- これはだめ http.HandleFunc("/three", three) http.HandleFunc("/four", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("four\n")) }) http.HandleFunc("/five", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("five\n")) })) http.ListenAndServe(":1991", nil) }解説
(*それぞれのエンドポイントに対応して、1の実装、2の実装のように表現します。)
順序立てれば、それほど難しいものでもないことが分かると思います。
- エンドポイントを作成する方法は、
http.Handle
とhttp.HandleFunc
の二つ。http.HandlerFunc
はfunc(w http.ResponseWriter, r *http.Request)
の別名。ただし、ServeHTTP
メソッドが定義されている。http.Handle
の第2引数はHandler
型で、ServeHTTP
を実装している必要がある。http.HandleFunc
の第2引数は、結局http.HandlerFunc
にキャストされる。この4つを覚えておけば、理解できると思います。まず、1の実装は自分ので定義した
struct
にServeHTTP
を実装しているので、問題ないです。2つめの実装ですが、単純な関数をhttp.HandlerFunc
型にキャストしているので、ServeHTTP
が実装され、問題ないです。逆に言うと、http.Handle
の第2引数には、単純なfunc(w http.ResponseWriter, r *http.Request)
型の関数を渡すとエラーです。3つめの実装と、4つめの実装は、無名関数かどうかの違いでしかなく、4.で書いているように、http.HandleFunc
にキャストされるため、通常の関数を渡しても問題ないです。もちろん5つ目の実装のように前もってキャストしても問題ないです。まとめ
- エンドポイントを作成する方法は、
http.Handle
とhttp.HandleFunc
の二つがある。http.Handle
の第2引数は、Handler
インターフェースを実装した(ServeHTTP
メソッドが定義されている)ものを渡せば良い。http.HandleFunc
の第2引数は、func(w http.ResponseWriter, r *http.Request)
型の関数を渡せば良い。Go言語は公式のドキュメントがしっかりしているので、良いですね。
参考
- 投稿日:2020-08-10T00:28:11+09:00
[Go] pprofの使い方が未だにイマイチよくわかってない人のための記事
はじめに
Go言語でのプロファイリングといえば
go tool pprof
ですがコマンドの仕様と使い方がわかりづらく感じていたので不明瞭であった点をまとめました。TL;DR
- プロファイリング方法として1. go testのベンチマークで取る方法と、2. コードに追記する方法がある。
- 2. コードに追記する方法の中に、2-1.
"runtime/pprof"
と、2-2."net/http/pprof"
の2つがある。pprof [-option] [binary] <source>
という使い方においてそれぞれ以下にように把握しよう
- sourceはプロファイル元リソース。生成されたプロファイルファイルだったりURLリソースだったり。
- optionはどのようにプロファイル結果を可視化するか のためのオプション。指定しないとプロンプトが出てくる。
- binaryはsourceのコード箇所を名前付け(symbolization)するためのオプション。ほとんど場合不要(のはずです)。
プロファイリングのやり方
1. go testのベンチマークを取る方法
go test
でサポートされているGoのベンチマークでプロファイル用のオプションをつけるとプロファイル結果ファイルが生成されます。ベンチマークなテストファイルがあるディレクトリにて以下のように使います。
$ ls -1 bench_test.go main.go$ go test -cpuprofile cpu.prof -memprofile mem.prof -bench . goos: darwin goarch: amd64 BenchmarkVar-4 302041 3462 ns/op BenchmarkPointer-4 113582 10360 ns/op PASS ok _/Users/shintaro/.ghq/github.com/momotaro98/go-codes-for-learning/go-comparison/var_vs_pointer 2.881s$ ls -1 bench_test.go cpu.prof main.go mem.prof dirname.testちなみに、
-bench BenchmarkVar
とすることでプロファイルしたいテスト関数のみを指定できます。2. コードに追記する方法
2-1. "runtime/pprof" の利用
runtime/pprof
ではプロファイルで分析したい処理セグメントに対してStartCPUProfile
関数とStopCPUProfile
関数で挟んで利用します。runtime.pprof.gopackage main import ( "os" "runtime/pprof" "time" ) func run() error { f, err := os.Create("cpu.pprof") if err != nil { return err } if err := pprof.StartCPUProfile(f); err != nil { return err } defer pprof.StopCPUProfile() process := func() { time.Sleep(time.Second * 5) } process() return nil } func main() { if err := run(); err != nil { panic(err) } }$ go run runtime.pprof.go $ ls -1 cpu.pprof runtime.pprof.goruntime/pprofの公式ドキュメントを見るとラベリングをしたりする機能もあるようです。
2-2. "net/http/pprof" の利用
HTTPサーバなど定常的に動いているプログラムに対して一定時間の間プロファイルしたい場合は
net/http/pprof
を利用します。
import
に_ net/http/pprof
を追加し、対象の定常的なプログラムにgo func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }()を追加することで、
http://localhost:6060/debug/pprof
というエンドポイントが開放されます。特に
http://localhost:6060/debug/pprof/profile
が一番良く利用されるエンドポイントで、下記のようなpprofコマンドを叩いた時点からプロファイルを開始します。デフォルトでは30秒間プロファイルを実施し、終了すると結果ファイルを出力します。秒間は指定することが可能です。プログラム開始$ go run net.http.pprof.go awaiting signal working with &{st:test} working with &{st:test} working with &{st:test}別ターミナルにてプロファイル開始。10秒後にプロファイル結果ファイルが出力される。$ go tool pprof http://localhost:6060/debug/pprof/profile?seconds=10 Fetching profile over HTTP from http://localhost:6060/debug/pprof/profile?seconds=10 Saved profile in /Users/momotaro98/pprof/pprof.samples.cpu.001.pb.gznet.http.pprof.gopackage main import ( "fmt" "log" "net/http" _ "net/http/pprof" "os" "os/signal" "syscall" "time" ) func main() { go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() sigs := make(chan os.Signal, 1) done := make(chan struct{}) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) var s = &struct { st string }{ st: "test", } // 定常的に動くプログラム go func() { for { time.Sleep(time.Second * 1) select { case <-sigs: fmt.Println("signal has come") done <- struct{}{} break default: fmt.Printf("working with %+v\n", s) } } }() fmt.Println("awaiting signal") <-done fmt.Println("exiting") }net/http/pprofについて詳細を追ってみたので、備考として最下部に記載しています。
プロファイリング結果の分析のやり方
プロファイリング結果ファイルが生成後の分析方法については様々な記事でわかりやすく書かれているので本記事では割愛にします。
本家pprofのREADMEがわかりやすくそのまま参照できます。
備考 "net/http/pprof" の実装を追ってみる
import _ "net/http/pprof"
で何をしているかnet/http/pprofの利用においては
import
の中で_ "net/http/pprof"
をしますが、これにより下記のnet/http/pprofパッケージ内のinit()
関数が実行されます。golang.org/src/net/http/pprof/pprof.gofunc init() { http.HandleFunc("/debug/pprof/", Index) http.HandleFunc("/debug/pprof/cmdline", Cmdline) http.HandleFunc("/debug/pprof/profile", Profile) http.HandleFunc("/debug/pprof/symbol", Symbol) http.HandleFunc("/debug/pprof/trace", Trace) }つまりこれは、
net/http
パッケージ内のDefaultServeMux
に対してハンドラー追加していることになるので、http.ListenAndServe(":6060", nil)
とnil指定で立ち上げることで/debug/pprof/
以下のパスはnet/http/pprofのハンドラにさばかれることになります。そのため、指定のポートは6060である必要は全くありません。上記の内容はnet/http/pprofの公式ドキュメントにきっちりと記載されています。
/debug/pprof/profile
はruntime.pprof
のStartCPUProfile
を呼んでいる。メインの
/debug/pprof/profile
のエンドポイントのハンドラ関数はruntime/pprof
のStartCPUProfile
を呼ぶラッパーであることがわかります。golang.org/src/net/http/pprof/pprof.gofunc Profile(w http.ResponseWriter, r *http.Request) { // 【前略】 sec, err := strconv.ParseInt(r.FormValue("seconds"), 10, 64) if sec <= 0 || err != nil { sec = 30 } // 【中略】 if err := pprof.StartCPUProfile(w); err != nil { // StartCPUProfile failed, so no writes yet. serveError(w, http.StatusInternalServerError, fmt.Sprintf("Could not enable CPU profiling: %s", err)) return } sleep(w, time.Duration(sec)*time.Second) pprof.StopCPUProfile() }参考
- http://klabgames.tech.blog.jp.klab.com/archives/pprof1-cpuprofile.html
- https://docs.google.com/presentation/d/1Fghxd31leQN2N2dW9h9D0KivPSSh-ROnzlsdqfsaGVw/pub
- https://chromium.googlesource.com/external/github.com/google/pprof/+/HEAD/doc/README.md
- https://users.rust-lang.org/t/pprof-wont-show-symbols/24241
- 投稿日:2020-08-10T00:28:11+09:00
[Go] pprofの使い方を改めて整理
はじめに
Go言語でのプロファイリングといえば
go tool pprof
ですがコマンドの仕様と使い方がわかりづらく感じていたので不明瞭であった点をまとめました。TL;DR
- プロファイリング方法として1. go testのベンチマークで取る方法と、2. コードに追記する方法がある。
- 2. コードに追記する方法の中に、2-1.
"runtime/pprof"
と、2-2."net/http/pprof"
の2つがある。pprof [-option] [binary] <source>
という使い方においてそれぞれ以下にように把握しよう
- sourceはプロファイル元リソース。生成されたプロファイルファイルだったりURLリソースだったり。
- optionはどのようにプロファイル結果を可視化するか のためのオプション。指定しないとプロンプトが出てくる。
- binaryはsourceのコード箇所を名前付け(symbolization)するためのオプション。ほとんど場合不要(のはずです)。
プロファイリングのやり方
1. go testのベンチマークを取る方法
go test
でサポートされているGoのベンチマークでプロファイル用のオプションをつけるとプロファイル結果ファイルが生成されます。ベンチマークなテストファイルがあるディレクトリにて以下のように使います。
$ ls -1 bench_test.go main.go$ go test -cpuprofile cpu.prof -memprofile mem.prof -bench . goos: darwin goarch: amd64 BenchmarkVar-4 302041 3462 ns/op BenchmarkPointer-4 113582 10360 ns/op PASS ok _/Users/shintaro/.ghq/github.com/momotaro98/go-codes-for-learning/go-comparison/var_vs_pointer 2.881s$ ls -1 bench_test.go cpu.prof main.go mem.prof dirname.testちなみに、
-bench BenchmarkVar
とすることでプロファイルしたいテスト関数のみを指定できます。2. コードに追記する方法
2-1. "runtime/pprof" の利用
runtime/pprof
ではプロファイルで分析したい処理セグメントに対してStartCPUProfile
関数とStopCPUProfile
関数で挟んで利用します。runtime.pprof.gopackage main import ( "os" "runtime/pprof" "time" ) func run() error { f, err := os.Create("cpu.pprof") if err != nil { return err } if err := pprof.StartCPUProfile(f); err != nil { return err } defer pprof.StopCPUProfile() process := func() { time.Sleep(time.Second * 5) } process() return nil } func main() { if err := run(); err != nil { panic(err) } }$ go run runtime.pprof.go $ ls -1 cpu.pprof runtime.pprof.goruntime/pprofの公式ドキュメントを見るとラベリングをしたりする機能もあるようです。
2-2. "net/http/pprof" の利用
HTTPサーバなど定常的に動いているプログラムに対して一定時間の間プロファイルしたい場合は
net/http/pprof
を利用します。
import
に_ net/http/pprof
を追加し、対象の定常的なプログラムにgo func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }()を追加することで、
http://localhost:6060/debug/pprof
というエンドポイントが開放されます。特に
http://localhost:6060/debug/pprof/profile
が一番良く利用されるエンドポイントで、下記のようなpprofコマンドを叩いた時点からプロファイルを開始します。デフォルトでは30秒間プロファイルを実施し、終了すると結果ファイルを出力します。秒間は指定することが可能です。プログラム開始$ go run net.http.pprof.go awaiting signal working with &{st:test} working with &{st:test} working with &{st:test}別ターミナルにてプロファイル開始。10秒後にプロファイル結果ファイルが出力される。$ go tool pprof http://localhost:6060/debug/pprof/profile?seconds=10 Fetching profile over HTTP from http://localhost:6060/debug/pprof/profile?seconds=10 Saved profile in /Users/momotaro98/pprof/pprof.samples.cpu.001.pb.gznet.http.pprof.gopackage main import ( "fmt" "log" "net/http" _ "net/http/pprof" "os" "os/signal" "syscall" "time" ) func main() { go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() sigs := make(chan os.Signal, 1) done := make(chan struct{}) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) var s = &struct { st string }{ st: "test", } // 定常的に動くプログラム go func() { for { time.Sleep(time.Second * 1) select { case <-sigs: fmt.Println("signal has come") done <- struct{}{} break default: fmt.Printf("working with %+v\n", s) } } }() fmt.Println("awaiting signal") <-done fmt.Println("exiting") }net/http/pprofについて詳細を追ってみたので、備考として最下部に記載しています。
プロファイリング結果の分析のやり方
プロファイリング結果ファイルが生成後の分析方法については様々な記事でわかりやすく書かれているので本記事では割愛にします。
本家pprofのREADMEがわかりやすくそのまま参照できます。
備考 "net/http/pprof" の実装を追ってみる
import _ "net/http/pprof"
で何をしているかnet/http/pprofの利用においては
import
の中で_ "net/http/pprof"
をしますが、これにより下記のnet/http/pprofパッケージ内のinit()
関数が実行されます。golang.org/src/net/http/pprof/pprof.gofunc init() { http.HandleFunc("/debug/pprof/", Index) http.HandleFunc("/debug/pprof/cmdline", Cmdline) http.HandleFunc("/debug/pprof/profile", Profile) http.HandleFunc("/debug/pprof/symbol", Symbol) http.HandleFunc("/debug/pprof/trace", Trace) }つまりこれは、
net/http
パッケージ内のDefaultServeMux
に対してハンドラー追加していることになるので、http.ListenAndServe(":6060", nil)
とnil指定で立ち上げることで/debug/pprof/
以下のパスはnet/http/pprofのハンドラにさばかれることになります。そのため、指定のポートは6060である必要は全くありません。上記の内容はnet/http/pprofの公式ドキュメントにきっちりと記載されています。
/debug/pprof/profile
はruntime.pprof
のStartCPUProfile
を呼んでいる。メインの
/debug/pprof/profile
のエンドポイントのハンドラ関数はruntime/pprof
のStartCPUProfile
を呼ぶラッパーであることがわかります。golang.org/src/net/http/pprof/pprof.gofunc Profile(w http.ResponseWriter, r *http.Request) { // 【前略】 sec, err := strconv.ParseInt(r.FormValue("seconds"), 10, 64) if sec <= 0 || err != nil { sec = 30 } // 【中略】 if err := pprof.StartCPUProfile(w); err != nil { // StartCPUProfile failed, so no writes yet. serveError(w, http.StatusInternalServerError, fmt.Sprintf("Could not enable CPU profiling: %s", err)) return } sleep(w, time.Duration(sec)*time.Second) pprof.StopCPUProfile() }参考
- http://klabgames.tech.blog.jp.klab.com/archives/pprof1-cpuprofile.html
- https://docs.google.com/presentation/d/1Fghxd31leQN2N2dW9h9D0KivPSSh-ROnzlsdqfsaGVw/pub
- https://chromium.googlesource.com/external/github.com/google/pprof/+/HEAD/doc/README.md
- https://users.rust-lang.org/t/pprof-wont-show-symbols/24241
- 投稿日:2020-08-10T00:28:11+09:00
[Go] pprofの使い方を改めて整理した
はじめに
Go言語でのプロファイリングといえば
go tool pprof
ですがコマンドの仕様と使い方がわかりづらく感じていたので不明瞭であった点をまとめました。TL;DR
- プロファイリング方法として1. go testのベンチマークで取る方法と、2. コードに追記する方法がある。
- 2. コードに追記する方法の中に、2-1.
"runtime/pprof"
と、2-2."net/http/pprof"
の2つがある。pprof [-option] [binary] <source>
という使い方においてそれぞれ以下にように把握しよう
- sourceはプロファイル元リソース。生成されたプロファイルファイルだったりURLリソースだったり。
- optionはどのようにプロファイル結果を可視化するか のためのオプション。指定しないとプロンプトが出てくる。
- binaryはsourceのコード箇所を名前付け(symbolization)するためのオプション。ほとんど場合不要(のはずです)。
プロファイリングのやり方
1. go testのベンチマークを取る方法
go test
でサポートされているGoのベンチマークでプロファイル用のオプションをつけるとプロファイル結果ファイルが生成されます。ベンチマークなテストファイルがあるディレクトリにて以下のように使います。
$ ls -1 bench_test.go main.go$ go test -cpuprofile cpu.prof -memprofile mem.prof -bench . goos: darwin goarch: amd64 BenchmarkVar-4 302041 3462 ns/op BenchmarkPointer-4 113582 10360 ns/op PASS ok _/Users/shintaro/.ghq/github.com/momotaro98/go-codes-for-learning/go-comparison/var_vs_pointer 2.881s$ ls -1 bench_test.go cpu.prof main.go mem.prof dirname.testちなみに、
-bench BenchmarkVar
とすることでプロファイルしたいテスト関数のみを指定できます。2. コードに追記する方法
2-1. "runtime/pprof" の利用
runtime/pprof
ではプロファイルで分析したい処理セグメントに対してStartCPUProfile
関数とStopCPUProfile
関数で挟んで利用します。runtime.pprof.gopackage main import ( "os" "runtime/pprof" "time" ) func run() error { f, err := os.Create("cpu.pprof") if err != nil { return err } if err := pprof.StartCPUProfile(f); err != nil { return err } defer pprof.StopCPUProfile() process := func() { time.Sleep(time.Second * 5) } process() return nil } func main() { if err := run(); err != nil { panic(err) } }$ go run runtime.pprof.go $ ls -1 cpu.pprof runtime.pprof.goruntime/pprofの公式ドキュメントを見るとラベリングをしたりする機能もあるようです。
2-2. "net/http/pprof" の利用
HTTPサーバなど定常的に動いているプログラムに対して一定時間の間プロファイルしたい場合は
net/http/pprof
を利用します。
import
に_ net/http/pprof
を追加し、対象の定常的なプログラムにgo func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }()を追加することで、
http://localhost:6060/debug/pprof
というエンドポイントが開放されます。特に
http://localhost:6060/debug/pprof/profile
が一番良く利用されるエンドポイントで、下記のようなpprofコマンドを叩いた時点からプロファイルを開始します。デフォルトでは30秒間プロファイルを実施し、終了すると結果ファイルを出力します。秒間は指定することが可能です。プログラム開始$ go run net.http.pprof.go awaiting signal working with &{st:test} working with &{st:test} working with &{st:test}別ターミナルにてプロファイル開始。10秒後にプロファイル結果ファイルが出力される。$ go tool pprof http://localhost:6060/debug/pprof/profile?seconds=10 Fetching profile over HTTP from http://localhost:6060/debug/pprof/profile?seconds=10 Saved profile in /Users/momotaro98/pprof/pprof.samples.cpu.001.pb.gznet.http.pprof.gopackage main import ( "fmt" "log" "net/http" _ "net/http/pprof" "os" "os/signal" "syscall" "time" ) func main() { go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() sigs := make(chan os.Signal, 1) done := make(chan struct{}) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) var s = &struct { st string }{ st: "test", } // 定常的に動くプログラム go func() { for { time.Sleep(time.Second * 1) select { case <-sigs: fmt.Println("signal has come") done <- struct{}{} break default: fmt.Printf("working with %+v\n", s) } } }() fmt.Println("awaiting signal") <-done fmt.Println("exiting") }net/http/pprofについて詳細を追ってみたので、備考として最下部に記載しています。
プロファイリング結果の分析のやり方
プロファイリング結果ファイルが生成後の分析方法については様々な記事でわかりやすく書かれているので本記事では割愛にします。
本家pprofのREADMEがわかりやすくそのまま参照できます。
備考 "net/http/pprof" の実装を追ってみる
import _ "net/http/pprof"
で何をしているかnet/http/pprofの利用においては
import
の中で_ "net/http/pprof"
をしますが、これにより下記のnet/http/pprofパッケージ内のinit()
関数が実行されます。golang.org/src/net/http/pprof/pprof.gofunc init() { http.HandleFunc("/debug/pprof/", Index) http.HandleFunc("/debug/pprof/cmdline", Cmdline) http.HandleFunc("/debug/pprof/profile", Profile) http.HandleFunc("/debug/pprof/symbol", Symbol) http.HandleFunc("/debug/pprof/trace", Trace) }つまりこれは、
net/http
パッケージ内のDefaultServeMux
に対してハンドラー追加していることになるので、http.ListenAndServe(":6060", nil)
とnil指定で立ち上げることで/debug/pprof/
以下のパスはnet/http/pprofのハンドラにさばかれることになります。そのため、指定のポートは6060である必要は全くありません。上記の内容はnet/http/pprofの公式ドキュメントにきっちりと記載されています。
/debug/pprof/profile
はruntime.pprof
のStartCPUProfile
を呼んでいる。メインの
/debug/pprof/profile
のエンドポイントのハンドラ関数はruntime/pprof
のStartCPUProfile
を呼ぶラッパーであることがわかります。golang.org/src/net/http/pprof/pprof.gofunc Profile(w http.ResponseWriter, r *http.Request) { // 【前略】 sec, err := strconv.ParseInt(r.FormValue("seconds"), 10, 64) if sec <= 0 || err != nil { sec = 30 } // 【中略】 if err := pprof.StartCPUProfile(w); err != nil { // StartCPUProfile failed, so no writes yet. serveError(w, http.StatusInternalServerError, fmt.Sprintf("Could not enable CPU profiling: %s", err)) return } sleep(w, time.Duration(sec)*time.Second) pprof.StopCPUProfile() }参考
- http://klabgames.tech.blog.jp.klab.com/archives/pprof1-cpuprofile.html
- https://docs.google.com/presentation/d/1Fghxd31leQN2N2dW9h9D0KivPSSh-ROnzlsdqfsaGVw/pub
- https://chromium.googlesource.com/external/github.com/google/pprof/+/HEAD/doc/README.md
- https://users.rust-lang.org/t/pprof-wont-show-symbols/24241