20200810のGoに関する記事は11件です。

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.go
server.go
package 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"))
}
Dockerfile
FROM 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

Dockerfile
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 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 を無効にするように変更して再チャレンジです。

Dockerfile
FROM 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 をコピーする方法を採用しました。

Dockerfile
FROM 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               0B   

13.3 MB。

OK。
ねじねじおでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 新規作成
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 新規作成
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.yml
name: 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の処理を並行で実行

というような感じです

スクリーンショット 2020-08-10 18.02.31.png
最終的にプルリクエストを出したときにこのようにワークフローが回っていることが確認できれば成功です!

おわりに

Github Actionsがリリースされてからまだ日が浅いこともありベストプラクティスが確立されてないところや、OSSのスター数がどれも少ないところなど多少不安な点もありつつもCIをもGithub内で完結できるのはとても魅力的ですし、他のCIツールを使ったことがある方であれば学習コストもほとんどかからないと思うので、1度は触った方がいいのかなと思っています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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を検索し、インストール。とっても簡単。

image.png

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 hello

VS Code起動

上記で作成したhelloディレクトリで、以下のコマンドを実行。Windows上でVS Codeがhelloディレクトリをオープンした状態で起動する。

code .

VS Codeの画面左下にWSL: XXXXXと表示されていれば成功。

image.png

Go Extentionのインストール

VS Codeで、hello.goを作成すると、Go Extentionのインストールを勧められるので、Installをクリック。

image.png

インストール完了後、Reload Requiredをクリック。

image.png

この後、ソースコードの編集、実行時にUpdateInstallの問い合わせが何回かあるが、基本的に言われるがままに実行する。

Go build

ソースの編集後、Ctrl + F5で、コマンドパレットが開くのでGoを選択。

hello.go
package main

import (
    "fmt"
)

func main() {
    msg := "Hello, Wold!"
    fmt.Println(msg)
}

image.png

下記のように、DEBUG CONSOLEHello, World!と表示されればOK。

image.png

Ctrl + F5を実行するたびに、Goを選択するのは面倒なので、画面左のRun(Ctrl + Shift + D)メニューをクリックし、create a launch.json fileをクリックし、コマンドパレットからGoを選択。

image.png

launch.jsonファイルが開くが、編集は不要なので、そのまま閉じる。これで、Ctrl + F5実行時の問い合わせがなくなる。

Debug

F5キーによりデバッグもできる。ソースコードの行番号の左側をクリックすることでブレークポイントを設定。F5キーを実行すると、デバッグ開始しブレークポイントで処理が止まる。あとは、Continue(F5)Step Over(F10)Step Into(F11)等で遊びましょう。

image.png

リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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ループの利用が多かったかなと思いますが、解決策は分からないです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語のhttp.Handle、http.HandleFuncを雑に理解する

はじめに

最近Goに入門しました。http.Handlehttp.HandleFunchttp.Handlerhttp.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の実装のように表現します。)

順序立てれば、それほど難しいものでもないことが分かると思います。

  1. エンドポイントを作成する方法は、http.Handlehttp.HandleFuncの二つ。
  2. http.HandlerFuncfunc(w http.ResponseWriter, r *http.Request)の別名。ただし、ServeHTTPメソッドが定義されている。
  3. http.Handleの第2引数はHandler型で、ServeHTTPを実装している必要がある。
  4. http.HandleFuncの第2引数は、結局http.HandlerFuncにキャストされる。

この4つを覚えておけば、理解できると思います。まず、1の実装は自分ので定義したstructServeHTTPを実装しているので、問題ないです。2つめの実装ですが、単純な関数をhttp.HandlerFunc型にキャストしているので、ServeHTTPが実装され、問題ないです。逆に言うと、http.Handleの第2引数には、単純なfunc(w http.ResponseWriter, r *http.Request)型の関数を渡すとエラーです。3つめの実装と、4つめの実装は、無名関数かどうかの違いでしかなく、4.で書いているように、http.HandleFuncにキャストされるため、通常の関数を渡しても問題ないです。もちろん5つ目の実装のように前もってキャストしても問題ないです。

まとめ

  1. エンドポイントを作成する方法は、http.Handlehttp.HandleFuncの二つがある。
  2. http.Handleの第2引数は、Handlerインターフェースを実装した(ServeHTTPメソッドが定義されている)ものを渡せば良い。
  3. http.HandleFuncの第2引数は、func(w http.ResponseWriter, r *http.Request)型の関数を渡せば良い。

Go言語は公式のドキュメントがしっかりしているので、良いですね。

参考

Go言語公式ドキュメント

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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.go
package 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.go

runtime/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.gz
net.http.pprof.go
package 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" の実装を追ってみる

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.go
func 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/profileruntime.pprofStartCPUProfileを呼んでいる。

メインの/debug/pprof/profileのエンドポイントのハンドラ関数はruntime/pprofStartCPUProfileを呼ぶラッパーであることがわかります。

golang.org/src/net/http/pprof/pprof.go
func 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()
}

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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.go
package 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.go

runtime/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.gz
net.http.pprof.go
package 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" の実装を追ってみる

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.go
func 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/profileruntime.pprofStartCPUProfileを呼んでいる。

メインの/debug/pprof/profileのエンドポイントのハンドラ関数はruntime/pprofStartCPUProfileを呼ぶラッパーであることがわかります。

golang.org/src/net/http/pprof/pprof.go
func 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()
}

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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.go
package 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.go

runtime/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.gz
net.http.pprof.go
package 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" の実装を追ってみる

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.go
func 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/profileruntime.pprofStartCPUProfileを呼んでいる。

メインの/debug/pprof/profileのエンドポイントのハンドラ関数はruntime/pprofStartCPUProfileを呼ぶラッパーであることがわかります。

golang.org/src/net/http/pprof/pprof.go
func 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()
}

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む