Go
Mackerel
MackerelDay 13

稼働中のバッチを監視したくなったら Mackerel Custom Metrics が便利

More than 1 year has passed since last update.

この記事は Mackerel Advent Calendar 2016 の 12/13 日の記事です。

はじめに

監視

皆さんは golang で書かれたプロセスの監視はどの様に行われているでしょうか。builderscon 2016 でも登壇された Dave Cheney 御大の gcvis をお使いでしょうか。

Dave Cheney 御大

https://github.com/davecheney/gcvis

確かに gcvis は便利なのですが一つ悩ましい点があり、gcvis 自信がプロセスを起動しないといけないという点にあります。作り上致しかたないのですが、コマンド引数にて起動するプロセスを指定する仕様になっています。つまり起動には gcvis が必要になるのです。監視の際にアプリケーションを止められるのならばいいのですが、そうでないときは使えない事もあります。

ところで昨日 golang で書かれたプロセスを監視/操作するためのツール「gops」をご紹介しました。

http://qiita.com/mattn/items/a92f69ff18eb5cbcdd59

この gops で見る事が出来るヒープの状態は、ウェブアプリであれば mackerel-agent-plugins に含まれる mackerel-plugin-gostats を設定にて有効にする事で mackerel 上にグラフ表示ができます。

mackerel-plugin-gostats

mackerel-plugin-gostats の設定は以下の様にします。

[plugin.metrics.gostats]
command = "/path/to/mackerel-plugin-gostats -port=8000 -path=/api/stats"

この設定内容からお分かり頂ける通り、mackerel-plugin-gostats を使う場合、アプリケーションは golang-stats-api-handler を使って stats 専用の http ハンドラを公開する必要があります。

package main

import (
    "net/http"
    "log"
    "github.com/fukata/golang-stats-api-handler" // これ
)

func main() {
    http.HandleFunc("/api/stats", stats_api.Handler)
    log.Fatal( http.ListenAndServe(":8080", nil) )
}

もし外部にこの情報を公開したくない場合には、見付かりづらいエンドポイントにしてしまうか、このサービスをリバースプロキシの内側で稼働させる必要があります。またバッチ系システムの場合、ソース内で新たに http サーバを立てる必要があります。社内ツール程度であればポートもハードコードされていても良いのですが、開発者ならどんどん欲が生まれて http サーバのアドレスやポートも制限したくなってきます。何か期待していない作業を自ら生んでしまった気がしてきますね。そこで昨日ご紹介した gops の出番です。gops の agent を起動させておき、外部から gops memstats を呼び出します。

alloc: 19267968 bytes
total-alloc: 19393560 bytes
sys: 23939320 bytes
lookups: 11
mallocs: 2518
frees: 104
heap-alloc: 19267968 bytes
heap-sys: 20578304 bytes
heap-idle: 983040 bytes
heap-in-use: 19595264 bytes
heap-released: 0 bytes
heap-objects: 2414
stack-in-use: 393216 bytes
stack-sys: 393216 bytes
next-gc: when heap-alloc >= 28678953 bytes
last-gc: 1481194106621190296 ns
gc-pause: 455534 ns
num-gc: 2
enable-gc: true
debug-gc: false

この gops の出力をパースして Mackerel Custom Metrics としてポストすれば...

  • http に依存せず
  • 既に起動中のプロセスであったとしても
  • なんなら mackerel-agent を動かさずとも

簡単にグラフが表示できる!!!!1

mackerel-gops 作った

という事で作ってみました。とても小さいツールなのでソースまるごと乗せてしまいます。

package main

import (
    "bytes"
    "encoding/json"
    "flag"
    "log"
    "net/http"
    "os"
    "os/exec"
    "strconv"
    "strings"
    "time"
)

