Studio3104::BLOG.new

uninitialized constant Studio3104 (NameError)

fluent-plugin-graphite を書いたよ

graphite にメトリクスをポストする fluent-plugin を書きました
先に github で公開されていた fluent-plugin-graphite がありましたが、イチから書いて gem release いたしました

https://github.com/studio3104/fluent-plugin-graphite
http://rubygems.org/gems/fluent-plugin-graphite

なぜイチから書いたのか

以下のような箇所に懸念があり、修正だと結局まるっと書き直すのと変わらないと思いイチから書いてしまいました

  • 先行プラグインは、
    • Fluent::BufferedOutput を継承し、内部でサンプリングやカウントなどの計算をしていたが、そういうのは他のプラグインに任せて、来た値をそのまま投げてあげればいいのではないかと思った
    • レコードの key というフィールドと、設定の key_prefix からグラフ名を生成し、 count, gauge というフィールドに値を入れなければならず、少し扱いにくさを感じた
    • メトリクスを送信するたびに TCP コネクションの切断、切断をしていた

新しく書いたプラグインの機能

詳しくは README を見ていただきたく

  • メトリクスを持つフィールドを指定して出力対象とする
  • グラフ名はタグと出力対象のフィールドのキーから生成
  • TCP コネクションを可能な限り使いまわす (graphite-api.gem による機能)
  • バッファリングしておいてある程度まとめてメトリクスを送信 (graphite-api.gem による機能)

たとえば dstat からグラフを生成したい場合

こんな感じの設定をすると、

<source>
  type dstat
  tag dstat.__HOSTNAME__
  option -lnc
  delay 3
</source>

<match dstat.**>
  type flatten_hash
  add_tag_prefix graphite.
  separator .
</match>

<match graphite.dstat.**>
  type graphite
  host localhost
  port 2003
  tag_for prefix
  remove_tag_prefix graphite.dstat.
  name_key_pattern ^((?!hostname).)*$
</match>

こんな感じのグラフが出来る

f:id:studio3104:20140408153558p:plain

先行プラグインとの互換性

設定ファイルを頑張って書きなおしてもらえれば同じグラフに対してメトリクスを送るようには出来ます
先日の このエントリ を例にすると、以下の様に設定ファイルを書き換えてあげる必要があります

<source>
  type dstat
  tag dstat
  option -lcn
  delay 5
</source>
 
<match dstat>
  type copy
  <store>
    type map
    tag  "map.dstat.gauges." + record["hostname"]
    time time
    record { "loadavg-short" => record["dstat"]["load avg"]["1m"] }
  </store>
  <store>
    type map
    tag  "map.dstat.gauges." + record["hostname"]
    time time
    record { "cpu-usr" => record["dstat"]["total cpu usage"]["usr"] }
  </store>
  <store>
    type map
    tag  "map.dstat.gauges." + record["hostname"]
    time time
    record { "cpu-sys" => record["dstat"]["total cpu usage"]["sys"] }
  </store>
  <store>
    type map
    tag  "map.dstat.gauges." + record["hostname"]
    time time
    record { "cpu-hiq" => record["dstat"]["total cpu usage"]["hiq"] }
  </store>
  <store>
    type map
    tag  "map.dstat.gauges." + record["hostname"]
    time time
    record { "cpu-siq" => record["dstat"]["total cpu usage"]["siq"] }
  </store>
  <store>
    type map
    tag  "map.dstat.gauges." + record["hostname"]
    time time
    record { "net-recv" => record["dstat"]["net/total"]["recv"] }
  </store>  
  <store>
    type map
    tag  "map.dstat.gauges." + record["hostname"]
    time time
    record { "net-send" => record["dstat"]["net/total"]["send"] }
  </store>  
</match>
 
<match map.dstat.**>
  type              graphite
  host              localhost
  port              2003
  remove_tag_prefix map
  name_key_pattern  .+
</match>

#CROSS2014 で #ぶつかり稽古 やります!

ぶつかり稽古って?

オリジナルは昨年11月に行われたこちらのイベントです。エンジニアとエンジニアとの魂のぶつかり愛。

企画について

