寝ても覚めてもこんぴうた

プログラム書いたり、ネットワーク設計したり、サーバ構築したり、車いじったり、ゲームしたり。そんなひとにわたしはなりたい。 投げ銭は kyash_id : chidakiyo マデ

goのプロファイラpprofを利用してパフォーマンスを計測する (GAE + ginもあり)

f:id:chidakiyo:20191031154054j:plain

pprof とは

Go言語のプロファイリングツール。
Web UIでフレームグラフや、関数の実行時間のグラフを見ることができる

通常のpprof利用方法

pprofの使い方として2つのパターンがあります。

  • runtime/pprof : ファイルにプロファイル情報を出力する
  • net/http/pprof : httpアクセスでプロファイル情報を取得する事ができる

ちょっと分かりづらいですね

runtime/pprof の場合には、go製のツールなどで利用することが想定されていると思います。
mainの実行時間が短く、mainのdeferでファイルを出力するような記述をします。

net/http/pprof の場合には、httpサーバとして動作することが想定されており、
pprofのimportを行うことでプロファイルを取得するためのハンドラが自動的に実装されます。

実装されたハンドラに対して外部からリクエストを行うことで、動作しているwebアプリケーションのプロファイル情報を見ることができます。

net/http/pprof の適用方法(net/http 利用時)

net/http 利用時には "net/http/pprof" をimportするだけでpprofを実行する準備ができます。

その場合のmain関数は以下のようになります

package main

import (
    "fmt"
    "log"
    "net/http"
    _ "net/http/pprof"
)

func main() {
    http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
        writer.WriteHeader(http.StatusOK)
        fmt.Fprint(writer, "hello hogehoge.")
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}

net/http/pprof の適用方法(gin 利用時)

gin を利用している場合には net/http のときのような _ "net/http/pprof" をimportするだけでは動作しません。
ginで利用する場合には以下の2ステップが必要です。

  1. "github.com/gin-contrib/pprof" をimportする
  2. pprof.Register(route) を実行する

です。

main関数は以下のようになります。

package main

import (
    "github.com/gin-contrib/pprof"
    "github.com/gin-gonic/gin"
    "net/http"
)

func main() {
    route := gin.Default()
    pprof.Register(route)
    route.GET("/", func(c *gin.Context) {c.String(http.StatusOK, "hello hogehoge.")})
    http.Handle("/", route)
    route.Run(":8080")
}

プロファイルの確認方法

プロファイルの確認方法には2つのケースがあります。

  1. pprofのパスからファイルを取得してからローカルで見る
  2. goのツールで直接パスを指定してweb UI上で見る

今回は 2 の方法をメインに確認してみます

登録されたハンドラ一覧

gin をデプロイした際に登録されたハンドラがログとして出力されたものが以下です

[GIN-debug] GET    /debug/pprof/             --> github.com/gin-contrib/pprof.pprofHandler.func1 (3 handlers)
[GIN-debug] GET    /debug/pprof/cmdline      --> github.com/gin-contrib/pprof.pprofHandler.func1 (3 handlers)
[GIN-debug] GET    /debug/pprof/profile      --> github.com/gin-contrib/pprof.pprofHandler.func1 (3 handlers)
[GIN-debug] POST   /debug/pprof/symbol       --> github.com/gin-contrib/pprof.pprofHandler.func1 (3 handlers)
[GIN-debug] GET    /debug/pprof/symbol       --> github.com/gin-contrib/pprof.pprofHandler.func1 (3 handlers)
[GIN-debug] GET    /debug/pprof/trace        --> github.com/gin-contrib/pprof.pprofHandler.func1 (3 handlers)
[GIN-debug] GET    /debug/pprof/allocs       --> github.com/gin-contrib/pprof.pprofHandler.func1 (3 handlers)
[GIN-debug] GET    /debug/pprof/block        --> github.com/gin-contrib/pprof.pprofHandler.func1 (3 handlers)
[GIN-debug] GET    /debug/pprof/goroutine    --> github.com/gin-contrib/pprof.pprofHandler.func1 (3 handlers)
[GIN-debug] GET    /debug/pprof/heap         --> github.com/gin-contrib/pprof.pprofHandler.func1 (3 handlers)
[GIN-debug] GET    /debug/pprof/mutex        --> github.com/gin-contrib/pprof.pprofHandler.func1 (3 handlers)
[GIN-debug] GET    /debug/pprof/threadcreate --> github.com/gin-contrib/pprof.pprofHandler.func1 (3 handlers)
[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)

