この記事は Mackerel Advent Calendar 2017 の 12/14 の記事です。

はじめに

Mackerel はサーバの監視を行うサービスです。サーバの負荷だけでなくあらゆるリソースの値をメトリクスとして収集し、グラフによる見える化、監視、アラーム送信といったインテグレーションを行うサービスです。Mackerel にメトリクスを送信する mackerel-agent はプラグイン方式を採用しており、色々なメトリクスを Mackerel に送信する事が出来ます。提供されているプラグインには既に色々な物が用意されていて、導入するだけで直ぐに監視を行う事が出来る様になっています。尚、今年は @soudai1025 さんが25日まるまる mackerel plugin だけの Advent Calendar を書いています。各プラグインの README だけでは伝わりづらい色々な便利な機能も紹介されています。

Mackerel プラグインアドベントカレンダー(全部CRE) Advent Calendar 2017

オフィシャルのプラグインリポジトリ mackerel-agent-plugins を見ると各種プラグインが揃っています。ただ、RDB を監視するプラグインとして MySQL と PostgreSQL はあるのですが Oracle の監視を行うプラグインが無かったんです。Oracle ユーザもいる筈だし、欲しいと思っている人もいるんじゃないかなと思っていました。

「これは作るしかないな」そう思いました。そして運よく、僕は Go の Oracle ドライバ go-oci8 の author でもあるのです。これは作るしかない、いや一周回って僕に作らせて下さいお願いします、そう思ったのでした。

Oracle のメトリクス

Oracle から得られるパフォーマンス情報には大きく2つあります。

  • プロセス数やセッションの数といった値
  • 待機イベント

どちらも動的パフォーマンスビュー(v$)から得られます。プロセスとセッションの数は他の RDB と同じく Oracle が稼働するサーバ上でどの程度リソースを使用しているかの値で、監視の際に役立ちます。以下の SQL で得られます。

select
  resource_name
  , current_utilization
from
  v$resource_limit
where
  resource_name = 'processes'
  or resource_name = 'sessions'

待機イベントは、各種イベントが発生する度に収集されレコードとして現れます。主要な項目(待機イベントクラス)は以下の通り。

待機クラス 説明
Administrative ユーザーが待機する原因となるDBAコマンドによる待機(たとえば、索引再作成)
Application ユーザーのアプリケーション・コードによる待機(たとえば、行レベル・ロックまたは明示的ロック・コマンドが原因のロック待機)
Cluster Real Application Clustersリソースに関連する待機(たとえば、gc cr block busyなどのグローバル・キャッシュ・リソース)
Commit 1つの待機イベントのみで構成される待機クラス: コミット後のREDOログ書込み確認用待機(log file sync)
Concurrency 内部データベース・リソースの待機(たとえば、ラッチ)
Configuration データベースの構成またはインスタンスのリソースが十分でないことによる待機(たとえば、ログ・ファイル・サイズ、共有プール・サイズなどが小さい)
Idle セッションがアクティブでない、すなわち作業(SQL*Net message from clientなど)の待機中であることを示す待機
Network ネットワーク・メッセージ(SQL*Net more data to dblinkなど)に関連する待機
Other 通常、システムでは発生しない待機(たとえば、wait for EMON to spawn)
Queue パイプライン化された環境における追加データ取得での遅延を示すイベントが含まれる。これらの待機イベントで費やされる時間は、パイプラインに非効率性などの問題があることを示す。この問題は、Oracle Streams、パラレル問合せ、DBMS_PIPE PL/SQLパッケージなどの機能に影響を与える。
Scheduler リソース・マネージャに関連する待機(たとえば、resmgr: become active)
System I/O バックグラウンド・プロセスI/Oの待機(たとえば、db file parallel writeのDBWR待機)
User I/O ユーザーI/Oの待機(たとえば、db file sequential read)

引用元: https://docs.oracle.com/cd/E60665_01/db112/REFRN/waitevents001.htm

待機イベントは以下の SQL で得られます。

select
  n.wait_class
  , round(m.time_waited / m.INTSIZE_CSEC, 3) AAS
from
  v$waitclassmetric m
  , v$system_wait_class n
where
  m.wait_class_id = n.wait_class_id
  and n.wait_class != 'Idle'
union
select
  'CPU'
  , round(value / 100, 3) AAS
from
  v$sysmetric
where
  metric_name = 'CPU Usage Per Sec'
  and group_id = 2
union
select
  'CPU_OS'
  , round((prcnt.busy * parameter.cpu_count) / 100, 3) - aas.cpu
from
  (
    select
      value busy
    from
      v$sysmetric
    where
      metric_name = 'Host CPU Utilization (%)'
      and group_id = 2
  ) prcnt
  , (
    select
      value cpu_count
    from
      v$parameter
    where
      name = 'cpu_count'
  ) parameter
  , (
    select
      'CPU'
      , round(value / 100, 3) cpu
    from
      v$sysmetric
    where
      metric_name = 'CPU Usage Per Sec'
      and group_id = 2
  ) aas

本プラグインでは、この SQL から得られたイベントの秒間の発生回数を返します。一般的な負荷であればこれで事足ります。しかしながら実際には、この待機イベントクラスにカテゴライズされた小さなイベントが沢山発生します。ちゃんと観測してボトルネックを見つけるには個々のイベントを可視化しないといけません。ただしその種別が半端ない量なのです。以下の URL に発生し得るイベントの一覧が記述されています。

