zabbix-agent で取得できる値を Mackerel の custom metrics として送り込む

Mackerel Meetup #1 Tokyo に行ってきました。鯖サンド美味しかったです。

Mackerel では custom metrics を sensu plugin 形式で出力するコマンドから送り込める (ドキュメント)、ということなので、思いついて拙作の go-zabbix-get に sensu plugin format 出力機能をつけてみました。

実行例はこんなかんじで、-f sensu をつけると key, value, unixtime をタブ区切りで出力します。

$ go-zabbix-get -k system.uptime -f sensu
system.uptime	2546472	1403230104

$ go-zabbix-get -k system.users.num -f sensu
system.users.num	1	1403230155

ということで mackerel-agent.conf に以下のような定義をするだけで

[plugin.metrics.system]
command = "go-zabbix-get -k system.users.num -f sensu"
type = "metric"

zabbix-agent で取得できる値をそのまま Mackerel の custom metrics として送信することができます。

既に zabbix-agent 側で UserParam が定義されている場合でも変更なしに Mackerel 側にもデータを送ることができるので、「いま Zabbix をメインで使っているんだけど Mackerel も試してみたいなー」というかた (私です) には大変便利じゃないかと思います!

Consul service のヘルスチェックを zabbix での監視項目と共用する

Consul での service 定義にはヘルスチェックを設定できます。Service Definition - Consul

以下のようにサービス定義に死活監視用のコマンドを登録しておくことで、一定時間ごとにコマンドを起動します。コマンドの終了ステータスが 0 : 正常、1 : warning、それ以外で critical という扱いです。このあたりは nagios, sensu 等のプラグインと互換性があるようですね。
(他に、外部から一定時間ごとに状態を API で登録する TTL 型の死活監視もあります)

{
  "Name": "nginx"
  "Check": {
    "Interval": "10s",
    "Script": "/path/to/healthcheck.sh"
  },
}

ところで、既に何らかのモニタリングツールで監視をしている場合、Node 上で動く daemon 類についてはあらかじめ監視が仕込まれていることが多いはずです。

使用しているのが nagios、sensu であればチェックスクリプトを共用できるので楽ですが、Zabbix で監視をしている場合はどのようにするのがよいか。

zabbix_get というコマンド(もしくは拙作の互換品 go-zabbix-get) を使うことで、Node 上で動いている zabbix-agent から各種情報を取ることができるため、これを Consul の死活監視にも流用できるように考えてみました。

まず、以下のような bash で書かれた wrapper script を、zabbix_get_eval という名前で用意しておきます。

#!/bin/bash
VALUES=()
# 最後の引数を除いた引数をループして zabbix_get した値を配列に入れる
for KEY in "${@:1:($#-1)}"; do
    V=`zabbix_get -k "${KEY}"`
    VALUES=("${VALUES[@]}" "${V}")
done
# 最後の引数は評価式
EXPR="${!#}"
# 評価式に値を渡して評価
bash -c "${EXPR}" -- "${VALUES[@]}"

このコマンドは引数に zabbix-agent から取得する key 名、最後の引数にそれを評価する bash script を取ります。

また、zabbix-agent の設定で、localhostからの情報取得を許可します。

Server=127.0.0.1,zabbix.example.com


以下のような状態を正常と見なす死活監視を定義してみると、

  • 動作しているnginxという名前のプロセス数が 1以上
  • かつ
  • TCP 80 を Listen している

zabbix-agent の proc.num と net.tcp.listen を以下のように使用することで、正常時には exit 0、異常時には exit 2 で終了するコマンドになります。

$ zabbix_get_eval 'proc.num[nginx]' 'net.tcp.listen[80]' '[[ $1 -ge 1 && $2 -eq 1 ]] || exit 2'

Consul に登録するサービス定義にはこれをそのまま渡せば OK です。

{
  "Name": "nginx"
  "Check": {
    "Interval": "10s",
    "Script": "zabbix_get_eval 'proc.num[nginx]' 'net.tcp.listen[80]' '[[ $1 -ge 1 && $2 -eq 1 ]] || exit 2'"
  },
}

