Hateburo: kazeburo hatenablog

SRE / 運用系小姑 / Goを書くPerl Monger

GoでMySQLの監視コマンドを作成するのが楽になるライブラリ

本エントリはMySQL Advent Calendar 2020 の9日目の記事になります。

これまでいくつものMySQLの監視コマンドをつくってきたのですが、毎回同じような処理を書いているので、それらをまとめるライブラリを作ってみました

github.com

主な機能は

  • go-flags形式でのコマンドオプションとデータベースへの接続
  • SHOW STATUS/VARIABLES/SLAVE(REPLICA) STATUSステートメントの構造体へのマッピング

の2つです。

使い方 (コマンドラインオプション)

まずコマンドオプションの処理

オプションを定義した構造体に mysqlflags.MyOpts を追加します

type opts struct {
    mysqlflags.MyOpts
    Timeout time.Duration `long:"timeout" default:"10s" description:"Timeout to connect mysql"`
    Version bool          `short:"v" long:"version" description:"Show version"`
}

opts := opts{}
psr := flags.NewParser(&opts, flags.HelpFlag|flags.PassDoubleDash)
_, err := psr.Parse()

これだけで、helpを表示させると以下のようになります。

Application Options:
      --defaults-extra-file= path to defaults-extra-file
      --mysql-socket=        path to mysql listen sock
  -H, --host=                Hostname (default: localhost)
  -p, --port=                Port (default: 3306)
  -u, --user=                Username (default: root)
  -P, --password=            Password
      --database=            database name connect to
      --timeout=             Timeout to connect mysql (default: 10s)
  -v, --version              Show version

MySQLの defaults-extra-file、socket、ホスト、ポート、ユーザなどのコマンドオプションが追加されてますね。

この構造体を利用してデータベースに接続するには次のようにします。

db, err := mysqlflags.OpenDB(opts.MyOpts, opts.Timeout, debug)
if err != nil {
    log.Printf("couldn't connect DB: %v", err)
    return 1
}
defer db.Close()

DSNだけ欲しい場合は、mysqlflags.CreateDSN()` を使います。

mysqlflagsはDSNを作る際に、my_print_defaults -s client コマンドを実行して my.cnf などに書かれた値を取得します。

% my_print_defaults -s client
--host=127.0.0.1
--user=xxx
--password=xxx

これらをデフォルト値として利用するので、$HOME/.my.cnf が設定されていればコマンドラインオプションが未指定でもコマンドが動きます。

また、接続する際のparameterを追加したい場合は、MySQLDSNParams にmapを渡します。

params := map[string]string{
    "readTimeout": timeout.String(),
}
opts.MySQLDSNParams = params

使い方 (SHOWステートメントの構造体へのマッピング)

MySQLのSHOW STATUSやVARIABLESでは列方向に情報が並べられています。行によってstringやint、boolっぽいものが混じります。

mysql> SHOW VARIABLES;
+-----------------------------+-------------------------------+
| Variable_name               | Value                         |
+-----------------------------+-------------------------------+
| activate_all_roles_on_login | OFF                           |
| admin_address               |                               |
| admin_port                  | 33062                         |
| admin_tls_version           | TLSv1,TLSv1.1,TLSv1.2,TLSv1.3 |
..

これらを良い感じにintやstringに自動で変換して構造体にマッピングしてくれると、監視コマンドのコードは短くなり、とても楽になります

また、SHOW SLAVE(REPLICA) STATUSではカラムが増え、行方向に情報が追加されています。情報が縦方向なのか横方向なのかあまり気にせず同じコードで必要とするカラムだけ良い感じにとれると嬉しいところです。

これをやるのが、mysqlflags.Query().Scan() です。内部では mapstructure というライブラリを利用しています。

github.com

thread関連の値をSHOW VARIABLES/SHOW GLOBAL STATUSから取得するのは以下のように書けます

type threads struct {
    Running   int64 `mysqlvar:"Threads_running"`
    Connected int64 `mysqlvar:"Threads_connected"`
    Cached    int64 `mysqlvar:"Threads_cached"`
}

type connections struct {
    Max       int64 `mysqlvar:"max_connections"`
    CacheSize int64 `mysqlvar:"thread_cache_size"`
}

var threads threads
err = mysqlflags.Query(db, "SHOW GLOBAL STATUS").Scan(&threads)
if err != nil {
    log.Printf("couldn't fetch status: %v", err)
    return 1
}
var connections connections
err = mysqlflags.Query(db, "SHOW VARIABLES").Scan(&connections)
if err != nil {
    log.Printf("couldn't fetch variables: %v", err)
    return 1
}

構造体のメタ情報のタグに、mysqlvar:"取得する変数名" を入れると自動的にマッピングを行います。

レプリケーションの情報を取得する際は次のようにします。

type replica struct {
    IORunning   bool `mysqlvar:"Slave_IO_Running"` // Replica_IO_Running
    SQLRunning  bool `mysqlvar:"Slave_SQL_Running"` // Replica_SQL_Running
    LastSQLError string `mysqlvar:"Last_SQL_Error"`
}

var replica replica
err := mysqlflags.Query(db, "SHOW SLAVE(REPLICA) STATUS").Scan(&replica)

f !replica.IORunning || !replica.SQLRunning {
    fmt.Errorf("something wrong in replication with %s", replica.LastSQLError);
}

bool へのマッピングは、値が Yes や On だった場合に true になり、それ以外は false になります。 Slave_IO_Running(Replica_IO_Running) は Connecting という文字列にもなりますが、こちらは false 扱いです。

もし、真偽値判定を楽にしたいけど、元の文字列も取得したいときは、mysqlflags.Bool 型が使えます。

type replica struct {
    IORunning   mysqlflags.Bool `mysqlvar:"Slave_IO_Running"`
    SQLRunning  mysqlflags.Bool `mysqlvar:"Slave_SQL_Running"`
    ChannelName *string         `mysqlvar:"Channel_Name"` // ポインタ型はオプショナルになる
    Behind      int64           `mysqlvar:"Seconds_Behind_Master"`
}

var replicas []replica
err := mysqlflags.Query(db, "SHOW SLAVE(REPLICA) STATUS").Scan(&replicas)

for _, replica := replicas {
    f !replica.IORunning.Yes() || !replica.SQLRunning.Yes() {
        if replica.ChannelName == nil {
            *replica.ChannelName = "-"
        }
        fmt.Errorf("something wrong in replication Channel:%s IO:%s SQL:%s Error:%s",
            *replica.ChannelName,
            replica.IORunning, replica.SQLRunning,
            replica.LastSQLError);
    }
}

Yes() メソッドがbool値を返し、String() で元の文字列を返します。

使用例

いくつかのMackerelプラグイン、checkプラグインはこのライブラリを使って実装しています。

mkr plugin installでインストールできるようになってますので、ニーズが合えばご利用ください。

ライブラリや監視コマンドの機能追加要望などありましたらお気軽にどうぞ〜