zbxapi(Ruby)でZabbixから取得した結果をキャッシュする

zbxapi(Ruby)でZabbixから取得した結果をキャッシュする
====

はじめに
----
[Bounscale](https://github.com/bounscale/bounscale/)というオートスケールするHeroku Addonを作っています。

Bounscaleはオートスケーリングの実施をするバックエンド部分にオープンソースの監視ソフト[Zabbix](https://www.zabbix.org/)を利用しています。１サーバ当たりの集約率を上げるために、Zabbix自体への接続をできるだけ回避する方法を考案したので、下記に書いておきます。

背景
----
Zabbixは[ZabbixAPIというWebAPI](https://www.zabbix.com/documentation/2.0/manual/appendix/api/api)を備えています。Bounscaleでは、フロントエンドのRubyからZabbixAPIへの接続ライブラリには[zbxapi](http://trac.red-tux.net/wiki/zbx_api)という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）しています。

````ruby
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}
````

キャッシュを効かせた状態では表示されていないので、見事にキャッシュされています。

終わりに
----
キャッシュの機構の骨子は上記で作ることができました。

次はどのタイミングでリフレッシュして最新のデータに同期するのかが問題となってきますが、これについてはアプリケーション毎にタイミングが異なるので、慎重に検討する必要があるでしょう。