Ruby でも Zipkin がしたい!
Ruby application でも Zipkin collctor に tracing data を送ってトレーストレースするためのメモ第1弾。まずは SERVER_RECV と SERVER_SEND だけを Zipkin collector に送信するやりかた。
経路としては App -> fluentd in-scribe-plugin -> fluentd out-scribe-plugin -> zipkin collector という経路にする。
使用したコードは https://gist.github.com/taiki45/057d50a5a3e5a9aa0407
fluentd の設定
scribe のデフォルトポートで待ち受けて、zipkin collector のポートへ送信する。
<source> type scribe port 1463 </source> <match zipkin.**> type scribe port 9410 </match>
port の指定は zipkin-collector-core/src/main/scala/com/twitter/zipkin/collector/builder/CollectorServiceBuilder.scala にデフォルト値がある。Admin port がなにに使われるのかはまだ調べてない。config ファイルで指定できるはずだ!(まだわかってない)
Zipkin の Rack middleware を挿入する
まずは対象アプリケーションで zipkin-tracer gem で定義されている Rack middleware を use
する。
zipkin-tracer gem は RubyGems 経由だと古いのしかないので、bulder の git
オプションで HEAD を使いたいところだけど、zipkin のディレクトリ構成的に git オプションで解決するのは難しかったのでローカルにクローンして path
オプションで指定する。本アプリケーションだと git submodule とかがいいと思う。
あとは configration を Zipkin の Rack Middleware に渡す。configration の詳細は zipkin-tracer gem の lib-zipkin-tracer.rb に書いてある。
主なオプション
- scribe_server: scribe server のアドレス。"127.0.0.1:1463" のような String を渡す。今回は fluentd へ scribe protocol で送信するので fluentd へ到達するように設定する。
- service_name: zipkin 上での service name
- sample_rate: 0 < x < 1 の範囲で設定する。
Sinatra だとこんな感じ。
require 'base64' # finagle-tracer needs this require 'sinatra' require 'zipkin-tracer' config = { service_name: 'test-app', service_port: 3000, sample_rate: 1, } use ZipkinTracer::RackHandler, config get '/' do 'hello' end
Rails の場合 config/application.rb とかで application config に設定しておいて RackMIddleware を引数なしで use
すると勝手に config.zipkin_tracer
読んでくれるのでそのキーに configuration 入れておくとよさそう。
Zipkin collector, query, web の起動
このへんはドキュメント通りに
./bin/collector ./bin/query ./bin/web
試してみる
fluentd を起動しておいて、さきほどの Sinatra アプリケーションを起動してリクエストすると zipkin-collector に届いてる。 Mac OS で thrift gem をビルドする時は configure option 渡さないと build failed する。ref
bundle config build.thrift --with-cppflags='-D_FORTIFY_SOURCE=0' bundle install bundle exec ruby app.rb -p3000 ❯ curl http://localhost:3000 hello
やったー! 動いたよー!
Microservices Architecture と非同期 API
Microservice Architecture を採用すると解決が難しい問題である、部分的障害に対する仕組みとして非同期 API にしてしまう、という方法を思いついた。
まず、今までの同期 API を使っていると想定してあるケースを検討してみる。
フロントとなるあるサービス A がバックエンドサービスである B, C に依存しているケースを考えてみよう。A はユーザーの登録やユーザーの属性の管理を B に委譲していて、A はユーザーの投稿したレシピの管理を C に依存している。別の言い方をすると、B は汎用的なユーザー管理サービスであり、C は汎用的なレシピ管理サービスであり、A はそれぞれを利用している。
このようなケースでサービス A がユーザー(クライアント)から名前の変更をリクエストされたとする。
正常な場合では、A は B のユーザー属性変更 API を call し、無事 B からレスポンスが返ってくると A はユーザーへ処理の完了をレスポンスできる。
異常な場合、例えば B がなんらか原因でサービスダウンしている場合では、A は B のユーザー属性変更 API を call しても処理が完了できない。A はユーザー(クライアント)に自身のサービスがダウンしているようなレスポンスを返さなければならない。
同期 API を利用している場合、B のサービスダウンが直接 A のサービスダウンへ繋がってしまう。これは大きな問題でフロントとなるサービスが依存しているバックエンドサービスが多くなれば多くなるほど、フロントとなるサービスの可用性は著しく下がってしまう。
さて、ここで非同期 API を部分的に採用して先ほどのケースを再検討しよう。部分的に、つまり B のユーザー属性変更 API のみに対して非同期 API を採用する、ということである。
正常な場合では同期 API と変わらず A は B のユーザー属性変更 API を call し、B から正常なレスポンスを得る。この時違う点としては A が B からレスポンスをもらった際には A が変更したい B のリソースはまだ変更されていない可能性がある、ということである。A がユーザー(クライアント)にレスポンスする際もユーザーが行いたかった変更は反映されてない可能性がある。しかし、部分的障害が起こっていない場合は、可及的速やかに B にリクエストされたリソースの変更は処理されるので問題にはならない。
異常な場合、B がサービスダウンしている状況を検討してみる。A は B のユーザー属性変更 API を call しても B からは正常なレスポンスは返却されない。この時 A はユーザー(クライアント)が期待している結果が、リソースの変更ではなくリソースが変更される約束、なので A は自身がリソースの変更をバッファリングしB のサービスが復帰した際に B へリクエストすることを保証することで、ユーザー(クライアント)に正常なレスポンスを返却することができる。B のサービスダウンが一定の期間に収まるならば、A がユーザーへレスポンスしたリソースが変更される約束は無事実行される。
このように、サービス間の結合部分に対して非同期 API を採用することで、部分的障害の影響を狭められるかもしれない。
もちろん同期しなければならない処理もあるので (先の例で言うと B へのユーザー登録などは同期処理しなければならない)、全てに適用できるものではない。さらに、同期 API に比べてシステム全体が複雑度を増してしまうことは間違いない (先の例での A のバッファリング部分)。
以上の考察・アイディアはサービスデザインパターンの2章を読んで思いついたジャストアイディアであり、この本を読み進めていく内に消化されるのかもしれない。
追記
公開したらさっそく指摘があった、このアイディアを採用すると、B が復旧した時に B に依存しているサービス群がリクエストする順番を正しくするのがものすごく難しいので現実は厳しい。
HTTP header value の構成文字セットとは
Authorization header value に filter 処理を当てる時にどんな入力が来るのか知りたかったので調べた。
rfc2616 の sec2.2 に仕様があって
field value = token token = 1*<any CHAR except CTLs or separators> CHAR = <any US-ASCII character (octets 0 - 127)> CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)> separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <"> | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT SP = <US-ASCII SP, space (32)> HT = <US-ASCII HT, horizontal-tab (9)>
こんな感じ。予想してたよりも自由度高かった。
Bot に UNIX 思想持ち込んでみた
UNIX コマンドみたいに、ひとつのことをやる小さなコマンドを組み合わせて、なにかできたらいいな、ということで、echo とか作ってみた。
とりあえず作ったのは echo と delay の2つで、まあ1回つかいかた見たらわかるくらいシンプル。
echo のほうはただ繰り返すだけ。
> ruboty echo hubot image me deal with it hubot image me deal with it
社のチャットには Bot が 2 いるので、Bot が Bot を使役できる。そんなことはどうでも良くて、delay の使い方は秒数を与えられたらその秒数後コマンド実行するというもの。
> ruboty delay 3 ruboty ping # ...3 seconds after pong
で、これを組み合わせるとタイマーっぽくなる。
> ruboty delay 1200 ruboty echo Finish 20 min period! # ...20 minutes after Finish 20 min period!
チャットサービスの 1vs1 チャットはだいたいの人が通知オンにしているから、けっこう良さのあるタイマーになりそう。
ほんとは、パイプとかあるといいね、とか話していたので俺達の戦いはこれからだ 〜Fin〜
本日のおさかな
クリックでスタート!
@taiki45 http://t.co/ovtHeUKNjO
— みょん (@myuon_myon) June 22, 2014
元 gif upload してもらったので、みょんさんの。 の一覧の下の方に original content がありますでし…!
Ruboty 動かした
hubot like bot frame work in Ruby こと、Ruboty を動かしてみた。
ruboty に eve のアイコンを仮で設定したら思いの外グッときたでし…! pic.twitter.com/zSkcgXW8i1
— (deffish *tai*) (@taiki45) 2014, 6月 22
Slack で動かす場合は注意点が2点だけあって、
- これみながらやってる場合は
SLCAK_USERNAME -> SLACK_USERNAME
という typo がある - Slack の Admin setting で XMPP gateway を有効にする
- 使うパスワードは XMPP geteway 用のパスワードを使う
3点だった…
Twitter で動かす場合は難しくはないんだけど、Ruboty の思想が Hipchat とか Slack みたいなチームチャット向けだからあんまり合わない感じがした。Message#reply で "@mention_name" 付きじゃなかったりしてもんにょりする。Twitter adapter でデフォルトで mention name 付けるかどうか微妙だし、むずかしい。あと、ping デバッグがしにくくてつらい。nullpo デバッグを導入するはめになった。★
あとローカルでは shell で動かしたいし Bundler の group 良いのでは???って思ったら accept された良かった。★
Ruboty 全体で言えば、終日ご自愛しながらでも作れるし良さがあった。★ ★
今日の進捗 pic.twitter.com/JOYV7zDIln
— (deffish *tai*) (@taiki45) 2014, 6月 22
Ruby 向け非同期マルチストリーミング処理ライブラリ作った
Twitter bot に HTTP インターフェイスとか Timer 処理とかなんかいろいろそういうの付けたくて、そしたら既存の bot フレームワークの考え方だとつらそう、ということで Queue を使ったメッセージパッシング方式ならいいかんじに扱えそうじゃないかと思って書いてみた。
作ってる途中に、これ Haskell の Conduit っぽいのかなと思って、ちょっと Conduit のインターフェイスを調べてみたけど、やはり Ruby でやるにはいろいろ無理があったし良くなかったのでコンポーネントの名前だけ拝借した。
実際どういう設計なのか、というのは About にそれなりにわかりやすく書いたつもりなので見てもらえるとわかりやすい気がする。
それぞれのコンポーネントに流す、加工する、消費する、という責務で切り分けて、かつその責務の中でも Twitter ストリームを取得するとか Timer でイベント発火するとかさらに責務をわけたコンポーネントを作って、最後にそれを合成して1つのコンポーネントにする、というアイディアでやった。この最後に合成するというアイディアはとても良いのだけれど、Ruby でやろうとしたらどうにもうまくできなくて力不足を感じている。今の実装はダメなスメルがすごい。
datapipes という名前にしたのだけど、これには理由があって、conduit って名前もちょっと考えたけどそんなに参考にしてないし実装もだいぶ違うから却下して、次に pipe とかって名前にしたかったけどそのへんは軒並みすでに取られていた悲しい。datapipe も取られていた悲しいので、datapipes という名前にした。
作ってる Bot はこれ でまだ複数のストリームを読むとかはしてないんだけどいい感じに処理の抽象ができてるから、datapipes の設計としては良さそう。
ただ Concurrency と付き合うのはなにぶん初めて、実装は拙いというのを感じている。