zbxapi(Ruby)でZabbixから取得した結果をキャッシュする
はじめに
BounscaleというオートスケールするHeroku Addonを作っています。
Bounscaleはオートスケーリングの実施をするバックエンド部分にオープンソースの監視ソフトZabbixを利用しています。1サーバ当たりの集約率を上げるために、Zabbix自体への接続をできるだけ回避する方法を考案したので、下記に書いておきます。
背景
ZabbixはZabbixAPIというWebAPIを備えています。Bounscaleでは、フロントエンドのRubyからZabbixAPIへの接続ライブラリにはzbxapiというgemを利用しています。
Zabbix上のデータが多数溜まってくると、このZabbixAPIからのレスポンスが悪くなってきて、フロントエンドがもっさりしてきます。
もちろんZabbix自体のチューニングを実施する事も重要ですが、ほとんど更新のないようなデータを、Rubyが繰り返しZabbixへリクエスト部分があったので、キャッシュ化することで不要なやりとりを回避しようと考えました。
アイデア
ZabbixAPIはHTTPのリクエスト/レスポンスをJSON形式でやり取りします。
そこで、リクエストのJSONデータをキー、レスポンスをバリューとしたキーバリューストアを準備すれば、キャッシングに使えそうです。
キーは一意であれば全データである必要はありませんので、SHAなどでハッシュ化した文字列を持たせておけば十分でしょう。
バージョン
Zabbix 2.0.3
zbxapi 0.2.415
実装方式
zbxapiライブラリのメソッドをRubyのオープンクラスとaliasでチェーン化して、間にキャッシュの機構をはさみます。キーバリューストアは今回のサンプルでは単にグローバルなハッシュテーブルとします。
ソース
下記のような感じで実装してみました。
一応Thread.currentでスイッチのON/OFFを切り替えていてcache_enableブロックで囲まれている箇所でのみ有効となるようにしています。(Thread.currentはノンブロッキングだと崩壊しそうですが、今回はサンプルなので多めに見てください)
また、リクエストのJSONの中に全体の問い合わせの通番のような感じでカウントアップしていく値が含まれているので、そこは塗りつぶし(gsub)しています。
require 'rubygems'
require 'zbxapi'
require 'openssl'
# 簡易なキャッシュ用のクラス
class ZabbixCache
class << self
@@cache = {}
def set(key, value)
@@cache[key] = value
end
def get(key)
@@cache[key]
end
def cache_enable
begin
Thread.current[:zabbix_cache_enable] = true
yield
ensure
Thread.current[:zabbix_cache_enable] = nil
end
end
end
end
# zbxapiから提供されるZabbixAPIクラスを再オープン
class ZabbixAPI
alias :do_request_orig :do_request
private
def do_request(json_obj,truncate_length=5000)
if defined? ZabbixCache
json_obj_key = OpenSSL::Digest::SHA1.hexdigest(json_obj.gsub(/"id":\d*/, ''))
cache = ZabbixCache.get(json_obj_key)
result = nil
enable = Thread.current[:zabbix_cache_enable]
if cache && enable
puts 'CACHE HIT'
result = cache
else
result = do_request_orig(json_obj, truncate_length)
ZabbixCache.set(json_obj_key, result)
end
return result
else
return do_request_orig(json_obj, truncate_length)
end
end
end
# 実行
puts "####### login ######"
zabbix = ZabbixAPI.new('http://localhost/zabbix', :debug => 4)
zabbix.login('admin', 'zabbix')
puts "####### request without cache ######"
result = zabbix.user.get(:filter => {:alias => ['admin']}).first
p result
puts "####### request with cache ######"
ZabbixCache.cache_enable do
cached_result = zabbix.user.get(:filter => {:alias => ['admin']}).first
p cached_result
end
結果
結果は下記です。
####### login ######
D4 ..././zbxapi.rb:do_request_orig:352 Sending: {"method":"user.login","auth":null,"params":{"password":"zabbix","user":"admin"},"id":0,"jsonrpc":"2.0"}
D4 ..././zbxapi.rb:do_request_orig:354 Response Code: 200
D4 ..././zbxapi.rb:do_request_orig:355 Response Body: {"jsonrpc":"2.0","result":"efbabb46f84d3c9f409392b575261156","id":0}
D4 ..././zbxapi.rb:do_request_orig:352 Sending: {"method":"APIInfo.version","auth":"efbabb46f84d3c9f409392b575261156","params":{},"id":1,"jsonrpc":"2.0"}
D4 ..././zbxapi.rb:do_request_orig:354 Response Code: 200
D4 ..././zbxapi.rb:do_request_orig:355 Response Body: {"jsonrpc":"2.0","result":"1.4","id":1}
####### request without cache ######
D4 ..././zbxapi.rb:do_request_orig:352 Sending: {"method":"user.get","auth":"efbabb46f84d3c9f409392b575261156","params":{"filter":{"alias":["admin"]}},"id":2,"jsonrpc":"2.0"}
D4 ..././zbxapi.rb:do_request_orig:354 Response Code: 200
D4 ..././zbxapi.rb:do_request_orig:355 Response Body: {"jsonrpc":"2.0","result":[{"userid":"1"}],"id":2}
{"userid"=>"1"}
####### request with cache ######
CACHE HIT
{"userid"=>"1"}
下記の部分がZabbixAPIへのリクエスト処理です。
D4 ..././zbxapi.rb:do_request_orig:352 Sending: {"method":"user.get","auth":"efbabb46f84d3c9f409392b575261156","params":{"filter":{"alias":["admin"]}},"id":2,"jsonrpc":"2.0"}
D4 ..././zbxapi.rb:do_request_orig:354 Response Code: 200
D4 ..././zbxapi.rb:do_request_orig:355 Response Body: {"jsonrpc":"2.0","result":[{"userid":"1"}],"id":2}
キャッシュを効かせた状態では表示されていないので、見事にキャッシュされています。
終わりに
キャッシュの機構の骨子は上記で作ることができました。
次はどのタイミングでリフレッシュして最新のデータに同期するのかが問題となってきますが、これについてはアプリケーション毎にタイミングが異なるので、慎重に検討する必要があるでしょう。