zabbix-agent から取得できる項目は結構いろいろあり、(【参考】1 Zabbix エージェント [Zabbix Documentation 2.0]) CPUやプロセス、ネットワークの情報以外にも、面白いところでは

  • web.page.get (HTTPでURLにアクセスして内容を取得)
  • net.dns.record (DNSで名前解決した結果を取得)

などもあります。

bash での値評価は数値、文字列の一致や大小比較の他にも正規表現(=~)も使えるので、複数項目の値を使って柔軟に評価できるかと思います。

Consul の情報を Chef / Ohai から使う ohai-plugin-consul を作ったのとその周辺の話

先日とあるサービスに Consul を入れました。

内部 DNS と、たとえば nginx からアプリケーションサーバに振り分ける定義をするために service を使用しています。

そこで使うために、ohai-plugin-consul を書きました。Github にあります。

fujiwara/ohai-plugin-consul · GitHub

Ohai の version 6 と 7 で plugin の interface が変わっており、ohai-plugin-consul は Ohai 7 向けなので、Chefから使う場合は Chef-11.12.0 以上、または 11.10.4.ohai7.0 が必要です。
【参考】 Ohai, new Ohai plugins! - O'Reilly Radar

使用方法

ohai コマンドから使う場合は -d で plugin (consul.rb) を配置したディレクトリを指定して実行すると、最上位の consul というキーに API を叩いた情報が入ってきます。

ohai -d /path/to/plugin_dir | jq .consul
{
  "agent": {
    "checks": { ... },      #= /v1/agent/checks
    "members": [ ... ],     #= /v1/agent/members
    "services": [ ... ]     #= /v1/agent/services
  },
  "catalog": {
    "datacenters": [ ... ], #= /v1/catalog/datacenters
    "nodes": [ ... ],       #= /v1/catalog/nodes
    "services": [ ... ],    #= /v1/catalog/services
    "node": {
      "FOO": { },           #= /v1/catalog/node/FOO
      ...
    },
    "service": {
      "BAR": { },           #= /v1/catalog/service/BAR
      ...
    }
  }
  "status": {
    "leader": "...",        #= /v1/status/leader
    "peers": [ ... ],       #= /v1/status/peers
  }
}

Chefから使用する場合は、(client|solo).rb に plugin_path を定義してください。

Ohai::Config[:plugin_path] << '/path/to/plugins'

node[:consul] で上記と同様の情報が取得できます。

開発経緯

たとえば以下のように、app というサービスを定義して、その node に nginx からリクエストを振り分けたいとします。

$ curl localhost:8500/v1/catalog/services | jq .
{
  "app": [
    "pc",
    "mobile"
  ]
}
$ curl localhost:8500/v1/catalog/service/app | jq .
[
  {
    "ServicePort": 0,
    "ServiceTags": [
      "pc",
      "mobile"
    ],
    "ServiceName": "app",
    "ServiceID": "app",
    "Address": "192.168.1.11",
    "Node": "app001"
  },
  {
    "ServicePort": 0,
    "ServiceTags": [
      "pc",
      "mobile"
    ],
    "ServiceName": "app",
    "ServiceID": "app",
    "Address": "192.168.1.12",
    "Node": "app002"
  }
]

最初は DNS interface を使って、以下のように nginx から app.service.consul の名前解決をして振り分けようとしました。

# nginx.conf
location / {
  set $app "app.service.consul";
  proxy_pass http://$app:5000;
}

が、以下の事情により DNS による振り分けは断念。

  • Consul (v0.2.1) では DNS (UDP) でのアクセスでは、サービスに node が何台いても 3アドレスをランダムに返す
  • Consul が TTL 0 で応答を返すが、nginx は1秒間は名前解決結果を cache する
  • そのため、任意の1秒間では特定の 3 node にしか振り分けられない
  • 4 node 以上ある場合は1秒ごとに全くアクセスが行かない node ができてしまう

ということで、Chef でテンプレートから生成している nginx.conf に Consul API から取得した service を渡す形にしました。

# nginx.conf.erb
upstream pc_backend {
<% node[:consul][:catalog][:service][:app].select{|n| n[:ServiceTags].include?("pc") }.each do |n| %>
   server <%= n[:Address] %>:5000;  # <%= n[:Node] %>
<% end %>
}

このテンプレートを上記の service 定義で展開すると以下のようになります。