var (
    names = map[string]string{
        "alloc":         "bytes",
        "sys":           "bytes",
        "lookups":       "integer",
        "mallocs":       "integer",
        "frees":         "integer",
        "heap_alloc":    "bytes",
        "heap_sys":      "bytes",
        "heap_idle":     "bytes",
        "heap_in_use":   "bytes",
        "heap_released": "bytes",
        "heap_objects":  "integer",
        "stack_in_use":  "bytes",
        "stack_sys":     "bytes",
    }
    prefix  = flag.String("name", "", "prefix of keys for metrics")
    service = flag.String("service", "", "service name")
    sleep   = flag.Duration("sleep", 5*time.Second, "sleep")
)

type metric struct {
    Name  string  `json:"name"`
    Time  int64   `json:"time"`
    Value float64 `json:"value"`
    Unit  string  `json:"unit"`
}

func main() {
    flag.Parse()

    if *prefix == "" || *service == "" {
        flag.Usage()
        os.Exit(2)
    }

    os.Setenv("GODEBUG", "http2client=0")

    for {
        time.Sleep(*sleep)

        b, err := exec.Command("gops", "memstats", flag.Arg(0)).CombinedOutput()
        if err != nil {
            log.Fatal(err)
        }
        var metrics []metric
        for _, line := range strings.Split(string(b), "\n") {
            tokens := strings.SplitN(line, ":", 2)
            if len(tokens) != 2 {
                continue
            }
            name := strings.Replace(tokens[0], "-", "_", -1)
            unit, ok := names[name]
            if !ok {
                continue
            }
            val, _ := strconv.ParseFloat(strings.Split(strings.TrimSpace(tokens[1]), " ")[0], 64)
            metrics = append(metrics, metric{Name: *prefix + "." + name, Time: time.Now().Unix(), Value: val, Unit: unit})
        }

        var buf bytes.Buffer
        err = json.NewEncoder(&buf).Encode(metrics)
        if err != nil {
            log.Print(err)
            continue
        }
        log.Print(buf.String())
        req, err := http.NewRequest("POST", "https://mackerel.io/api/v0/services/"+*service+"/tsdb", &buf)
        if err != nil {
            log.Print(err)
            continue
        }
        req.Header.Set("X-Api-Key", os.Getenv("MACKEREL_API_KEY"))
        req.Header.Set("content-type", "application/json")
        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            log.Print(err)
        }
        resp.Body.Close()
    }
}

リポジトリは以下の URL にあります。

https://github.com/mattn/mackerel-gops

使い方は以下の様に起動します。-p で指定するプロセスIDは gops コマンドで得られる * の付いている物を指定します。

$ mackerel-gops -p 12345 -name myserver -service ore-no-service

つまり Mackerel のアカウントとこのツールが1つあれば、すぐにでも監視が行えます。監視の必要がなくなったのであれば、プロセスは終了しないまま mackerel-gops を止めてしまえばいいのです。

メトリクスの取得方法

サービスを作るだけです。

サービス作成

サービスメトリクス

あとはコマンドラインから上記の手順で mackerel-gops コマンドを実行するだけ。name オプションは、プロセス名などを指定すると良いです。

起動の際には環境変数 MACKEREL_API_KEY を設定しておく必要がある事だけ注意して下さい。API KEY は Mackerel の右上のメニューから「アカウント設定」「オーガニゼーション」、そして何れかの所属の「設定」、「API キー」のタブから参照できます。

API KEY の取得方法

API KEY の場所

あとは待つだけ

簡単すぎる!実際は tmux 等で常駐させておけば良いと思います。

ほら、記事を書いている間にもうメトリクスが表示されました。

カスタムメトリクス

まとめ

開発中のアプリケーションを予め gops 対応しておくと、いざ起動中のプロセスを監視したくなった場合でも簡単に対応できます。そして Mackerel Custom Metrics にも2~3回の手順で簡単にグラフが表示されて ウマー! キャー素敵~! 監視大好き! となる事間違いなしです。

という事で、今年もあと少しになってきました。バックエンドエンジニアやインフラエンジニアにとってはビクビクせざるを得ない時期でもあります。アラームが鳴らない事を祈りながら、ぜひ出来るところからの監視を始めましょう。