オリジナルのほうはペアプロでしたが、今回は趣向を変えて、コードレビューをテーマとしました。
自分はオリジナルのぶつかり稽古の関係者ではないのですが、許可を賜り、今回の企画名に冠しております。

実行委員長の @ さんがご自身のブログで仰っておられますが、今年の CROSS はこのようなテーマのもと各セッションが準備されてきました。

今年はテーマとして「クロスでススム、クロスで変わる」を掲げました。 「交流」「議論」を通して、明日への知識・人脈をひとつでも得て、変わる機会になってくだされば幸いです。

自分がセッションオーナーを務めさせていただくことになったセッションでは、このようなモチベーションのもと企画しました。

  • 普段コードレビュをしてもらっていて、よそのあの人達はどんな感じでやっているのだろうと気になる
  • 普段の勉強会の発表やセッションは最適化された手法を人に話す用にブラッシュアップされたものになっていることが多いので、現場感ライブ感のあるセッションを見たい

どんなセッション?

さて肝心の内容ですが、このような感じでお届けする予定です。

  • コードレビュをテーマとしたパネルディスカッションを軸として進行
    • 気をつけていること
    • 使用しているツールについて
    • など!など!!
  • 実際にレビュー、修正を行いながら進行
    • レビュー、修正は各ペアで予め何往復かしておいてもらっているので、普段の開発の一部分を切り取ってお見せする、という感じ

詳しくは実際にセッションにいらっしゃってご覧くださればと存じます。

各弟子に実装していただいた WEB アプリケーションは事前にデプロイし聴衆の方が見られるようにします。 また、各リポジトリの URL も公表しますので、勇気のあるあなたはレビュアーとしてライブで参加することも出来てしまうかもしれません!!!

課題の WEB アプリケーション

仕様はこんな感じです。
コレを各弟子に実装していただきました。

  • アイドルグループの人気楽曲投票WEBアプリケーション
    • 要件
      • 661曲の候補の中から好きな1曲に投票
      • 1シリアルナンバーにつき1回投票可能
      • 最低でも1,000,000個のシリアルナンバーが発行されている
      • 会員登録は不要
    • 使用するフレームワーク、データストアなどは自由
    • スキーマと楽曲データの定義もお願いします

お待ちしております

会場でお会いしましょう!!!!!!

http://www.cross-party.com/programs/butsukari/

HRForecast に値を投げた時に一緒に複合グラフも作っちゃいたい!

HRForecast の API で値を投げた時に返ってくる JSON に、これまではエラーの有無とエラーメッセージだけでしたが、POST 成功時にはグラフの情報が含まれるようになりました。

{
    :metricses => [
        [0] {
            :section_name => "test_section",
                      :id => "1",
                  :colors => "[\"#99cc33\"]",
              :updated_at => nil,
            :service_name => "test_service",
              :graph_name => "test_graph",
                   :color => "#99cc33",
                    :meta => "{\"color\":\"#99cc33\"}",
              :created_at => nil,
                    :sort => "0"
        }
    ],
        :error => 0
}

他にもいくつかの JSON API が追加され、ますます便利になってしまっています。 https://github.com/kazeburo/HRForecast/commit/a5b28908a0fe53c45f9b576687df6a58af385b17

値を投げた時にグラフの情報が返ってくれると例えば何が嬉しいのか

複合グラフを作成する API 自体はないのですが、これまででも POST してあげれば WEB UI からでなくても複合グラフを作成することは出来ました。 しかし複合グラフを作成するには、複合する対象のグラフの ID を送ってあげる必要があります。 ちょっと変な実装をすればグラフ ID を HTTP リクエストで取得することは出来ますが、トリッキーなことをせずとも複合グラフの作成が出来るようになります。

こんな感じでどうでしょうか

ふたつのグラフ(test_1, test_2)に値を投げて、複合グラフ(test_complex)がなければ作成します。
ちょっと工夫すれば test_complex がすでに作られていて、そこに test_3 をつっこみたい、なんてこともカンタンに出来るでしょう。

require "net/http"
require "json"