# nginx.conf
upstream pc_backend {
   server 192.168.1.11:5000;  # app001
   server 192.168.1.12:5000;  # app002
}

まだやってないこと

nginx.conf をファイルとして静的に展開するので、service の状態に変化 (nodeの増減など) があった場合にはそれを検知して設定ファイルを再生成、再読込する必要があります。

Consul には blocking query という仕組みがあり、状態の変化を long polling する HTTP API で検知することができます。

mizzyさんの consul-catalog というライブラリを使用すると、以下の Gist のようなコードで service の変更を検知して chef-client を実行、という形が取れるかと思います。

https://gist.github.com/fujiwara/4cdff1d718ecaa2b8294

【参考】【Consul】ブロッキング・クエリ(blokcing query)とは | Pocketstudio.jp log3

また、Consul の service にはヘルスチェック機構がありますが、他に Zabbix でやっている監視とうまく共用できないか構想中のためまだ入れていません。(backendに接続できなければ nginx が切り離すので、今はとりあえずそれで…)

zabbix-getコマンドのGo版を書いた ので、うまいこと組み合わせられないかと構想中です。

Zabbix のスクリーンを percol で快適に選択して開く open_zabbix_screen を作った

数えてみたら Zabbix のスクリーンが180枚もできていて、こうなるとブラウザのプルダウンでの選択がめんどくさいわけです。

一応、先頭一致でインクリメンタルサーチはできますが日本語が混じっているとできないし……と不満に思いつつ使っていたのですが、percol というコマンドが便利だということを知ったので percol で選択したスクリーンを開くツールを書いてみました。

App::OpenZabbixScreen

Perl製です。あらかじめ PATH が通ったところに percol をインストールしてください。