結構な数のハンドラが利用できるようですが、 /debug/pprof にアクセスすると以下のようなサマリが確認できます。

f:id:chidakiyo:20191031153812p:plain

ローカルからサーバのプロファイルを確認する

ローカルからサーバのプロファイルを確認する際には、閲覧するPCにgoと、graphvizがインストールされている必要があります

それぞれMacであれば

brew install go
brew install graphviz

のような形でインストールが可能です。

profileを確認する

まずはprofileを確認してみます

go tool pprof -http=":8888" http://localhost:8080/debug/pprof/profile

のようなコマンドをMac上で実行します。
コマンドの意味としては、 http://localhost:8080/debug/pprof/profile から情報を出力し、ローカルの 8888 ポートでプロファイラのWeb UIを起動するという意味合いになります。

コマンドを実行ししばらくすると、自動的に適当なブラウザが起動すると思います。

以下のようなグラフを確認することができます。

f:id:chidakiyo:20191031153836p:plain f:id:chidakiyo:20191031153846p:plain f:id:chidakiyo:20191031153855p:plain

heapを確認する

同様にheapを確認したい場合には

go tool pprof -http=":8888" http://localhost:8080/debug/pprof/heap

のように接続先のURLのパスをheapに変更します。

f:id:chidakiyo:20191031153936p:plain

appengineにデプロイする

pprof自体はappengineにデプロイする際でも特に実装上の変更はありません。
注意点としては、app.yamlでgoにマッピングをしているパスに /debug で始まるパスがgoに対してマッピングされているかを確認しましょう。

また、SourceのViewでheapを見た際、ローカルではgoのコードが表示されますが、
appengine上ではgoのコードが表示されないようです。

まとめ

プロファイラを適用した際のオーバヘッドはゼロではないのですが、かなりオーバヘッドが少なくなるように設計されているようなので、もし本番環境で問題などが起きた場合にはこのようなツールを使って一部トラフィックを流すなどして確認ができそうです。

今回は各種Viewの見方などは特に説明しませんでしたが、
パフォーマンスの問題を解決するには必須のツールだと思います。
便利に使って楽しいgoライフを!

参考

Go言語のプロファイリングツール、pprofのWeb UIがめちゃくちゃ便利なので紹介する - Eureka Engineering - Medium

Golangのpprofの使い方【基礎編】 - Carpe Diem

Bazelでbuild試してみる(Javaでgetting started)

f:id:chidakiyo:20191024145820j:plain

基本的な内容は Getting Started with Bazel をなぞってます。

Bazelとはなにか

Googleが開発したBuildツール。 もともとはGoogleが社内で使っているツール(の思想?)をベースに開発されているようですね。

makeを置き換えれるもの的な説明を見かけたが、もっと嬉しいことがありそう 実行時にディレクトリを汚染しない(サンドボックス内で実行する)や、並列ビルドが行えるなどがある

Bazelのインストール

ここ からインストール用の情報が確認できる。

今回はMac上で試すので、homebrew(tap)で入れてしまいます。

brew tap bazelbuild/tap
brew install bazelbuild/tap/bazel

一応インストール確認

bazel --version

bazel 1.0.0 と返ってくるのでリリースされたばかりの1.0.0がインストールされました(2019/10/18現在)

ちなみに こちら にあるようにIDEとの連携も可能なようです。 IntelliJを使って開発しているのでそのうち時間があるときに試してみようと思います。

また、Dockerコンテナを利用して試したい方には こちら のコンテナを利用できます。