class HRForecastRequestError < StandardError; end
class HRForecast
  def initialize(fqdn, bind_port, enable_https = false)
    @base_url = enable_https ? "https://" + fqdn : "http://" + fqdn
    @bind_port = bind_port
  end

  # datetime のサポートするフォーマットはドキュメント参照: @base_url/docs
  def post_value(service_name, section_name, graph_name, post_bodies)
    value = post_bodies[:value]
    datetime = post_bodies[:datetime] ? post_bodies[:datetime] : Time.now

    graph_path = [service_name, section_name, graph_name].join("/")
    result = post_request("/api/" + graph_path, number: value, datetime: datetime)

    # 失敗すると、200 で body にエラーメッセージの入った JSON が返るので成否はそこを見て判断
    if result[:error] == 1
      raise HRForecastRequestError, "could not post value to #{graph_path} (#{result[:messages].to_s.chomp})"
    end

    result[:metricses].first[:id].to_i
  end

  def create_complex_graph(service_name, section_name, graph_name, path_data, graph_options = {})
    description = graph_options[:description] ? graph_options[:description] : ""
    stack = graph_options[:stack] ? graph_options[:stack] : 0
    sort = graph_options[:sort] ? graph_options[:sort] : 19

    result = post_request("/add_complex", {
      service_name: service_name,
      section_name: section_name,
      graph_name: graph_name,
      description: description,
      stack: stack, # 0:積み上げないグラフ, 1:積み上げるグラフ
      sort: sort, # 値の大きいモノ順で list に表示される (0..19)
      :"path-data" => path_data # 複合グラフに突っ込むグラフの ID を配列で渡す
    })

    # 失敗すると、200 で body にエラーメッセージの入った JSON が返るので成否はそこを見て判断
    if result[:error] == 1
      graph_path = [service_name, section_name, graph_name].join("/")
      raise HRForecastRequestError, "could not create complex graph #{graph_path} (#{result[:messages].to_s.chomp})"
    end
  end

  def complex_graph_exist?(service_name, section_name, graph_name)
    url = URI.parse(@base_url + ":" + @bind_port.to_s + ["/json_complex", service_name, section_name, graph_name].join("/"))
    http = Net::HTTP.new(url.host, url.port)
    http.get(url.path).is_a?(Net::HTTPSuccess)
  end

  private
  def post_request(request_path, post_data)
    url = @base_url + ":" + @bind_port.to_s + request_path
    response = Net::HTTP.post_form(URI.parse(url), post_data)

    raise HRForecastRequestError, "post request was not success" unless  response.is_a?(Net::HTTPSuccess)
    JSON.parse(response.body, symbolize_names: true)
  end
end
hf = HRForecast.new("test.hrforecast.com", 5127)
service_name = "test_service"
section_name = "test_section"
graph_name_prefix = "test"

test1_graph_id = hf.post_value(service_name, section_name, graph_name_prefix + "_1", value: rand(100) + 1)
test2_graph_id = hf.post_value(service_name, section_name, graph_name_prefix + "_2", value: rand(100) + 1)

complex_graph_name = graph_name_prefix + "_complex"
if !hf.complex_graph_exist?(service_name, section_name, complex_graph_name) && test1_graph_id && test2_graph_id
  hf.create_complex_graph(
    service_name, section_name, complex_graph_name,
    [ test1_graph_id, test2_graph_id ]
  )
end

余談

前述のとおり、このような変な実装によってグラフ ID を取得することは出来ます。

