こんにちは。技術基盤部の磯野です。 ちょっと間が空いてしまいましたが引き続きZipkinです。 今回は Ruby + Sinatra で動いている Webアプリケーションへのトレーサーの導入です。
前々回の記事 → Zipkinを導入してみた(サーバー編)
前回の記事 → Zipkinを導入してみた(PHP編)
- 構成
- CompositeAPIでの処理の流れ
- RestAPIでの処理の流れ
- 処理の流れを追跡するためのリクエストヘッダ
- app.rbの設定 (CompositeAPI, RestAPI共通)
- monkey_patch/zipkin_tracer の実装
- HTTP::Requestクラス用のzipkinトレース処理用メソッド
- 結果
- 終わりに
構成
Rubyのライブラリは公式の zipkin-tracerを導入します。
構成はこのような感じです。 画像のアプリケーション・フレームワーク → Zipkin Sinatra
CompositeAPIでの処理の流れ
- トレースデータの初期化(trace_id, span_idはWebサーバーからのリクエストヘッダに設定されている)
- マルチスレッドで非同期にRestAPIを複数呼び出し、その際にトレースデータを記録し、追跡するための情報をリクエストヘッダに付与する
- トレースデータを集計しjson形式でzipkinサーバーにhttpで送信する
RestAPIでの処理の流れ
- トレースデータの初期化(trace_id, span_idはCompositeAPIからのリクエストヘッダに設定されている)
- 各データベースや全文検索エンジンなどにリクエスト送信、必要に応じてトレースデータを記録する
- トレースデータを集計しjson形式でzipkinサーバーにhttpで送信する
処理の流れを追跡するためのリクエストヘッダ
前回の記事で説明しているので詳細は省きますが、以下はそのHTTPヘッダとその説明です。
HTTP Header | Type | 説明 |
---|---|---|
X-B3-TraceId | 64 encoded bits *1 | リクエストごとに共通のID、これで追跡情報を紐付ける |
X-B3-SpanId | 64 encoded bits *1 | 計測ごとに一意に決まるID |
X-B3-ParentSpanId | 64 encoded bits *1 | 直前の計測のSpanId |
X-B3-Sampled | Boolean (either “1” or “0”) *2 | サンプリング対象かどうか |
X-B3-Flags | a Long | - |
*1 内部データは数値ですが、ヘッダに付与する際は16進数表現した文字列に変換します。
詳しくは以下URLの「HTTP Tracing」を参照してください。 http://zipkin.io/pages/instrumenting.html
app.rbの設定 (CompositeAPI, RestAPI共通)
以下のように ZipkinTracer::RackHandler を useするだけですが、マルチスレッドでは正常に動作しなかったので、パッチを当てています。
class Application < Sinatra::Base configure do require 'zipkin_tracer' require 'monkey_patch/zipkin_tracer' use ZipkinTracer::RackHandler, { server_name: 'composite_api', service_port: 80, sample_rate: 0.0, json_api_host: http://zipkin.example.com } end end
monkey_patch/zipkin_tracer の実装
モンキーパッチはこんな感じです。
- アプリケーション自身のTraceを親としたトレース情報を作れるように設定
- スレッドで動くようにMutexでの同期処理を追加
- parent_idがnilの場合zipkinが例外を吐くのでnilの場合リクエストに含めない(v0.18.2で修正済み)
Trace.module_eval do class << self remove_method :with_trace_id def root_id @root || self.id end def with_trace_id(trace_id) first = false @root ||= begin first = true trace_id end self.push(trace_id) yield ensure self.pop @root = nil if first end end remove_method :stack, :push, :pop def mutex @mutex ||= Mutex.new end def stack mutex.synchronize do @stack ||= [] end end def push(trace) mutex.synchronize do @stack ||= [] @stack.push(trace) end end def pop mutex.synchronize do @stack ||= [] @stack.pop end end end Trace::Span.class_eval do alias_method :to_h_original, :to_h remove_method :to_h def to_h data = to_h_original data.delete(:parentId) if data[:parentId].nil? data end end Trace::ZipkinTracerBase.class_eval do remove_method :spans, :store_span, :reset def mutex @mutex ||= Mutex.new end def spans mutex.synchronize do @spans ||= [] end end def store_span(id, span) mutex.synchronize do @spans ||= [] @spans.push(span) end end def reset mutex.synchronize do @spans = [] end end end
HTTP::Requestクラス用のzipkinトレース処理用メソッド
zipkin-tracerにはfaraday-middlewareが付属しているので、faradayを利用している場合にはほとんどそのまま利用できます。
今回はHTTPクライアントとしてNet::HTTPを直接使っている為、リクエストクラスにヘッダを追加する処理とZipkinのトレースデータの登録処理をまとめて行うメソッドを定義しています。
module CompositeAPI class ZipkinTracer class << self def trace_with_http_request(req) trace_id = Trace.root_id.next_id b3_headers.each do |method, header| req[header] = trace_id.send(method).to_s end return yield unless trace_id.sampled? res = nil Trace.with_trace_id(trace_id) do local_endpoint = Trace.default_endpoint remote_endpoint = Trace::Endpoint.make_endpoint('0.0.0.0', 80, 'restapi', local_endpoint.ip_format) Trace.tracer.with_new_span(trace_id, req.method.to_s.downcase) do |span| uri = URI.parse(req.path) span.record_tag(Trace::BinaryAnnotation::URI, uri.path, Trace::BinaryAnnotation::Type::STRING, local_endpoint) span.record_tag('http.query', uri.query.to_s, Trace::BinaryAnnotation::Type::STRING, local_endpoint) span.record_tag(Trace::BinaryAnnotation::SERVER_ADDRESS, '1', Trace::BinaryAnnotation::Type::BOOL, remote_endpoint) span.record(Trace::Annotation::CLIENT_SEND, local_endpoint) res = yield span.record_tag(Trace::BinaryAnnotation::STATUS, res.code.to_s, Trace::BinaryAnnotation::Type::STRING, local_endpoint) span.record(Trace::Annotation::CLIENT_RECV, local_endpoint) end end res end def b3_headers { trace_id: 'X-B3-TraceId', parent_id: 'X-B3-ParentSpanId', span_id: 'X-B3-SpanId', sampled: 'X-B3-Sampled', flags: 'X-B3-Flags' } end end end end
使い方
req = Net::HTTP::Get.new('http://restapi.example.com/path/to/resource?key=1') CompositeAPI::ZipkinTracer.trace_with_http_request(req)
結果
以下の赤で囲った部分が今回の作業により記録されるようになります。
終わりに
zipkin導入に関しては一旦以上で終わりになります。
今回、WebSite, CompositeAPI, RestAPIの3層構造のアプリケーションにzipkinを導入しましたが、普段見れない各サービスの積み重ねの部分が視覚化できた事はとても有意義でした。
軽く見ただけでも、以下のような問題に気付く事が出来ました。
- 各APIやミドルウェアの呼び出しでどこがボトルネックになっているか(視覚的に認識できる)
- CompositeAPI → RestAPIの部分で並列にリクエストを投げられるところで直列で行っている
- CompositeAPI → RestAPIの呼び出しを無駄に行っていた(キャッシュでごまかしていた為初回のみ遅い)
まだ各サービスの改修には至っていませんが、これらを一助としてサイトの高速化やマイクロサービス化を推進していけそうです。