ターミナルから open_zabbix_screen コマンドを起動すると、初回のみ Config::Pit が環境変数 $EDITOR のエディタを開くので、Zabbix の URL (API エンドポイントが http://example.com/zabbix/api_jsonrpc.php なら http://example.com/zabbix/ まで) とユーザ名、パスワードを入力して保存すると、スクリーンの選択画面が開きます。

Zabbix API を叩いてスクリーン一覧を取得して、percol で選択した URL を open コマンドの引数に渡してくれるので、ブラウザでスクリーンが一発で開きます。

便利!


【参考】

Starlet 0.24で子プロセスごとに乱数系列が初期化されるようになった

Perl 5で fork する場合に乱数系列が親と同じになってしまう現象については過去にもいろいろエントリがあります。

0.23以前の Starlet では、親で一度でもどこかで rand (またはsrand) が呼ばれていると、初期化された乱数系列が fork された子プロセスにも引き継がれるため、その後に実際のユーザアプリケーションが走る子プロセス内で rand() しても同じ系列が返ってきてしまう、という現象があったのですが、0.24 では子プロセス生成後に srand() が呼ばれるようになりました。
# 正確にいうと、0.14 で --min-reqs-per-child がサポートされたタイミングで「--min-reqs-per-child が指定されている場合のみ」srand() が呼ばれていたのですが、それがデフォルトの動作になりました。

なので POSIX::AtFork でごにょごにょとか、アプリケーション側での対処は不要になっています。

Starlet 同様にポピュラーな prefork タイプの実装である Starman では、以前から子プロセスの初期化処理で srand() が呼ばれています。

注意点としては、各子プロセスで乱数系列が同じことを期待しているアプリケーションは挙動が変わってしまいますが、普通ないですよね…? その場合は個別に同一引数で srand を実行して揃えればよいかと思います。

kazuho++

KyotoTycoonの更新ログをmemcachedにレプリケーションする ktlog2memcached を書いた

とある移行案件で必要になったのでえいやと書いた。

github.com/fujiwara/ktlog2memcached

以下の記事にあるように、KyotoTycoonは更新ログを外部から取得することができるので、これを用いてKTへの書き込み内容を別のストレージに反映するようなことができます。

http://fallabs.com/blog-ja/promenade.cgi?id=115

使い方

$ ktremotemgr slave -ts `date +%s000000000` -uw | ktlog2memcached --host 127.0.0.1 --port 11211 [--use-flag]

ktremotemgr slave で出力される更新ログを標準入力から流してやると、それをparseして引数で指定された memcached に set, delete, flush_all を発行します。

--use-flag オプションは、KT を memcached plugin を使用して起動し、memcached client からアクセスしていた場合には flag (memcached protocolの) がデータ末尾に記録されているので、それを Perl の Cache::Memcached と同様に認識し、圧縮の展開とシリアライズされた構造化データのデシリアライズを行うオプションです。

HAProxy で MySQL のヘルスチェックをちょっと便利にする

MySQL で slave を複数台立てて参照分散するには、HAProxy を利用してロードバランスと切り離しを行うと手軽に使えて便利です。
option mysql-check という設定で、HAProxy 自身が mysqld に接続してヘルスチェックが可能です。

listen mysql-slave
       bind     127.0.0.1:3307
       mode     tcp
       option   mysql-check user haproxy
       balance  roundrobin
       server   slave1  192.168.1.11 check
       server   slave2  192.168.1.12 check
       server   slave3  192.168.1.13 check

なのですが、この設定だと以下のように少々不便なことがあります。

  • mysqldに接続できるかどうかのみを死活の判断にしているので、レプリケーションが停止しているような場合にも構わず振り分けてしまう
    • 結果、ユーザに古い情報が見え続けてしまう事故が起きうる
  • 作業をするので一時的に特定のホストだけ外したい、という場合に切り離し作業が面倒

ということで、Having HAProxy check mysql status through a xinetd script | Sysbible を参考にして、HTTP経由での死活監視をしてみました。

HTTPリクエストを受けて、localhost の mysqld に接続して 200 / 500 を返すような daemon を実装します。

  • 接続できなければ Status 500
  • SHOW SLAVE STATUS の結果が得られなければ Status 500
  • SHOW SLAVE STATUS の結果を Status 200、JSON で返す

という挙動をします。

#!/usr/bin/env perl
use strict;
use Plack::Runner;
use DBI;
use Try::Tiny;
use JSON;

sub response {
    my ($code, $message) = @_;
    my $content = encode_json({ message => $message });
    my $header = [
        "Content-Type"   => "application/json",
        "Content-Length" => length($content),
    ];
    return [ $code, $header, [ $content ] ];
}

sub app {
    my $env = shift;
    my $result;
    my $res = try {
        my $dbh = DBI->connect(
            'dbi:mysql:database=test;host=127.0.0.1;port=3306',
            'haproxy', '',
            { RaiseError => 1, AutoCommit => 1 },
        );
        $dbh->ping;
        $result = $dbh->selectrow_hashref("SHOW SLAVE STATUS");
        $dbh->disconnect;
        undef;
    }
    catch {
        my $e = $_;
        response 500, $e;
    };
    return $res if defined $res;
    if (ref $result) {
        response 200, $result;
    }
    else {
        response 500, "This mysqld is not a slave.";
    }
}

my $runner = Plack::Runner->new;
$runner->parse_options(@ARGV);
$runner->run(\&app);

これを slave が動作しているホストそれぞれで動作させ、そこに対して HAProxy が HTTP でのヘルスチェックを行うように設定します。死活監視用のユーザには REPLICATION CLIENT の権限を与えておきます。

listen mysql
       bind     127.0.0.1:3307
       mode     tcp
       balance  roundrobin
       option   httpchk
       server   slave1 192.168.1.11:3306 check port 5000
       server   slave2 192.168.1.12:3306 check port 5000
       server   slave3 192.168.1.13:3306 check port 5000

これで接続できなかったり、master になっていたりした場合には HTTP 500 が返るので HAProxy がダウン検知して切り離されます。MHA で slave が master に昇格しても大丈夫。

特定のローカルファイルが存在していたら 503 を返すようにすれば、一時的に切り離したいときには touch で行えます。レプリケーションの状態を見て一定以上遅れていたら切り離しなど、アプリケーションの都合により柔軟に切り離し条件を設定できるので、便利になりますね。

もちろん死活監視用の daemon が落ちたら mysqld が生きていても切り離されてしまうので、万が一落ちても再度起動できるように daemontools 等から起動するとよいかと思います。

[追記]

ヘルスチェック用 httpd の Go 版も書きました。 fujiwara/mysql-slave-healthcheck-agent · GitHub