def get_graph_id(service_name, section_name, graph_name)
  # HTTP リクエストでグラフ ID を取得出来るクチが csv ダウンロードリンクしかなかったぽい
  url = URI.parse(@base_url + ":" + @bind_port.to_s + ["/csv", service_name, section_name, graph_name].join("/"))
  http = Net::HTTP.new(url.host, url.port)

  # "d=1" をつけてリクエストすると content-disposition の csv のファイル名からグラフ ID が取得できる
  response = http.head(url.path + "?d=1")
  return $1.to_i if response["content-disposition"] =~ %r{attachment; filename=\"metrics_(\d+).csv\"}
end

自分で書いてて違和感があったので、「複合グラフを作成するために、値が POSTされたときにグラフ ID を返すようにって出来ますか?」って kazeburo さんに相談したら、数分で実装してくれました。
はじめからお願いすれば良かった感が強いですけど、自分はこの変な実装をするために結構な時間を費やしてしまったのですが HRForecast のコードを読んでみたりだとか色々勉強になったこともあったのでまぁそれはそれで良かったと考えます。前向きに。

Percona Cloud Tools を試した

インストールや設定などは、pt-agent のドキュメントの通りに進めれば難なくこなせるはずですので割愛します。

感想

以下の点などを加味して考慮した結果、思っていたほどいいモノではないような気がしました。
ざっと使ってみただけなので誤っている点などあるかも知れませんが、ご容赦を。
「ここちがうよ!」「こうすればもっといいよ!」みたいなのよろしくお願い申し上げます。

pt-agent というエージェントを root 権限で動かさなくてはならない

pt-agent の設定は、Percona Cloud Tools の WEB 画面からも設定変更を行えます。
例えば以下画像だと、How often to report のところをいじるとサーバ上にある cron のファイルが書き換えられます。

f:id:studio3104:20131107163428p:plain

インターネットから操作するのに root 権限あげちゃうのか、、、って感じ。

[追記]
root じゃなくても動かすことは出来る模様。ドキュメントに書いてあった。
でも「面倒な手順で手動インストールが必要で、わかんなかったら Percona に問い合わせてね!」みたいな感じだ。

pt-agent must be installed and ran as root.
It is possible to run as a non-root user, but this requires a more complicated and manual installation.
Please contact Percona for help if you need to run pt-agent as a non-root user.

pt-agentMySQL に作成する pt_agent なるユーザに SUPER 権限がつく

上の画像を例にすると、Long query time を書き換えて Save した場合に、サーバ上の MySQLSET GLOBAL long_query_time=0.000000 とか実行されます。

インターネットから操作するのに SUPER 権限あげちゃうのか、、、って感じ。

mysql> SHOW GRANTS FOR 'pt_agent'@'localhost';
+-----------------------------------------------------------------------------------------------------------------+
| Grants for pt_agent@localhost                                                                                   |
+-----------------------------------------------------------------------------------------------------------------+
| GRANT SUPER ON *.* TO 'pt_agent'@'localhost' IDENTIFIED BY PASSWORD '*16CED81DAC5917722E82063353A27E11178399F0' |
+-----------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

MySQL サーバに pt-agent を泳がせなければならない

(このへんはあまり深くタッチしてないので、うまいやり方があるのかもしれないです。)

ひとつの pt-agent プロセスが ひとつの mysqld と対になるようなので、サーバごとにエージェントを泳がせなくてはなりません。
そうすると、一旦ログを集約サーバなどに集めておいて、そこからだけ投げるみたいなことがやりにくくなります。
ファイルやクエリごとにタグを付けられるわけではなさそうだし、どのクエリがどのサーバのモノなのかを一意に識別させる手段が、どのエージェントから送られてきたログなのか、というところだけなので。
タスクが JSON (/var/lib/pt-agent/servicesにあるファイル) で定義されているので、それをうまいことなんとかすればもしかしたらどうにか出来るのかも知れません。未検証。でもやれたとしても面倒な感じがする。

あとDB サーバが直接インターネットに繋ぎにいけないような構成をとっている場合には proxy を経由させたりとかしなくてはならないかも。面倒。

上述のエージェントに与える権限についてだけど、サーバごとにエージェントが要るのであれば、WEB から設定変えられるほうがまぁいいとは思うけど、権限過多ではないかという気がする。 サーバ もイントラで動かせるんだったらよいと思うけど、クラウドだしなぁ。

こんな感じのサマリー画面

ひとつの DB サーバ全体

f:id:studio3104:20131107170510p:plain

クエリの種類ごと

f:id:studio3104:20131107170640p:plain

これだったら EXPLAIN までしてから結果を表示して欲しい感じがする。。

f:id:studio3104:20131107170738p:plain

これも。。

f:id:studio3104:20131107170848p:plain

「偽だったら真、真だったら偽」みたいなのわかりにくい

flag ? false : true

みたいなコード見かけて、

!flag ? true : false

わかりにくいからこうしたほうがよさそうだと思ったけど、

!flag

これだけで良いじゃん、って指摘してもらった。確かに。

WeeChat は IRC Bouncer にもなる

テキストベースの IRC クライアントとして有名な WeeChat ですが、IRC Bouncer としても使えることがわかり、使ってみたら大変によさそうであったので共有いたします。

旧環境

最近まではこんな感じで ZNC に IRC サーバに繋ぎっぱなしにさせて、クライアントが繋いできたときは設定された行数をプレイバックするみたいにして使っていた。

+--------------+    IRC     +---------+  IRC   +-------------+
| LimeChat(PC) | ---------> |+-------+| -----> | Company IRC |
+--------------+            ||  ZNC  ||        +-------------+
+------------------+  IRC   |+-------+|  IRC   +----------+
| LimeChat(iPhone) | -----> | Server  | -----> | freenode |
+------------------+        +---------+        +----------+

これだとちょっとダルい問題があって、PC からだとネットワークが切れて再接続されるたびにバッファプレイバックするので、直近で nick 呼ばれてたりするといちいち LimeChat が通知出して来てウザい。
iPhone からだと、細い回線下ではバッファの読み込みに超時間がかかってウザい。
かと言ってバッファを少なくし過ぎたり、バッファを残さないようにするとかだと利便性に欠いてしまうので大変ウザい。

新環境

そこでこのような構成にしてみた。
WeeChat はサーバの tmux 上で動いていて、 SSH してから tmux attach してから普通にクライアントとして使います。
最初は WeeChat のウラに ZNC を残しておいて、WeeChat も iPhone も ZNC に繋ぎに行くようにしようと考えていたけど、WeeChat に IRC Boucer としての機能があるとわかり、ZNC は落とした。

+----+        SSH           +-----------+  IRC   +-------------+
| PC | -------------------> |+---------+| -----> | Company IRC |
+----+                      || WeeChat ||        +-------------+
+------------------+  IRC   |+---------+|  IRC   +----------+
| LimeChat(iPhone) | -----> |   Server  | -----> | freenode |
+------------------+        +-----------+        +----------+

新環境のメリットとデメリット

ごく短期間使っただけの所感です。

  • メリット
    • 未読行だけバッファプレイバックする!!
      • ZNC のように、毎回設定された行を読みに行かない
        • iPhone からの利用で上述のようなイライラがない
      • バッファプレイバックに関しては特に設定不要
        • もちろん設定で制御することは可能
    • 集中力が保つようになった
      • LimeChat とターミナルの行き来は地味にダルい
      • ターミナルに集中出来ると、ダレずに作業出来る
      • マルチモニタにして片方に置いておくとかしなくていい
    • CLI でッターンしてるとなんとなくカッコいい感じがする
  • デメリット
    • NOTICE をプレイバックしない!!!!
      • コレは自分にとって結構致命的
      • ユーザーズガイド読んだけどよくわからんかった
      • 誰か知ってたら是非教えて欲しい
      • まぁでもバッファプレイバックを必要とするのは iPhone だけだし、WeeChat からはちゃんと見られるのでとりあえずはおkかな・・・
iPhone と WeeChat で未読が共有出来るのは本当に便利。

Relay の設定

接続先のサーバごとに待ち受けポートを変えるなど、いくつかやり方があるので、ユーザーズガイドを一読してからお試しになることをオススメいたします。
今回は SSL で接続し、ひとつのポートで待ち受けて、サーバパスワードで接続先サーバを区別する方法を紹介します。

  • 前提
    IRC サーバへの接続など、WeeChat を IRC クライアントとしてひと通り普通に使えるようにしておく必要があります。
    ここでは割愛します。クイックスタートガイドを読みましょう。

  • SSL 鍵作成

$ mkdir -p ~/.weechat/ssl
$ cd ~/.weechat/ssl
$ openssl req -nodes -newkey rsa:2048 -keyout relay.pem -x509 -days 3650 -out relay.pem
weechat: /relay sslcertkey
  • パスワードを設定
weechat: /set relay.network.password **********
  • リレー設定
weechat: /relay add ssl.irc 8000
  • クライアントの設定
    • SSL 有効
    • ポート 8000
    • サーバパスワード servername:password

おまけ

WeeChat に乗り換えてやったことを都度書くようにした。
- WeeChat を入れてやったことまとめ
オススメプラグインや設定などあれば、是非教えて下さい。
Enjoy IRC!!