Zabbix Sender + LLD(ローレベルディスカバリ) + dstat + Grafanaでリソースモニタリング
はじめに
Zabbix Advent Calendar 2017 の3日目の投稿です。
リソースモニタリング方法については、色々な手法が提案されていると思いますが、今回はZabbix SenderとLLD(ローレベルディスカバリ)を使い、dstatで得られるデータを収集・グラフ化してみたいと思います。
こだわりポイントとしては2つあります。
dstatはオプション次第でさまざまなデータを取得することが可能です。そこで、オプション変更の都度テンプレートを修正しなくても良いように、LLDを使ってdstatのデータからアイテムをディスカバリさせます。
モニタリングするための導入物を減らすためにZabbixで提供されているzabbix_senderコマンドは使わず、Zabbix SenderのプロトコルをPythonで独自*1に実装します。
目次
Zabbixサーバ側の準備
まず、Zabbixサーバ側の設定をします。
LLD(ローレベルディスカバリ)用の汎用的なテンプレートを作成します。
ホストグループ作成
今回の環境準備のために、ホストグループ作成します。
特にこの名前のグループでないとダメといったことはありませんので、既存のホストグループを利用しても結構です。
ここでは、下記の名前でグループを作成することとします。
| 項目 | 設定値 |
|---|---|
| グループ名 | Universal Sender servers |
テンプレートの作成
ディスカバリルールを作成するためのテンプレートを作成します。
ホストに対してディスカバリルールを作成することもできそうですが、複数台展開のことを考慮してテンプレート化した方が後々楽できると思います。
下記のテンプレート名、そして先ほど作成したホストグループに所属させます。
| 項目 | 設定値 |
|---|---|
| テンプレート名 | Template Universal Sender |
| グループ | Universal Sender servers |
ディスカバリルールの作成
先に作成したテンプレートに対してディスカバリルールを作成します。
「作成したテンプレートを選択 > ディスカバリルール > ディスカバリルールの作成」で作成画面に遷移できます。
Zabbix Senderを利用するので、タイプはZabbix トラッパーとします。キーはスクリプトでZabbixサーバにデータを送る際に使うため、間違えないよう注意してください。
| 項目 | 設定値 |
|---|---|
| 名前 | universal sender discovery |
| タイプ | Zabbix トラッパー |
| キー | universal.discovery |
アイテムプロトタイムの作成
ディスカバリルールと同じく、先に作成したテンプレートに対してアイテムプロトタイプを作成します。
「作成したディスカバリルールを選択 > アイテムのプロトタイプ > アイテムのプロトタイプ作成」で作成画面に遷移できます。
ディスカバリルールと同様に、タイプはZabbix トラッパーとします。後にデータを送るときやグラフ化する際に名前とキーを使うため、ここも間違えないよう注意してください。
データ型は、数値データをまとめて扱うために数値(浮動小数)としています。
| 項目 | 設定値 |
|---|---|
| 名前 | {#KEY_NAME} |
| タイプ | Zabbix トラッパー |
| キー | universal_sender[{#KEY_NAME}] |
| データ型 | 数値(浮動小数) |
ホストの作成とテンプレート割り当て
最後に、ホストを作成して先に作成したテンプレートを割り当てます。
| 項目 | 設定値 |
|---|---|
| ホスト名 | test_server |
| グループ | Universal Sender servers |
| テンプレート | Template Universal Sender |
クライアント側の準備
スクリプトの配置
下記コマンドで、モニタリング対象のホストにPythonスクリプトを配置します。 dstatだけインストールされていれば、Python2,3どちらの環境でも追加パッケージ不要で実行できるはずです。
ZABBIX_SERVERとZABBIX_PORTについては、各環境に合わせて変更してください。
$ cat <<EOF > universal_sender_dstat.py
import json
import socket
import re
import csv
import subprocess
import tempfile
from datetime import datetime
# change for your environment.
ZABBIX_SERVER = "10.0.0.1"
ZABBIX_PORT = 10051
ZABBIX_HOST = 'test_server'
DSTAT = 'dstat -cdglmnprsy --tcp --udp --unix --nocolor --noheaders --output {} 1 2'
KEY_NAME = '{#KEY_NAME}'
DISCOVERY_KEY = 'universal.discovery'
ITEM_KEY = 'universal_sender[{}]'
def get_data():
with tempfile.NamedTemporaryFile('rw') as f:
command = DSTAT.format(f.name)
subprocess.check_output(command, shell=True)
f.seek(2)
data = csv.reader(f)
# skip header rows
data.next()
data.next()
first_header_list = list()
prev_header = ""
for row_item in data.next():
if row_item:
prev_header = re.sub('\s|/', '_', row_item)
first_header_list.append(prev_header)
second_header_list = data.next()
key_list = [
'dstat.{}.{}'.format(
first_header_list[i],
second_header_list[i]
) for i in range(len(first_header_list)) ]
# discovery data
discovery_data = [
{
'host': ZABBIX_HOST,
'key': DISCOVERY_KEY,
'value': json.dumps({'data': [ { KEY_NAME: key } for key in key_list ]})
}
]
# sender data
# skip first sample data
data.next()
item_data = [
{
'host': ZABBIX_HOST,
'key': ITEM_KEY.format(key_list[i]),
'clock': datetime.now().strftime('%s'),
'value': value,
} for i, value in enumerate(data.next())
]
return (discovery_data, item_data)
def send(data):
sender_data = {
'request': 'sender data',
'data': data,
}
# print(json.dumps(sender_data, indent=2, ensure_ascii=False))
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((ZABBIX_SERVER, ZABBIX_PORT))
client.sendall(json.dumps(sender_data))
response = client.recv(4096)
print('response: {}'.format(response))
if __name__ == '__main__':
discovery_data, sender_data = get_data()
send(discovery_data)
send(sender_data)
EOF
なお、Zabbix Senderのプロトコルは下記フォーマットのJSONデータをSocketで送るだけの非常にシンプルな作りになっています。
[LLDのデータを送信する場合]
{
"request": "sender data",
"data": [
{
"host": "test_server",
"value": "{\"data\": [{\"{#KEY_NAME}\": \"dstat.total_cpu_usage.usr\"}, ... ]}",
"key": "universal.discovery"
}
]
}
[アイテムのデータを送信する場合]
{
"request": "sender data",
"data": [
{
"host": "test_server",
"value": "39.547",
"key": "universal_sender[dstat.total_cpu_usage.usr]",
"clock": "1512231561"
},
.
.
.
]
}
どちらのデータも基本的には同じフォーマットですが、LLDのvalueはディスカバリによって作成されるアイテム名のJSONオブジェクトが、dumpされた文字列となっているので注意してください。
モニタリング開始
データ取得の開始
下記のコマンドで、データ収集を開始します。
$ watch -n 10 python universal_sender_dstat.py
正常に動作した場合、Zabbix Senderプロトコルで通信した際のレスポンスとして、下記のメッセージが出力されます。
ZBXD\{"response":"success","info":"processed: 1; failed: 0; total: 1; seconds spent: 0.006614"}
ZBXD\{"response":"success","info":"processed: 38; failed: 0; total: 38; seconds spent: 0.000635"}
データ送信に失敗した場合は、下記のメッセージが出力されます。
ZBXD\{"response":"success","info":"processed: 0; failed: 1; total: 1; seconds spent: 0.006340"}
ZBXD\{"response":"success","info":"processed: 0; failed: 38; total: 38; seconds spent: 0.000724"}
プロトコルとしては正しく通信できているが、キーが異なるかZabbixサーバ側でまだアイテムが作成されていないために、データを受け取れていないと思われます。
また、プロトコルが不正(dstatコマンドの結果のパースに失敗してJSONデータがおかしくなった場合など)の場合には、下記のようなメッセージが表示されます。
ZBXDa{"response":"failed","info":"cannot parse as a valid JSON object: unexpected end of string data"}
データ取得結果の確認
まずは、ホストのアイテム一覧でディスカバリで作成されたアイテムが存在することを確認します。
ステータスが正常となっていれば問題ありません。

次に、ホストの最新データを確認し、ディスカバリで作成されたアイテムについてデータが取得できていることを確認します。

データが取得できていれば、取得時刻と値が表示されます。
取得したデータのグラフ化
Grafanaでダッシュボードを作成して取得したデータをグラフ化します。
Grafanaの導入および、Zabbixのデータソース設定方法は既に記事が多数ありますのでここでは割愛します。
最終的な見た目は下記のようになります。

ダッシュボードはテンプレート機能を利用し、Zabbixのクエリでホストを選択できるようにします。
| 項目 | 設定値 | 説明 |
|---|---|---|
| Name | Group | Host |
| Type | Query | Query |
| Data source | Zabbixのデータソースを指定 | Zabbixのデータソースを指定 |
| Query | {Universal Sender servers} | {Universal Sender servers}{*} |
| Query の意味 | Universal Sender servers のみ選択できる | Universal Sender servers グループのホストが選択できる |

グラフ作成画面にて、テンプレート機能で作成した変数およびディスカバリで作成されたアイテム名を入力し、グラフを作成します。
| 項目 | 設定値 | 説明 |
|---|---|---|
| Group | $Group | テンプレート機能で選択されたホストグループの変数 |
| Host | $Host | テンプレート機能で選択されたホストの変数 |
| Item | /dstat.*cpu.*/ | グラフ化したいアイテム名(ここではCPU系のデータを正規表現でまとめて指定) |

まとめ
Zabbix Senderでdstatのデータをモニタリングしてみました。
今回の方法には下記の課題がある思いますが、数値データのモニタリングに限ってしまえば妥協できるものかと思います。
- アイテムプロトタイプを数値(浮動小数)としているため、文字列データが扱えない。
- トリガーの設定をしようとすると大変。
まだカレンダーに空きがあるようなので、次回は今回の内容を下記観点でブラッシュアップした記事がかければと思っています!
- watchコマンドではなくcronなどで定期実行されるようにする。
- dstatでのデータ取得とZabbix Senderでデータ送信する処理を粗結合にし、モニタリング項目追加に拡張性を持たせる。
参考
Zabbix Sender 無しで Zabbix Server にデータを送る - mattintosh note
*1:偉そうなことを言っていますが、既に先人がおりますので、それらを参考にさせていただいています