https://docs.oracle.com/database/122/REFRN/descriptions-of-wait-events.htm#REFRN-GUID-2FDDFAA4-24D0-4B80-A157-A907AF5C68E2

これを全て実装してしまうと、発生するイベントの量だけでもかなりの量になってしまいます。Mackerel の個人利用の範囲ではとうてい収まらないかもしれません。

お金が飛んでいく

そこで本プラグインでは、ユーザが指定した欲しいイベントのみを引っ掛ける様にしました。

select
  n.wait_class wait_class
  , n.name wait_name
  , m.wait_count cnt
  , round(10 * m.time_waited / nullif(m.wait_count, 0), 3) avgms
from
  v$eventmetric m
  , v$event_name n
where
  m.event_id = n.event_id
  and n.wait_class <> 'Idle'
  and m.wait_count > 0
order by
  1

v$eventmetric から得た個々のイベントの内、引数で指定したイベント名のみを拾います。


ここで余談

Go で複数のオプションを扱うにはどうすれば良いでしょうか。即答できる人はそこそこ Go をやっている人では無いでしょうか。

正解は

スライスに別名を付け、Set(s string)String() を実装すれば良い

今回であれば -event=/xxx/ の書式でイベント名称を正規表現マッチしたいので以下の様に実装します。

type waitEventName struct {
    Name    string
    Pattern *regexp.Regexp
}

type waitEventNames []waitEventName

var optWaitEvents waitEventNames

func (we *waitEventNames) String() string {
    var buf bytes.Buffer
    for i, w := range *we {
        if i > 0 {
            buf.WriteString(",")
        }
        fmt.Fprintf(&buf, "%q", w.Name)
    }
    return buf.String()
}

func (we *waitEventNames) Set(value string) error {
    if value == "" {
        return errors.New("event name must not be empty")
    }
    var w waitEventName
    w.Name = value
    if len(value) > 2 && value[0] == '/' && value[len(value)-1] == '/' {
        var err error
        w.Pattern, err = regexp.Compile(value[1 : len(value)-2])
        if err != nil {
            return err
        }
    }
    *we = append(*we, w)
    return nil
}

ついでなのでイベント名称をマッチさせるメソッドも作りましょう。

func (we *waitEventNames) Match(name string) bool {
    for _, w := range *we {
        if w.Pattern != nil && w.Pattern.MatchString(name) {
            return true
        }
        if w.Name == name {
            return true
        }
    }
    return false
}

ここまで作っておけば待機イベントを select してきた結果に対してこれだけの実装で済む事になります。

for rows.Next() {
    var class, name string
    var cnt, avgms float64
    err = rows.Scan(&class, &name, &cnt, &avgms)
    if err != nil {
        return nil, err
    }
    logger.Infof("Event %s.%s: count=%f, latency=%f", class, name, cnt, avgms)
    if optWaitEvents.Match(name) {
        stat[normalize(name)+"_count"] = cnt
        stat[normalize(name)+"_latency"] = avgms
    }
}

あとテストも書きやすくて良いですね。

ところでもう1問、Go で

$ foo -v -v -v

といった curl ぽいオプションを実装するにはどうすれば良いでしょうか。これを知っている人は Go 使いの中でもあまりいないかも知れません。正解は

スライスに IsBoolFlags() を実装する

です。

package main

import (
    "flag"
    "fmt"
)

type verboses []bool

func (v verboses) Set(s string) error {
    println(s)
    v = append(v, true)
    return nil
}

func (v verboses) String() string {
    return fmt.Sprint([]bool(v))
}

func (v verboses) IsBoolFlag() bool {
    return true
}

func main() {
    var verboseFlags verboses
    flag.Var(&verboseFlags, "v", "verbose")
    flag.Parse()

    fmt.Println(verboseFlags)
}

これで -v -v -v を指定すると verboseFlags に true が3つ格納されます。ちなみに現状の flag では -vvv は扱えないので、自前でフラグをマージするかサードパーティ製のフラグパーサを使います。


閑話休題

さて話を戻して今回作った Oracle プラグインですが、設定は以下の様に行います。

[plugin.metrics.oracle]
command = [
    "/path/to/mackerel-plugin-oracle",
    "-event=Disk File Operations I/O",
    "-event=control file sequential read",
    "-event=OS Thread Startup",
    "-dsn=system/manager@orcl"
]

-event にどんな値を設定したら良いか分からないという場合は、mackerel-agent のログに発生したイベント名称を出力していますので、気になるイベントがあれば -event フラグで指定するか、正規表現パターンを使って -event=/foo.*bar/ の様に指定して下さい。

mackerel-agent を起動すると以下のグラフが表示されます。

グラフ

  • リソース情報(プロセス数、セッション数)
  • CPU 使用率、待機時間(Idle ではない)

また引数で指定した場合にはマッチしたイベントの発生回数とレイテンシがグラフ表示されます。待機時間が長ければ例えば制御ファイルへの書き込みやネットワークトラフィック等を疑った方が良いかと思います。これはラッチも含みます。

おわりに

今回は mackerel-plugin-oracle を作ってみました。Linux でも Windows でも導入する事が出来るので、Oracle の負荷にお困りの方は一度試して見られてはどうでしょうか。また Go には sqlite3 ドライバもあるので Mackerel Plugin に興味のある方は mackerel-plugin-sqlite3 等作って見るのも面白いかもしれませんね。
リポジトリは以下になります。

https://github.com/mattn/mackerel-plugin-oracle