読者です 読者をやめる 読者になる 読者になる

株式会社ネクスト エンジニアBlog

不動産・住宅情報サイト HOME'Sを運営する株式会社ネクストのエンジニアが提供する技術ブログです。エンジニアに役立つ情報の発信や、弊社エンジニアの活動を中心にお届けします。

Zipkinを導入してみた(Ruby編)

こんにちは。技術基盤部の磯野です。 ちょっと間が空いてしまいましたが引き続きZipkinです。 今回は Ruby + Sinatra で動いている Webアプリケーションへのトレーサーの導入です。

前々回の記事 → Zipkinを導入してみた(サーバー編)
前回の記事 → Zipkinを導入してみた(PHP編)

構成

Rubyのライブラリは公式の zipkin-tracerを導入します。

構成はこのような感じです。 f:id:nextdeveloper:20160906172420p:plain 画像のアプリケーション・フレームワーク → Zipkin Sinatra

CompositeAPIでの処理の流れ

  1. トレースデータの初期化(trace_id, span_idはWebサーバーからのリクエストヘッダに設定されている)
  2. マルチスレッドで非同期にRestAPIを複数呼び出し、その際にトレースデータを記録し、追跡するための情報をリクエストヘッダに付与する
  3. トレースデータを集計しjson形式でzipkinサーバーにhttpで送信する

RestAPIでの処理の流れ

  1. トレースデータの初期化(trace_id, span_idはCompositeAPIからのリクエストヘッダに設定されている)
  2. 各データベースや全文検索エンジンなどにリクエスト送信、必要に応じてトレースデータを記録する
  3. トレースデータを集計し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)

結果

以下の赤で囲った部分が今回の作業により記録されるようになります。 f:id:nextdeveloper:20160906150543p:plain

終わりに

zipkin導入に関しては一旦以上で終わりになります。

今回、WebSite, CompositeAPI, RestAPIの3層構造のアプリケーションにzipkinを導入しましたが、普段見れない各サービスの積み重ねの部分が視覚化できた事はとても有意義でした。

軽く見ただけでも、以下のような問題に気付く事が出来ました。

  • 各APIやミドルウェアの呼び出しでどこがボトルネックになっているか(視覚的に認識できる)
  • CompositeAPI → RestAPIの部分で並列にリクエストを投げられるところで直列で行っている
  • CompositeAPI → RestAPIの呼び出しを無駄に行っていた(キャッシュでごまかしていた為初回のみ遅い)

まだ各サービスの改修には至っていませんが、これらを一助としてサイトの高速化やマイクロサービス化を推進していけそうです。