MAGAZINE
ルーターマガジン
Ruby
クローラーの負荷監視に便利なSNMPの簡易Rubyラッパー
2019.04.24
こんにちは。エンジニアのAraoです。今回は、簡易的なSNMPのRubyラッパーを紹介します。随分と間が空いてしまいましたが、SNMPでサーバーのモニタリングの続きになります。
本記事で実現したいこと
前回記事で、Net-SNMPを用いて、同一ネットワーク内のLinuxサーバーのCPU使用率、ロードアベレージ、ストレージの使用率、メモリ使用率を監視する方法を紹介しました。
今回は、それらのサーバー監視を、Rubyスクリプト内で実行できるようにすることが目標になります。Rubyスクリプト内で監視を行うことにより、監視対象サーバーの状態を定期的にログファイルに吐き出し、閾値を超えていた場合はSlackやメールで異常を知らせる、などといったことが、お手軽にできるようになります。
Rubyスクリプト内でSNMPを使うためのクラスの実装
snmp_ruby_wrapper.rb
require 'open3'
class SnmpError < StandardError
end
# snmpwalkコマンドの終了ステータスが0以外だった場合、
# 例外SnmpErrorをraiseする
# エラーメッセージはsnmpwalkコマンドの標準エラー出力
# 例:NetSNMPのデフォルトタイムアウト10秒を超えてもレスポンスがない場合、
# Timeout: No Response from server_name (SnmpError)
# がraiseされる
class Snmp
SNMPWALK_VALUE_ONLY = 'snmpwalk -Ovq -v 2c -c internal'
# 値のみを取得するsnmpwalkコマンド
SNMPWALK_QUICK_PRINT = 'snmpwalk -Oq -v 2c -c internal'
# OIDも一緒に取得するsnmpwalkコマンド
CPU_USAGE_OID = 'HOST-RESOURCES-MIB::hrProcessorLoad'
# CPU使用率
# マルチコアの場合それぞれの使用率が返ってくる(INTEGER)
LOAD_AVERAGE_OID = 'UCD-SNMP-MIB::laLoad'
# ロードアベレージ
# 1分間、5分間、15分間が返ってくる(STRING)
STORAGE_NAME_OID = 'HOST-RESOURCES-MIB::hrStorageDescr'
# ストレージ名
# 物理メモリなどの情報も含む(STRING)
STORAGE_SIZE_OID = 'HOST-RESOURCES-MIB::hrStorageSize'
# ストレージの総容量
# 物理メモリなどの情報も含む(INTEGER)
STORAGE_USED_OID = 'HOST-RESOURCES-MIB::hrStorageUsed'
# ストレージの使用量
# 物理メモリなどの情報も含む(INTEGER)
MEMORY_TOTAL_OID = 'UCD-SNMP-MIB::memTotalReal'
# メモリ総容量
# 物理メモリの総容量(INTEGER)
MEMORY_AVAIL_OID = 'UCD-SNMP-MIB::memAvailReal'
# メモリの空き容量
# 物理メモリの空き容量(INTEGER)
def initialize(server_name)
@server_name = server_name
# 監視対象サーバー名
end
def get_cpu_usage
# CPU使用率(%)を取得するメソッド
# 返り値はIntegerのArray(マルチコアの場合に対応するため)
cpu_usages, error_message, exit_status = Open3.capture3("#{SNMPWALK_VALUE_ONLY} #{@server_name} #{CPU_USAGE_OID}")
unless exit_status == 0 then
raise SnmpError.new(error_message.chomp)
end
cpu_usages.split.map{|cpu_usage|
cpu_usage.to_i
}
end
def get_load_average
# 1分間のロードアベレージを取得するメソッド
# 返り値はFloat
load_averages, error_message, exit_status = Open3.capture3("#{SNMPWALK_VALUE_ONLY} #{@server_name} #{LOAD_AVERAGE_OID}")
unless exit_status == 0 then
raise SnmpError.new(error_message.chomp)
end
load_averages.split.first.to_f
end
def get_storage_usage
# スラッシュ始まりのファイルシステムごとの使用率(%)を取得するメソッド
# 返り値は以下形式のArray
# [
# {name: '/dev/sda1(String)', usage: 12.3(Float)},
# {name: '/dev/sdb1(String)', usage: 45.6(Float)},
# ...
# ]
storage_usages = Array.new
storages = Hash.new
storage_names_with_id, error_message, exit_status = Open3.capture3("#{SNMPWALK_QUICK_PRINT} #{@server_name} #{STORAGE_NAME_OID}")
unless exit_status == 0 then
raise SnmpError.new(error_message.chomp)
end
storage_names_with_id.split("\n").each{|storage_name_with_id|
storage_name_with_id.match(/^#{STORAGE_NAME_OID}\.(\d+)\s(.+)$/)
id = $1
storage_name = $2
if storage_name.match(/^\//) then
storages.store(id, {name: storage_name})
end
}
storage_sizes_with_id, error_message, exit_status = Open3.capture3("#{SNMPWALK_QUICK_PRINT} #{@server_name} #{STORAGE_SIZE_OID}")
unless exit_status == 0 then
raise SnmpError.new(error_message.chomp)
end
storage_sizes_with_id.split("\n").each{|storage_size_with_id|
storage_size_with_id.match(/^#{STORAGE_SIZE_OID}\.(\d+)\s(.+)$/)
id = $1
storage_size = $2.to_f
if storages.has_key?(id) then
storages[id][:size] = storage_size
end
}
storage_useds_with_id, error_message, exit_status = Open3.capture3("#{SNMPWALK_QUICK_PRINT} #{@server_name} #{STORAGE_USED_OID}")
unless exit_status == 0 then
raise SnmpError.new(error_message.chomp)
end
storage_useds_with_id.split("\n").each{|storage_used_with_id|
storage_used_with_id.match(/^#{STORAGE_USED_OID}\.(\d+)\s(.+)$/)
id = $1
storage_used = $2.to_f
if storages.has_key?(id) then
storages[id][:used] = storage_used
end
}
storages.each_value{|name_size_used|
name = name_size_used[:name]
usage = name_size_used[:used] / name_size_used[:size] * 100
storage_usages.push({name: name, usage: usage})
}
storage_usages
end
def get_memory_usage
# メモリ使用率(%)を取得するメソッド
# 返り値はFloat
memory_total, error_message, exit_status = Open3.capture3("#{SNMPWALK_VALUE_ONLY} #{@server_name} #{MEMORY_TOTAL_OID}")
unless exit_status == 0 then
raise SnmpError.new(error_message.chomp)
end
memory_avail, error_message, exit_status = Open3.capture3("#{SNMPWALK_VALUE_ONLY} #{@server_name} #{MEMORY_AVAIL_OID}")
unless exit_status == 0 then
raise SnmpError.new(error_message.chomp)
end
memory_total_f = memory_total.to_f
memory_avail_f = memory_avail.to_f
100 - memory_avail_f / memory_total_f * 100
end
end
使用例
monitor.rb
require './snmp_ruby_wrapper.rb'
# 使用例
# 定期監視する場合、各自で閾値や通知先を設定してください
snmp_web102 = Snmp.new('web102')
pp snmp_web102.get_cpu_usage
pp snmp_web102.get_load_average
pp snmp_web102.get_storage_usage
pp snmp_web102.get_memory_usage
実行結果
$ ruby monitor.rb
[16]
0.26
[{:name=>"/", :usage=>17.468030286989382},
{:name=>"/dev/shm", :usage=>0.0},
{:name=>"/run", :usage=>11.34152585765489},
{:name=>"/sys/fs/cgroup", :usage=>0.0},
{:name=>"/run/user/0", :usage=>0.0}]
73.48760925655539
簡単な解説
大体のことはコード内の説明に書いてあるので、エラー処理の部分だけちょっと解説しておきます。最初はRuby側でタイムアウトしたときに例外(SnmpError)を発生させようとしていたのですが、Ruby内部で呼び出しているSNMPコマンド側でもエラーが発生するとのことだったので、SNMPコマンドが正常終了しなかった場合に例外を発生させるようにしました。
web4649のロードアベレージが1を超えていたり、メモリ使用率が90%を超えていたり、そもそもSNMPでの監視ができない場合にどこかへ通知したいのなら、こんな感じにすれば良いと思います。
monitor_web4649.rb
require './snmp_ruby_wrapper.rb'
snmp_web4649 = Snmp.new('web4649')
begin
load_average = snmp_web4649.get_load_average
if load_average > 1.0 then
alert("ロードアベレージが1を超えています!ロードアベレージ:#{load_average}")
end
memory_usage = snmp_web4649.get_memory_usage
if memory_usage > 90.0 then
alert("メモリ使用率が90%を超えています!メモリ使用率:#{memory_usage}")
end
rescue SnmpError
alert("web4649が止まっているかもしれません!")
end
まとめ
Net-SNMPのRubyラッパーを作ってあげることで、サーバーを監視してそのログを溜めたり、異常があったときにどこかへ通知することが簡単にできるようになりました。皆さんもこれを機に、サーバー監視に挑戦してみてください。
CONTACT
お問い合わせ・ご依頼はこちらから