Zabbix Senderを自前実装する場合にはZabbix Protocolのヘッダーが必要だった
はじめに
以前に下記の記事でZabbix Senderを自前実装していましたが、一つのコネクションで一度に大量のデータを送ろうとするとたびたびエラーになることがわかりました。
実装に不備があることもわかったので、正しいと思われる方法についてまとめておきます。
原因
私の誤った解釈により、Zabbix Senderは決められたJSONフォーマットのデータをTCPでZabbixサーバに送ればOKだと思っていましたが、実はJSONデータの前にZabbixプロトコルのヘッダーをつける必要がありました。
ヘッダーを含めたZabbix Senderの実装としては下記が参考になります。
AWS Lambda(Python)からZabbix Senderでメトリクス値を送るスクリプト | 外道父の匠
- Zabbix Sender Python version - ZABBIX Forums
- Docs/protocols/zabbix sender/3.4 - Zabbix.org
- python-zabbix/senderprotocol.py at master · jbfavre/python-zabbix · GitHub
ヘッダーについては下記が参考になります。
検知経緯
原因を突き止めるまでに、下記のような事象を確認していました。
Zabbix Senderのレスポンス
まず、Zabbix Senderでのデータ送信に成功した場合には、下記のようなレスポンスが返されました。
{"response":"success","info":"processed: 39; failed: 0; total: 39; seconds spent: 0.093660"} {"response":"success","info":"processed: 33; failed: 0; total: 33; seconds spent: 0.916001"} {"response":"success","info":"processed: 3; failed: 0; total: 3; seconds spent: 0.131982"} {"response":"success","info":"processed: 56; failed: 0; total: 56; seconds spent: 0.690923"}
次に、失敗した場合のレスポンスが下記の通りです。
[1回目] {"response":"failed","info":"cannot parse as a valid JSON object: invalid object name/value separator at: ''"} {"response":"failed","info":"cannot parse as a valid JSON object: unexpected end of string data"} {"response":"success","info":"processed: 3; failed: 0; total: 3; seconds spent: 0.093064"} {"response":"failed","info":"cannot parse as a valid JSON object: unexpected end of string data"} [2回目] {"response":"failed","info":"cannot parse as a valid JSON object: unexpected end of string data"} {"response":"success","info":"processed: 33; failed: 0; total: 33; seconds spent: 1.029911"} {"response":"success","info":"processed: 3; failed: 0; total: 3; seconds spent: 0.086790"} {"response":"failed","info":"cannot parse as a valid JSON object: unexpected end of string data"} [3回目] {"response":"success","info":"processed: 39; failed: 0; total: 39; seconds spent: 0.074564"} {"response":"failed","info":"cannot parse as a valid JSON object: unexpected end of string data"} {"response":"success","info":"processed: 3; failed: 0; total: 3; seconds spent: 0.085845"} {"response":"failed","info":"cannot parse as a valid JSON object: unexpected end of string data"}
実行する度に成功したり失敗したりと不安定です。
Zabbixサーバのログ
今後は、Zabbixサーバのログを確認してみました。DebugLevel=4に変更しています。
10279:20180108:010832.540 __zbx_zbx_setproctitle() title:'trapper #2 [processing data]' 10279:20180108:010832.540 trapper got '{"data": [{"host": "laptop03", "value": 6.983, "key": "zzz_float_value[dstat.total_cpu_usage.usr]", "clock": "1515341312"}, {"host": "laptop03", "value": 1.496, "key": "zzz_float_value[dstat.total_cpu_usage.sys]", "clock": "1515341312"}, {"host": "laptop03", "value": 91.521, "key": "zzz_float_value[dstat.total_cpu_usage.idl]", "clock": "1515341312"}, {"host": "laptop03", "value": 0.0, "key": "zzz_float_value[dstat.total_cpu_usage.wai]", "clock": "1515341312"}, {"host": "laptop03", "value": 0.0, "key": "zzz_float_value[dstat.total_cpu_usage.stl]", "clock": "1515341312"}, {"host": "laptop03", "value": 0.0, "key": "zzz_float_value[dstat.dsk_nvme0n1.read]", "clock": "1515341312"}, {"host": "laptop03", "value": 0.0, "key": "zzz_float_value[dstat.dsk_nvme0n1.writ]", "clock": "1515341312"}, {"host": "laptop03", "value": 0.0, "key": "zzz_float_value[dstat.paging.in]", "clock": "1515341312"}, {"host": "laptop03", "value": 0.0, "key": "zzz_float_value[dstat.paging.out]", "clock": "1515341312"}, {"host": "laptop03", "value": 1.09, "key": "zzz_float_value[dstat.load_avg.1m]", "clock": "1515341312"}, {"host": "laptop03", "value": 2.84, "key": "zzz_float_value[dstat.load_avg.5m]", "clock": "1515341312"}, {"host": "laptop03", "value": 2.89, "key": "zzz_float_value[dstat.load_avg.15m]", "clock": "1515341312"}, {"host": "laptop03", "value": 12326502400.0, "key": "zzz_float_value[dstat.memory_usage.used]", "clock": "1515341312"}, {"host": "laptop03", "value": 1197580288.0, "key": "zzz_float_value[dstat.memory_usage.free]", "clock": "1515341312"}, {"host": "laptop03", "value": 384909312.0, "key": "zzz_float_value[dstat.memory_usage.buff]", "clock": "1515341312"}, {"host": "laptop03", "value": 3268669440.0, "key": "zzz_float_value[dstat.memory_usage.cach]", "clock": "1515341312"}, {"host": "laptop03", "value": 0.0, "key": "zzz_float_value[dstat.net_wlp4s0.recv]", "clock": "1515341312"}, {"host": "laptop03", "value": 0.0, "key": "zzz_float_value[dstat.net_wlp4s0.send]", "clock": "1515341312"}, {"host": "laptop03", "value": 0.0, "key": "zzz_float_value[dstat.procs.run]", "clock": "1515341312"}, {"host": "laptop03", "value": 0.0, "key": "zzz_float_value[dstat.procs.blk]", "clock": "1515341312"}, {"host": "laptop03", "value": 0.0, "key": "zzz_float_value[dstat.procs.new]", "clock": "1515341312"}, {"host": "laptop03", "value": 0.0, "key": "zzz_float_value[dstat.io_nvme0n1.read]", "clock": "1515341312"}, {"host": "laptop03", "value": 0.0, "key": "zzz_float_value[dstat.io_nvme0n1.writ]", "clock": "1515341312"}, {"host": "laptop03", "value": 0.0, "key": "zzz_float_value[dstat.swap.used]", "clock": "1515341312"}, {"host": "laptop03", "value": 8359243776.0, "key": "zzz_float_value[dstat.swap.free]", "clock": "1515341312"}, {"host": "laptop03", "value": 963.0, "key": "zzz_float_value[dstat.system.int]", "clock": "1515341312"}, {"host": "laptop03", "value": 2470.0, "key": "zzz_float_value[dstat.system.csw]", "clock": "1515341312"}, {"host": "laptop03", "value": 21.0, "key": "zzz_float_value[dstat.tcp_sockets.lis]", "clock": "1515341312"}, {"host": "laptop03", "value": 50.0, "key": "zzz_float_value[dstat.tcp_sockets.act]", "clock": "1515341312"}, {"host": "laptop03", "value": 0.0, "key": "zzz_float_value[dstat.tcp_sockets.syn]", "clock": "1515341312"}, {"host": "laptop03", "value": 4.0, "key": "zzz_float_value[dstat.tcp_sockets.tim]", "clock": "1515341312"}, {"host": "laptop03", "value": 4.0, "key": "zzz_float_value[dstat.tcp_sockets.clo]", "clock": "1515341312"}, {"host": "laptop03", "value": 12.0, "key": "zzz_float_value[dstat.udp.lis]", "clock": "1515341312"}, {"host": "laptop03", "value": 0.0, "key": "zzz_float_value[dstat.udp.act]", "clock": "1515341312"}, {"host": "laptop03", "value": 62.0, "key": "zzz_float_value[dstat.unix_sockets.dgm]", "clock": "1515341312"}, {"host": "laptop03", "value": 1724.0, "key": "zzz_float_value[dstat.unix_sockets.str]", "clock": "1515341312"}, {"host": "laptop03", "value": 638.0, "key": "zzz_float_value[dstat.unix_sockets.lis]", "clock": "1515341312"}, {"host": "laptop03", "value": 1086.0, "key": "zzz_float_value[dstat.unix_sockets.act]", "clock": "1515341312"}, {"host": "laptop03", "value": "{\"data\": [{\"{#KEY_NAME}\": \"dstat.total_cpu_usage.usr\"}, {\"{#KEY_NAME}\": \"dstat.total_cpu_usage.sys\"}, {\"{#KEY_NAME}\": \"dstat.total_cpu_usage.idl\"}, {\"{#KEY_' 10279:20180108:010832.540 In zbx_send_response() 10279:20180108:010832.540 zbx_send_response() '{"response":"failed","info":"cannot parse as a valid JSON object: unexpected end of string data"}' 10279:20180108:010832.540 End of zbx_send_response():SUCCEED 10279:20180108:010832.540 received invalid JSON object from 10.0.0.245: cannot parse as a valid JSON object: unexpected end of string data 10279:20180108:010832.541 __zbx_zbx_setproctitle() title:'trapper #2 [processed data in 0.000432 sec, waiting for connection]'
2行目(trapper got)に送信したJSONデータが記録されていましたが、途中で途切れていました。
後続のログでもJSONをパースできない旨のエラーメッセージが記録されていますので、Zabbixサーバが送信したデータを期待通り受け取れていないようです。
tcpdump
最後にtcpdumpで通信の様子を確認してみました。
クライアント側で下記コマンドでtcpdumpします。
$ sudo tcpdump -n -i any port 10051
下記が失敗した場合の結果です。10.0.0.1がZabbixサーバ、10.0.0.245がクライアントです。最後にZabbixサーバからクライアントにリセットパケットが送られているようです。
01:43:13.795474 IP 10.0.0.245.47534 > 10.0.0.1.zabbix-trapper: Flags [S], seq 2393260655, win 29200, options [mss 1460,sackOK,TS val 4115495880 ecr 0,nop,wscale 7], length 0 01:43:13.798023 IP 10.0.0.1.zabbix-trapper > 10.0.0.245.47534: Flags [S.], seq 1719041873, ack 2393260656, win 28960, options [mss 1460,sackOK,TS val 1378069742 ecr 4115495880,nop,wscale 7], length 0 01:43:13.798092 IP 10.0.0.245.47534 > 10.0.0.1.zabbix-trapper: Flags [.], ack 1, win 229, options [nop,nop,TS val 4115495883 ecr 1378069742], length 0 01:43:13.798490 IP 10.0.0.245.47534 > 10.0.0.1.zabbix-trapper: Flags [.], seq 1:1449, ack 1, win 229, options [nop,nop,TS val 4115495883 ecr 1378069742], length 1448 01:43:13.798513 IP 10.0.0.245.47534 > 10.0.0.1.zabbix-trapper: Flags [.], seq 1449:2897, ack 1, win 229, options [nop,nop,TS val 4115495883 ecr 1378069742], length 1448 01:43:13.798519 IP 10.0.0.245.47534 > 10.0.0.1.zabbix-trapper: Flags [.], seq 2897:4345, ack 1, win 229, options [nop,nop,TS val 4115495883 ecr 1378069742], length 1448 01:43:13.798523 IP 10.0.0.245.47534 > 10.0.0.1.zabbix-trapper: Flags [.], seq 4345:5793, ack 1, win 229, options [nop,nop,TS val 4115495883 ecr 1378069742], length 1448 01:43:13.798527 IP 10.0.0.245.47534 > 10.0.0.1.zabbix-trapper: Flags [P.], seq 5793:5974, ack 1, win 229, options [nop,nop,TS val 4115495883 ecr 1378069742], length 181 01:43:13.799338 IP 10.0.0.1.zabbix-trapper > 10.0.0.245.47534: Flags [.], ack 1449, win 249, options [nop,nop,TS val 1378069745 ecr 4115495883], length 0 01:43:13.799373 IP 10.0.0.1.zabbix-trapper > 10.0.0.245.47534: Flags [.], ack 2897, win 272, options [nop,nop,TS val 1378069745 ecr 4115495883], length 0 01:43:13.799386 IP 10.0.0.1.zabbix-trapper > 10.0.0.245.47534: Flags [.], ack 4345, win 295, options [nop,nop,TS val 1378069745 ecr 4115495883], length 0 01:43:13.799683 IP 10.0.0.1.zabbix-trapper > 10.0.0.245.47534: Flags [P.], seq 1:142, ack 4345, win 295, options [nop,nop,TS val 1378069745 ecr 4115495883], length 141 01:43:13.799710 IP 10.0.0.245.47534 > 10.0.0.1.zabbix-trapper: Flags [.], ack 142, win 237, options [nop,nop,TS val 4115495885 ecr 1378069745], length 0 01:43:13.799732 IP 10.0.0.1.zabbix-trapper > 10.0.0.245.47534: Flags [.], ack 5974, win 340, options [nop,nop,TS val 1378069745 ecr 4115495883], length 0 01:43:13.799747 IP 10.0.0.1.zabbix-trapper > 10.0.0.245.47534: Flags [F.], seq 142, ack 5974, win 340, options [nop,nop,TS val 1378069745 ecr 4115495883], length 0 01:43:13.799760 IP 10.0.0.1.zabbix-trapper > 10.0.0.245.47534: Flags [R.], seq 143, ack 5974, win 340, options [nop,nop,TS val 0 ecr 4115495883], length 0 01:43:13.800474 IP 10.0.0.1.zabbix-trapper > 10.0.0.245.47534: Flags [R], seq 1719042015, win 0, length 0
ちなみに、成功した場合は下記のような結果になりました。
01:47:03.611559 IP 10.0.0.245.48794 > 10.0.0.1.zabbix-trapper: Flags [S], seq 587356983, win 29200, options [mss 1460,sackOK,TS val 4115725695 ecr 0,nop,wscale 7], length 0 01:47:03.616337 IP 10.0.0.1.zabbix-trapper > 10.0.0.245.48794: Flags [S.], seq 1228978652, ack 587356984, win 28960, options [mss 1460,sackOK,TS val 1378299561 ecr 4115725695,nop,wscale 7], length 0 01:47:03.616385 IP 10.0.0.245.48794 > 10.0.0.1.zabbix-trapper: Flags [.], ack 1, win 229, options [nop,nop,TS val 4115725700 ecr 1378299561], length 0 01:47:03.616660 IP 10.0.0.245.48794 > 10.0.0.1.zabbix-trapper: Flags [.], seq 1:1449, ack 1, win 229, options [nop,nop,TS val 4115725700 ecr 1378299561], length 1448 01:47:03.616674 IP 10.0.0.245.48794 > 10.0.0.1.zabbix-trapper: Flags [.], seq 1449:2897, ack 1, win 229, options [nop,nop,TS val 4115725700 ecr 1378299561], length 1448 01:47:03.616676 IP 10.0.0.245.48794 > 10.0.0.1.zabbix-trapper: Flags [.], seq 2897:4345, ack 1, win 229, options [nop,nop,TS val 4115725700 ecr 1378299561], length 1448 01:47:03.616678 IP 10.0.0.245.48794 > 10.0.0.1.zabbix-trapper: Flags [.], seq 4345:5793, ack 1, win 229, options [nop,nop,TS val 4115725700 ecr 1378299561], length 1448 01:47:03.616685 IP 10.0.0.245.48794 > 10.0.0.1.zabbix-trapper: Flags [P.], seq 5793:5967, ack 1, win 229, options [nop,nop,TS val 4115725700 ecr 1378299561], length 174 01:47:03.619438 IP 10.0.0.1.zabbix-trapper > 10.0.0.245.48794: Flags [.], ack 1449, win 249, options [nop,nop,TS val 1378299565 ecr 4115725700], length 0 01:47:03.620498 IP 10.0.0.1.zabbix-trapper > 10.0.0.245.48794: Flags [.], ack 2897, win 272, options [nop,nop,TS val 1378299565 ecr 4115725700], length 0 01:47:03.620749 IP 10.0.0.1.zabbix-trapper > 10.0.0.245.48794: Flags [.], ack 4345, win 295, options [nop,nop,TS val 1378299566 ecr 4115725700], length 0 01:47:03.620785 IP 10.0.0.1.zabbix-trapper > 10.0.0.245.48794: Flags [.], ack 5793, win 317, options [nop,nop,TS val 1378299566 ecr 4115725700], length 0 01:47:03.624183 IP 10.0.0.1.zabbix-trapper > 10.0.0.245.48794: Flags [.], ack 5967, win 340, options [nop,nop,TS val 1378299566 ecr 4115725700], length 0 01:47:03.850201 IP 10.0.0.1.zabbix-trapper > 10.0.0.245.48794: Flags [P.], seq 1:106, ack 5967, win 340, options [nop,nop,TS val 1378299795 ecr 4115725700], length 105 01:47:03.850298 IP 10.0.0.245.48794 > 10.0.0.1.zabbix-trapper: Flags [.], ack 106, win 229, options [nop,nop,TS val 4115725934 ecr 1378299795], length 0 01:47:03.850357 IP 10.0.0.1.zabbix-trapper > 10.0.0.245.48794: Flags [F.], seq 106, ack 5967, win 340, options [nop,nop,TS val 1378299795 ecr 4115725700], length 0 01:47:03.850643 IP 10.0.0.245.48794 > 10.0.0.1.zabbix-trapper: Flags [F.], seq 5967, ack 107, win 229, options [nop,nop,TS val 4115725934 ecr 1378299795], length 0 01:47:03.851771 IP 10.0.0.1.zabbix-trapper > 10.0.0.245.48794: Flags [.], ack 5968, win 340, options [nop,nop,TS val 1378299797 ecr 4115725934], length 0
ApacheのScoreboardをモニタリングする
はじめに
私事ですが、昨年末に運用しているWEBサービスにて、Apacheの同時接続数が上限に達し一時的にサービス提供できなくなる経験をしました。 その際に、エラーログにScoreboardという文字列を含む下記のエラーログが頻発していました。
AH00286: server reached MaxRequestWorkers setting, consider raising the MaxRequestWorkers setting AH00287: server is within MinSpareThreads of MaxRequestWorkers, consider raising the MaxRequestWorkers setting AH00288: scoreboard is full, not at MaxRequestWorkers
対応の一環で、Scoreboadのモニタリング設定などをしたので、その際の内容をまとめておきたいと思います。 また、私の理解不足で不正確な内容を記載している箇所もあるかと思いますが、自分の外部記憶装置代わり的な意味でも記載していますのでご了承下さい。
検証環境
用語
Slot
ApacheがHTTPリクエストを処理する実体のことで、MPMがPreforkであればプロセスのことであり、Worker, Eventであればスレッドのこと。
Scoreboard
Apacheが親プロセスと子プロセスとの間で共有するメモリ領域のこと。各Slot毎にさまざまな情報を保持しているらしい。
下記あたりのコードを見れば、Scoreboardについて詳細にわかりそう。
- https://github.com/apache/httpd/blob/trunk/include/scoreboard.h
- https://github.com/apache/httpd/blob/trunk/server/scoreboard.c
Scoreboardの状態確認方法
調査すると、mod_statusモジュールでステータスページを通じて確認する方法と、共有メモリ上のScoreboardに直接アクセスして情報を取得する方法があるようです。 今回は、前者のmod_statusモジュールを使った方法を記載します。
後者の共有メモリに直接アクセスする方法は情報があまり多くなく、Scoreboardについて実装レベルで把握していないと難しいようだったので、今回は断念しました。 ただ、mod_statusモジュールを使う場合と比較して、モニタリングのためにApacheにアクセスする必要がないため、接続数が飽和している状態でもScoreboardの状態が確認できるという点でメリットがありそうです。
参考までに、下記に記事を紹介しておきます。
mod_statusの有効化とステータスページからのデータ取得
下記の内容をApacheの設定ファイルに追記します。これにより、Apacheの状態がステータスページで確認できるようになります。
LoadModule status_module modules/mod_status.so ExtendedStatus On Listen 81 <VirtualHost *:81> <Location /server-status> SetHandler server-status Require all granted </Location> </VirtualHost>
ここでは、ステータスページのURIがサービスに干渉することを防ぐために、VirtualHostで未使用のポートに対してステータスページを表示させるよう設定しています。 モニタリングや通信制限の運用次第ではありますが、接続元をlocalhostなどに絞るなどのセキュリティ対策は実際の環境に応じて検討したほうが良いと思います。
また、.htaccessなどの設定ファイル内でもステータスページの表示設定ができるようになるため、.htaccessを有効している環境では意図せずステータスページを外部に公開してしまう恐れもあるため注意が必要です。回避策としては、AllowOverrideやAllowOverrideListなどでSetHandlerの使用を許可しないよう設定すれば良さそうです。
https://httpd.apache.org/docs/2.4/mod/core.html#allowoverride
設定ファイルを編集後、Apacheを再起動します。
$ apachectl -t Syntax OK $ sudo systemctl restart httpd
ローカルからステータスページにアクセスしてみます。先のVirtualHostで定義したURLに対して?auto
パラメタをつけてアクセスすることで、下記のようなkey-value形式でデータを取得できます。
$ curl localhost:81/server-status?auto localhost ServerVersion: Apache/2.4.28 (CentOS) OpenSSL/1.0.2k-fips ServerMPM: worker Server Built: Oct 9 2017 12:34:18 CurrentTime: Monday, 01-Jan-2018 16:11:40 JST RestartTime: Monday, 01-Jan-2018 16:08:56 JST ParentServerConfigGeneration: 1 ParentServerMPMGeneration: 0 ServerUptimeSeconds: 163 ServerUptime: 2 minutes 43 seconds Load1: 0.04 Load5: 0.12 Load15: 0.22 Total Accesses: 305 Total kBytes: 361 CPUUser: .39 CPUSystem: .18 CPUChildrenUser: 0 CPUChildrenSystem: 0 CPULoad: .349693 Uptime: 163 ReqPerSec: 1.87117 BytesPerSec: 2267.88 BytesPerReq: 1212.01 BusyWorkers: 1 IdleWorkers: 49 Scoreboard: __________________W_______________________________ TLSSessionCacheStatus CacheType: SHMCB CacheSharedMemory: 512000 CacheCurrentEntries: 76 CacheSubcaches: 32 CacheIndexesPerSubcaches: 88 CacheTimeLeftOldestAvg: 183 CacheTimeLeftOldestMin: 136 CacheTimeLeftOldestMax: 288 CacheIndexUsage: 2% CacheUsage: 3% CacheStoreCount: 76 CacheReplaceCount: 0 CacheExpireCount: 0 CacheDiscardCount: 0 CacheRetrieveHitCount: 0 CacheRetrieveMissCount: 0 CacheRemoveHitCount: 0 CacheRemoveMissCount: 0
ここで、目的のScoreboardは下記のような文字列として表示されており、1文字が1つのSlotの状態を表現しています。
Scoreboard: __________________W_______________________________
各文字とSlotの状態の意味は下記の通りです。
文字 | 状態名 | 意味 |
---|---|---|
_ | Waiting for Connection | プロセス/スレッド起動し、アクセス待ちの状態 |
S | Starting up | プロセス/スレッドが起動中の状態 |
R | Reading Request | クライアントからのアクセスを受け付けて、リクエストを読み込んでいる状態 |
W | Sending Reply | クライアントにレスポンスを返している状態 |
K | Keepalive (read) | KeepAliveによりリクエストを待機している状態 |
D | DNS Lookup | クライアントのIPの名前解決している状態 |
C | Closing connection | 接続を終了している状態 |
L | Logging | ログ出力している状態 |
G | Gracefully finishing | スレッドのgracefulな停止処理中またはそこからの起動中 ? |
I | Idle cleanup of worker | スレッドが終了中な状態 |
. | Open slot with no current process | 空きSlot状態 |
何度かステータスページにアクセスしていると気づきますが、アクセスが全くない状態でもScoreboardには必ず1つW
が表示されます。これはステータスページへのアクセスを処理しているSlotの状態を示しており、そのレスポンス中のScoreboardの状態が取得されステータスページが生成されているためだと思います。
Scorboardの数値化
ステータスページに表示されていたScoreboardは1文字が1つのSlotの状態を表現した文字列として表示されるため、その意味に従って状態毎にカウントして数値化することでモニタリングできるようになります。
ここでは、下記のPythonスクリプトでステータスページのScoreboardの文字列を数値化してみます。
#!/bin/env python import requests import argparse scoreboard_lookup = { '_': 'WaitingForConnection', 'S': 'StartingUp', 'R': 'ReadingRequest', 'W': 'SendingReply', 'K': 'KeepAlive', 'D': 'DNSLookup', 'C': 'ClosingConnection', 'L': 'Logging', 'G': 'GracefullyFinishing', 'I': 'IdleCleanupOfWorker', '.': 'OpenSlotWithNoCurrentProcess', } def main(url): r = requests.get(url) for key, value in [ tuple(x) for x in [ line.split(': ') for line in r.text.splitlines() ] if len(x) == 2]: key = key.replace(' ', '') if key == 'Scoreboard': print('apache.scoreboard.{},{}'.format('TotalSlot', len(value))) for scoreboard_char,scoreboard_label in scoreboard_lookup.items(): print('apache.scoreboard.{},{}'.format(scoreboard_label, value.count(scoreboard_char))) else: print('apache.{},{}'.format(key, value)) if __name__ == '__main__': argparser = argparse.ArgumentParser() argparser.add_argument('-u', '--url', type=str, required=True) args = argparser.parse_args() main(args.url)
上記のスクリプトは、ステータスページの内容を取得して下記の処理をしています。
- csv形式のkey-value(key,value)形式に整形
- keyのprefixに
apache.
を追加 - keyに含まれていた半角スペースを削除
- ScoreboardをSlotの状態毎にカウントし、apache.scoreboard.状態名というkeyのvalueに追加
- Scoreboardの総Slot数(apache.scoreboard.TotalSlot)を追加
適当にファイル名でスクリプトを作成し、実行権限を付与して実行します。
$ vi apache.py $ chmod 755 apache.py $ apache.py -u http://pxy01.private:81/server-status?auto apache.ServerVersion,Apache/2.4.28 (CentOS) OpenSSL/1.0.2k-fips apache.ServerMPM,worker apache.ServerBuilt,Oct 9 2017 12:34:18 apache.CurrentTime,Monday, 01-Jan-2018 17:53:38 JST apache.RestartTime,Monday, 01-Jan-2018 16:08:56 JST apache.ParentServerConfigGeneration,1 apache.ParentServerMPMGeneration,0 apache.ServerUptimeSeconds,6282 apache.ServerUptime,1 hour 44 minutes 42 seconds apache.Load1,0.00 apache.Load5,0.01 apache.Load15,0.05 apache.TotalAccesses,11589 apache.TotalkBytes,13357 apache.CPUUser,16.41 apache.CPUSystem,6.45 apache.CPUChildrenUser,0 apache.CPUChildrenSystem,0 apache.CPULoad,.363897 apache.Uptime,6282 apache.ReqPerSec,1.84479 apache.BytesPerSec,2177.26 apache.BytesPerReq,1180.22 apache.BusyWorkers,1 apache.IdleWorkers,49 apache.scoreboard.TotalSlot,50 apache.scoreboard.ClosingConnection,0 apache.scoreboard.DNSLookup,0 apache.scoreboard.GracefullyFinishing,0 apache.scoreboard.IdleCleanupOfWorker,0 apache.scoreboard.KeepAlive,0 apache.scoreboard.Logging,0 apache.scoreboard.StartingUp,0 apache.scoreboard.ReadingRequest,0 apache.scoreboard.SendingReply,1 apache.scoreboard.WaitingForConnection,49 apache.scoreboard.OpenSlotWithNoCurrentProcess,0 apache.CacheType,SHMCB apache.CacheSharedMemory,512000 apache.CacheCurrentEntries,137 apache.CacheSubcaches,32 apache.CacheIndexesPerSubcaches,88 apache.CacheTimeLeftOldestAvg,65 apache.CacheTimeLeftOldestMin,0 apache.CacheTimeLeftOldestMax,222 apache.CacheIndexUsage,4% apache.CacheUsage,5% apache.CacheStoreCount,2883 apache.CacheReplaceCount,0 apache.CacheExpireCount,2746 apache.CacheDiscardCount,0 apache.CacheRetrieveHitCount,0 apache.CacheRetrieveMissCount,0 apache.CacheRemoveHitCount,0 apache.CacheRemoveMissCount,0
上記のapache.scoreboard.
あたりがScoreboardの状態が数値化したものになります。
私の場合は、上記のkey-valueのデータをZabbix SenderでZabbixサーバに送ることでグラフ化したりしています。
モニタリングツールでScoreboardをモニタリングする参考リンク
私自身ですべて試したことがあるわけではありませんが、有名どころのモニタリングツールを使ってScoreboardをモニタリングする方法の関連リンクを紹介しておきます。
Zabbix
- Docs/howto/apache monitoring script - Zabbix.org
- GitHub - lorf/zapache: Zabbix Apache Monitoring Script (from https://www.zabbix.org/wiki/Docs/howto/apache_monitoring_script#Method_3, originally from https://www.zabbix.com/forum/showthread.php?p=62457)
Datadog
mackerel
Prometheus
Pythonでネストを減らす(if文によるdictのキーチェック編)
やりたいこと
元の方法
YAMLで記述された設定ファイルを読み込み、item_listキーが存在する場合にのみ、キーに対するリストに対してループ処理をするサンプルです。
for文でループ処理する前にif文でキーチェックをしているため、ネストが深くなってしまっています。
with open(config_file) as f: config = yaml.load(f) if 'item_list' in config: for item in config['item_list']: # ここでやりたい処理を記述
三項演算子を使って改善した方法
Pythonの三項演算子を使うことで、if文のキーチェックをfor文の行に持ってくることができます。
with open(config_file) as f: config = yaml.load(f) for item in config['item_list'] if 'item_list' in config else list(): # ここでやりたい処理を記述
上記のように記述することで、
- item_listキーが存在する場合は、そのキーに対するリストに対してループ処理
- item_listキーが存在しない場合は、空のリストに対してループ処理(つまり、処理をスキップさせる)
ことができます。
dictのgetを使って改善した方法
dictのgetメソッドを使うことで、さらにシンプルに記載できます。
with open(config_file) as f: config = yaml.load(f) for item in config.get('item_list', list()): # ここでやりたい処理を記述
dictのgetメソッドでは、get(key[, default])
のように記述でき、下記の振る舞いをします。
- dictにkeyが存在していれば、それに対する値を返す
- dictにkeyが存在していなければ、defaultを返す
- defaultが与えられない場合は、Noneを返す
参考
Pythonとcronで秒単位で定期処理する
やりたいこと
解決方法
Pythonスクリプト作成
下記のように、実行間隔(秒)と実行回数をコマンドライン引数で与えて、Pythonのスクリプトの中で繰り返し実行できるようにします。
import argparse from datetime import datetime import time # ここに定期実行したい処理を記載 def main(): pass if __name__ == '__main__': argparser = argparse.ArgumentParser() argparser.add_argument('-i', '--interval', type=int, default=1) argparser.add_argument('-c', '--count', type=int, default=1) args = argparser.parse_args() _start_time = 0 for i in range(args.count): while int(datetime.now().strftime('%s')) - _start_time < args.interval: time.sleep(0.1) _start_time = int(datetime.now().strftime('%s')) # ここで定期実行したい処理を呼び出す main()
Pythonスクリプトをcronに設定
例えば、5秒毎に処理をしたい場合には、下記のようにcronの設定をします。
$ crontab -e ---- * * * * * /path/to/python_script -i 5 -c 12 ----
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:偉そうなことを言っていますが、既に先人がおりますので、それらを参考にさせていただいています
SSH秘密鍵から公開鍵を作成する
はじめに
SSHの秘密鍵から公開鍵って生成できるのかと思って調べてみたので、手順をまとめておきます。
環境
秘密鍵から公開鍵を生成できるか確認する方法
SSH秘密鍵と公開鍵のペア作成
カレントディレクトリに、test_keyという名前で秘密鍵を、test_key.pubという名前で公開鍵を作成します。
$ ssh-keygen -t rsa -b 4096 -f test_key $ ls -al test_key* -rw------- 1 aoishi aoishi 3247 12月 2 00:35 test_key -rw-r--r-- 1 aoishi aoishi 746 12月 2 00:35 test_key.pub
SSH秘密鍵から公開鍵を作成
ssh-keygenコマンドに-yオプションを付与し、-fオプションで秘密鍵ファイルを指定することで、そのペアとなる公開鍵が生成できます。
$ ssh-keygen -y -f test_key
復元したSSH公開鍵との比較
試しに、ペアで作成した時の公開鍵と、後から生成した公開鍵とで差分を比較してみます。
$ ssh-keygen -y -f test_key | diff test_key.pub - 1c1 < ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDTSzQjqHpUxl2tDhqawT3HFrvq5ACq+7IoTIGxUapiX1bmeV9f1EJHHAuAp4d2YJvaOfawxJoRAgUKnwPxYn5o62ANffyzo36C9H0SsIg+/QVTqfErTUpNGzGo/ATP99Oin1XY3hyCOdghhR4fU2cWbTKwGpPzAMeED012qwgOG12UaA5Us9xdgtyo8MXax36IYrletbsSUtS90K1zw+WWYhJBpBa54R6T/0SQdtA0sIxVrYhZ/mbv3VgP2atI7kMiCHA4MHbZe4rFbHnwG5QkyCGi+2+leoH81u8FF5pSxgyT+iY8tnq7mqqojFYAnyj2tbOfk3nOKBzftfTg05CjNrs4wZyqXEgpsY6BgxMxDdE6mWwjC2PqejjmXkfhjDoywaClRpFEqJV6/3zKzidilajJMH2suowio2dYT01rUdUFyUdQRi2a2ynlX1y0tKnAc0E86evC+sTeddRukc1ka6tFT0CVBsP+MYHe2v6Qq8OwOKItKPSsoUv95Ufw2Hagv4Q4MdrasowilDNdeg4B78jcZektAZ3ofEvfTIia6ae6XxDE8PNHrrhQkDB7kuzPH9wuaixChMt2D3IyHgEFXmWNkEAz2Nj1p+MRGTwNrnUBAWMROO/r2ejMpgbbi44HfLcsBd2Zay+dyJgo+VHe7RIph9CzIEg8S1NozoznpQ== aoishi@ope01.private --- > ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDTSzQjqHpUxl2tDhqawT3HFrvq5ACq+7IoTIGxUapiX1bmeV9f1EJHHAuAp4d2YJvaOfawxJoRAgUKnwPxYn5o62ANffyzo36C9H0SsIg+/QVTqfErTUpNGzGo/ATP99Oin1XY3hyCOdghhR4fU2cWbTKwGpPzAMeED012qwgOG12UaA5Us9xdgtyo8MXax36IYrletbsSUtS90K1zw+WWYhJBpBa54R6T/0SQdtA0sIxVrYhZ/mbv3VgP2atI7kMiCHA4MHbZe4rFbHnwG5QkyCGi+2+leoH81u8FF5pSxgyT+iY8tnq7mqqojFYAnyj2tbOfk3nOKBzftfTg05CjNrs4wZyqXEgpsY6BgxMxDdE6mWwjC2PqejjmXkfhjDoywaClRpFEqJV6/3zKzidilajJMH2suowio2dYT01rUdUFyUdQRi2a2ynlX1y0tKnAc0E86evC+sTeddRukc1ka6tFT0CVBsP+MYHe2v6Qq8OwOKItKPSsoUv95Ufw2Hagv4Q4MdrasowilDNdeg4B78jcZektAZ3ofEvfTIia6ae6XxDE8PNHrrhQkDB7kuzPH9wuaixChMt2D3IyHgEFXmWNkEAz2Nj1p+MRGTwNrnUBAWMROO/r2ejMpgbbi44HfLcsBd2Zay+dyJgo+VHe7RIph9CzIEg8S1NozoznpQ==
差分を確認する限り、当初ペアで作成した公開鍵にはコメント(aoishi@ope01.private)が付いていますが、後から生成した公開鍵にはついていようです。
コメントがあったほうが何かと便利なので、適宜追加しておくと良いですね。
ちなみに、diffコマンドはパイプで標準入力を渡すことで - でその入力を参照して差分比較することができます。
CentOS7のtcpdumpでインターフェースを指定しないとエラーになる
はじめに
CentOS7のtcpdumpで、ネットワークインターフェースを明示的に指定しない場合に下記のメッセージが出力されたので、その際に対応したことをまとめます。
$ sudo tcpdump tcpdump: packet printing is not supported for link type NFLOG: use -w
環境
tcpdumpがエラーになった原因
tcpdumpはネットワークインターフェースが指定されなかった場合、インターフェース一覧の先頭のものに対して動作するようです。
利用可能なネットワークインターフェース一覧は、下記コマンドで確認できます。
$ sudo tcpdump -D 1.nflog (Linux netfilter log (NFLOG) interface) 2.nfqueue (Linux netfilter queue (NFQUEUE) interface) 3.usbmon1 (USB bus number 1) 4.usbmon2 (USB bus number 2) 5.ens160 6.any (Pseudo-device that captures on all interfaces) 7.lo [Loopback]
このため、1. のnflogというインターフェースで動作しており、このインターフェースでtcpdumpしようとしたことでエラーとなっていたようです。
なお上記の仕様については、manコマンドで確認できます。
$ man tcpdump ~~~ -i interface --interface=interface Listen on interface. If unspecified, tcpdump searches the sys‐ tem interface list for the lowest numbered, configured up interface (excluding loopback), which may turn out to be, for example, ``eth0''. On Linux systems with 2.2 or later kernels, an interface argu‐ ment of ``any'' can be used to capture packets from all inter‐ faces. Note that captures on the ``any'' device will not be done in promiscuous mode. If the -D flag is supported, an interface number as printed by that flag can be used as the interface argument, if no inter‐ face on the system has that number as a name. ~~~
tcpdumpでインターフェースを指定する方法
今回確認したかったのは、ens160(CentOS7では一般的?)なので、インターフェース名か一覧の番号を指定してtcpdumpし直します。
$ sudo tcpdump -i ens160 or $ sudo tcpdump -i 5
また、manの説明にもありましたが、anyを指定すると全インターフェースに対して動作させることができるようです。
$ sudo tcpdump -i any
まとめ
CentOS7のtcpdumpでインターフェースを明示的にしないことで発生したメッセージに対する対応方法をまとめてみました。
普段何気なく使っているtcpdumpですが、改めてmanを読むと新たな気づきとか勉強になることが沢山ありますね。
参考
��2898 CentOS7のtcpdumpがなにやらおかしい - Web Patio - CentOSで自宅サーバー構築