BuildのTutorialの実行(Java

ひとまず こちらJavaチュートリアルをやってみようと思います

事前に必要なもの

  • bazelのインストール
  • JDKのインストール
    • JDK8をインストールするように書いてありますが、JDK13でやってみます

サンプルプロジェクトの取得

以下を実行し、サンプルプロジェクトを取得します

git clone https://github.com/bazelbuild/examples/

Bazelを利用してBuildする

workspaceをセットアップする

ワークスペースはプロジェクトのソースファイルとBazelの生成ファイルが置かれます。 Bazelの特別なファイルも置かれます。 WORKSPACEファイルはディレクトリ構造のrootに置く必要があります。 BUILDファイルも配置する必要がありますが、BUILDファイルを配置するディレクトリはパッケージとなります。

Bazel workspaceとして指定するディレクトリに 空の WORKSPACE というファイル を配置します。

Bazelがビルドする際の依存ファイルはすべて同一のワークスペース内に存在する必要があります (外部のワークスペースとリンクするという概念もあるようです)

java-tutorial パッケージをBuildしてみる

java-tutorial ディレクトリ配下のBUILDファイルを利用してBuildしてみます。

ディレクトリに移動してBuildコマンドを実行します。

cd java-tutorial
bazel build //:ProjectRunner

// はプロジェクトルートからの相対的なBUILDファイルの位置で、 ProjectRunner はBUILDファイル内で名付けられたターゲットです。

Buildが成功すると、ワークスペースのrootにあるbazel-binディレクトリにビルドの出力を配置します。

bazel-bin/ProjectRunner

のコマンドを実行することで動作確認が行なえます。

bazel-binのディレクトリをlsすると

$ ls bazel-bin

ProjectRunner               ProjectRunner.runfiles          libgreeter-native-header.jar
ProjectRunner.jar           ProjectRunner.runfiles_manifest     libgreeter.jar
ProjectRunner.jar-0.params      _javac                  libgreeter.jar-0.params
ProjectRunner.jar-1.params      external                libgreeter.jar-1.params
ProjectRunner.jar_manifest_proto    libgreeter-hjar.jar         libgreeter.jar_manifest_proto
ProjectRunner.jdeps         libgreeter-hjar.jdeps           libgreeter.jdeps

という感じで大量にファイルが生成されているようです。

依存グラフを参照する

BUILDファイルで定義した依存関係を表示します。

以下のコマンドを実行します

$ bazel query --noimplicit_deps "deps(//:ProjectRunner)" --output graph

チュートリアルでは --notool_deps オプションも付けていますが、それをつけるとうまく動きません。

出力結果はこんな感じ

digraph mygraph {
  node [shape=box];
  "//:ProjectRunner"
  "//:ProjectRunner" -> "//:greeter"
  "//:ProjectRunner" -> "//:src/main/java/com/example/ProjectRunner.java"
  "//:src/main/java/com/example/ProjectRunner.java"
  "//:greeter"
  "//:greeter" -> "//:src/main/java/com/example/Greeting.java"
  "//:src/main/java/com/example/Greeting.java"
}

出力結果のJSONGraphviz に食わせると以下のような画像化ができます。 これは便利。

f:id:chidakiyo:20191024145927p:plain

build targetを読む

java-tutorial/BUILD を以下のように複数に分割されています。

java_binary(
    name = "ProjectRunner",
    srcs = ["src/main/java/com/example/ProjectRunner.java"],
    main_class = "com.example.ProjectRunner",
    deps = [":greeter"],
)

java_library(
    name = "greeter",
    srcs = ["src/main/java/com/example/Greeting.java"],
)

このような構成にすると ProjectRunner を実行する前に greeter が実行されます。 depsで greeter が指定されています。

以下のコマンドでBuildを実行してみます

bazel build //:ProjectRunner

Buildが成功したら以下のコマンドで生成したバイナリのテストを行えます。

bazel-bin/ProjectRunner

ProjectRunner.javaを変更するとBazelはその変更されたファイルのみを再度コンパイルします。

まとめ

チュートリアルの流れ的には、単一のグラフになっているBUILDファイルを、Bazelを改善するという流れで複数に分割するという流れになってるのですが、実際にgithubから取得したexampleはすでに複数に分割されているようなので、流れが違っています。

とりあえずチュートリアル動かした、というレベルであまり良くわかってないっすw

今後やりたい

  • ScalaをBazelで
  • ElmをBazelで

discourse.elm-lang.org

databricks.com

OAuth/OpenID Connectを理解したければここを読め(メモ

f:id:chidakiyo:20191016153134j:plain

タイトルの通り。
OAuth、OpenID Connectを理解するための記事まとめ。

その他、良さそうな記事があればコメントで教えて下さい。

記事

一番分かりやすい OAuth の説明 - Qiita

一番分かりやすい OpenID Connect の説明 - Qiita

OpenID Connect 全フロー解説 - Qiita

IDトークンが分かれば OpenID Connect が分かる - Qiita

おまけ

OpenID Connect の JWT の署名を自力で検証してみると見えてきた公開鍵暗号の実装の話 - Qiita

今日 (2019/09/10) 現在のGAE/goをUPDATEする際の注意点、俺まとめ

f:id:chidakiyo:20190910182707j:plain

Appengine/goが過渡期とでもいいましょうか、
日々変化も目まぐるしく、いろいろ気をつけるべき点もありそうなので雑にまとめました。
ご参考まで。(2019/09/10現在)

GAE/go 1.9 は終わり

f:id:chidakiyo:20190910182745p:plain

画像のとおりです。

警告: Go 1.9 ランタイム バージョンは非推奨になりました。2019 年 10 月 1 日以降、新しいデプロイではこのバージョンを使用できなくなります。2019 年 10 月 1 日までに、Go 1.11 または Go 1.12 を使用するようにアプリケーションをアップグレードしてください。

とあるので、今年の10月1日以降はgo1.9でのデプロイは不可能になります。
すでにデプロイしているものは動くんじゃないか(未確認)と思いますが、速やかに移行しましょう。
go1.11 への移行は比較的容易だと思います。(appengineパッケージも利用できるので)

追記: すでにデプロイされているバージョンは引き続き利用できるようです。 [1]

f:id:chidakiyo:20190911134641p:plain

GAE/go 1.11 は気をつけろ

個人的に気になる点は3点、

  • ランタイムのパフォーマンスが 1.9 より少々悪い
  • 非appengineパッケージのライブラリのRPC部分にメモリリークの問題が有りそう
  • RPCのタイムアウトに気をつけて!

ランタイムのパフォーマンスに関しては、レスポンスの時間などは特に差を感じませんが、
CPUの利用量とメモリの使用量の増加があるように感じています。
Stackdriverのグラフで見た感じで2-3割り程度の差があるように見えます。(ざっくり

非appengineパッケージ(cloud.google.com/go/datastore など)を利用した際に今日現在でRPC部分にメモリリークする箇所がありそうです。
そのため、appengineパッケージをあえて利用することで問題を回避できます。(ですが次の問題があります)

appengineパッケージのRPCに関しては、2nd-genランタイムは多くのRPCを実行しているようで、比較的頻繁にタイムアウトするケースがあります。
service bridge HTTP failed などのログが出力される場合には internal/net.go の以下の画像の箇所のtimeoutを伸ばしてあげると良いようです。(現在修正し様子を見ようとしているところです)

f:id:chidakiyo:20190910183656p:plain

GAE/go 1.12 はまだよくわかりません

一応(?)1.12対応で実装しているものありますが、app.yamlで1.11の設定でデプロイしているので厳密にはよくわかっていません。(1.11と違いがないかもしれません。)

注意点としては、↑のgo1.11でも書いたメモリリークの問題は1.12ランタイムではappengineパッケージが利用不可になっているため、現状、回避策がないと思います。
大量にRPCを実行しているような場合にはインスタンスタイプを上げるなどして神に祈る必要がありそうです。

まとめ

1.9からの移行を皆様急いでいるかと思いますが、ライブラリ併せてのupdateは若干罠があるので(今日 2019/09/10 現在)、
一旦 GAE/go1.11 環境に appengine ライブラリを利用したままの移行が今日現在、私のオススメの予感です。

おまけ

[1] Feature Deprecations  |  App Engine Documentation  |  Google Cloud

fix · f81/appengine@15ec82a · GitHub

goのベンチマークメモ

f:id:chidakiyo:20190905164829p:plain

goのベンチマーク周りのメモです

2つのベンチマークの比較

godoc.org

pprof のツール

github.com

goのパフォーマンス周りの資料(observability)

bit.ly

GoのSSA最適化

qiita.com

おまけ

厳密なgofmt

github.com

go!

GAE/go1.11 で Stackdriver Profiler を使ってみる

f:id:chidakiyo:20190822202047j:plain

appengine go1.11以降でgoでもプロファイラが使えるようなので初期設定だけやってみます。
内容的にはほぼ こちら の内容です。

Stackdriver Profiler は何ができるの?

Goの場合には、CPU、ヒープ、競合、スレッドに関するプロファイリングが可能です。
その他、競合プロファイリングやスレッドプロファイリング等があるようです。ここはこの記事では説明しません。

GAE/go アプリケーションにプロファイラを組み込む

ドキュメントには アプリケーションにパッケージをインポートしてから、コードのできるだけ早い段階でProfilerを初期化する とあるので、mainロジックの早い段階で以下のように初期化すると良いと思います。

以下の例は ginを利用しています。

import (
    "cloud.google.com/go/profiler"
    "github.com/gin-gonic/gin"
    "google.golang.org/appengine"
    "net/http"
)

func main() {
    if err := profiler.Start(profiler.Config{
        DebugLogging: true,
    }); err != nil {
        // TODO: Handle error.
    }

  route := gin.Default()
  http.Handle("/", route)
  route.GET("/", func(context *gin.Context){
    context.String(http.StatusOK, "hello!")
  })
    appengine.Main()
}

プロファイラのAPIを有効化する

プロファイラを利用するためにはAPIの有効化が必要です。
WebUIから Profiler API を検索し、有効化しても良いですし、
コマンドで gcloud services enable cloudprofiler.googleapis.com と実行しても良いです。

いずれかの方法でAPIを有効化します。

作成したappengineアプリケーションをデプロイする

デプロイ方法は特にこの記事では説明しません。
デプロイ完了の後に、アプリケーションに何度かアクセスし、 hello! とレスポンスが帰ってくることを確認します。

プロファイラを確認する

いよいよメインのプロファイラの確認です。
プロファイラの画面 にアクセスし、確認します。

CPU time

f:id:chidakiyo:20190822201626p:plain

Heap

f:id:chidakiyo:20190822201615p:plain

Allocated heap

f:id:chidakiyo:20190822201604p:plain

Threads

f:id:chidakiyo:20190822201552p:plain

まとめ

まずは使えるようになるという意味ではここまで。
また時間があればプロファイラの使い方の記事でも書こうかと思います。

Go で StartsWith/EndWith、前方一致/後方一致はないの?

f:id:chidakiyo:20190815155700j:plain

Javaで育った私としては、前方一致/後方一致は StartsWith/EndWith というイメージが勝手にありますが、
goを書いているとたまにど忘れするので備忘録。

go での前方一致/後方一致

goでももちろん前方一致/後方一致のための関数は用意されています。

stringsパッケージの HasPrefix/HasSuffix がそれです。
もうほぼこれで答えなんですが、サンプルは以下のようになります。

import "strings"

~~snip~~

strings.HasPrefix("hello world", "hello") // -> true
strings.HasSuffix("hello world", "world") // -> true

簡単ですね。
ではでは。

参考

https://golang.org/pkg/strings/#HasPrefix https://golang.org/pkg/strings/#HasSuffix