見出し画像

Zipkin で分散トレーシング

こんにちは、けんにぃです。ナビタイムジャパンでサーバーサイドの開発やマネージメントを担当しています。

今回はプログラムのトレーシングを行うための Zipkin がとても便利だったのでご紹介しようと思います。

昨年度、『バスNAVITIME』のバスロケーション機能の開発環境を改善する目的で Zipkin を導入しました。プログラム内で作成されている SQL や URL などをトレースすることで不具合の調査にかなり役立っています。

バスロケーション機能の改善については他にも記事がありますので、良かったらご覧ください。

そもそもトレーシングって何?

トレーシングというのはサーバーへリクエストを投げた際にプログラム内で呼び出された関数や DB のクエリなどを記録して、不具合の調査やパフォーマンスの分析などに利用するための仕組みのことです。

分散トレーシングとは

「分散」というのは、リクエストの処理が複数のマイクロサービスで処理されるときに、そのマイクロサービス全体にまたがってトレーシングを行う仕組みのことです。

Zipkin

Zipkin は分散トレーシングシステムの 1 つで、Google が開発した Dapper という分散トレーシングにヒントを得て作られました。

Zipkin は次の 2 つから構成されます。

  • トレーシングを行うためのトレーサー

  • トレース結果を確認するための Zipkin サーバー

Zipkin の構成図

Zipkin サーバーは Java で実装されています。
トレーサーは下記の通り多くの言語に対応しています。

公式サポート
C# / Go / Java / JavaScript / Ruby / Scala / PHP

コミュニティサポート
Python / Clojure / Elixir / Lua

Tracers and Instrumentation - Zipkin

Java 製の Zipkin トレーサーは Brave という名前で提供されています。

Java でマイクロサービスを実装するときは Spring を使うことが多いと思いますが、Spring プロジェクトの一部である Spring Cloud Sleuth にこの Brave を使ってトレーシングするための機能が同梱されています。

そこで今回は Spring で実装されたサーバーに Spring Cloud Sleuth をインストールして、Zipkin サーバーにトレース結果を送るところまでの手順を説明しようと思います。

Zipkin サーバーのインストール

Java 8 以上がインストールされた状態で下記コマンドを実行します。

$ curl -sSL https://zipkin.io/quickstart.sh | bash -s
$ java -jar zipkin.jar

                  oo
                 oooo
                oooooo
               oooooooo
              oooooooooo
             oooooooooooo
           ooooooo  ooooooo
          oooooo     ooooooo
         oooooo       ooooooo
        oooooo   o  o   oooooo
       oooooo   oo  oo   oooooo
     ooooooo  oooo  oooo  ooooooo
    oooooo   ooooo  ooooo  ooooooo
   oooooo   oooooo  oooooo  ooooooo
  oooooooo      oo  oo      oooooooo
  ooooooooooooo oo  oo ooooooooooooo
      oooooooooooo  oooooooooooo
          oooooooo  oooooooo
              oooo  oooo

     ________ ____  _  _____ _   _
    |__  /_ _|  _ \| |/ /_ _| \ | |
      / / | || |_) | ' / | ||  \| |
     / /_ | ||  __/| . \ | || |\  |
    |____|___|_|   |_|\_\___|_| \_|

:: version 2.23.16 :: commit b90f2b3 ::

2022-05-16 20:42:20.885  INFO [/] 64099 --- [oss-http-*:9411] c.l.a.s.Server                           : Serving HTTP at /0:0:0:0:0:0:0:0:9411 - http://127.0.0.1:9411/

サーバーを起動して http://localhost:9411 にアクセスすると Web UI が表示されます。

http://localhost:9411

トレースした結果はこの Web UI 上で確認することができます。

本運用をするときはトレースデータを DB に入れる必要があるのですが、Zipkin サーバーはテスト用にメモリ上にもトレースデータを保存できるので、今回は DB を用意せずに使用してみます。

Spring のインストール

Spring Initializr で下記の通り Spring プロジェクトを作成します。

Spring Initializr

プロジェクトを IDE で開いた後、下記のような HelloController.java を作成します。

package com.example.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class HelloController {

    private static final Logger log = LoggerFactory.getLogger(HelloController.class);

    @GetMapping("/")
    public Map<String, String> hello() {
        log.info("Hello, Zipkin!");

        HashMap<String, String> map = new HashMap<>();

        map.put("message", "Hello, Zipkin!");

        return map;
    }

}

application.properties も下記のように作成しておきます。

spring.application.name=demo

これをビルドしてサーバーを起動すると http://localhost:8080 にアクセスできるようになっていると思います。

$ curl -si http://localhost:8080
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 17 May 2022 01:51:23 GMT

{"message":"Hello, Zipkin!"}

リクエストを投げた際、Spring のログに下記のような行が現れていると思います。

2022-05-17 11:11:57.291  INFO [demo,1377e768b13a6b72,1377e768b13a6b72] 69779 --- [nio-8080-exec-1] com.example.demo.Hello                   : Hello, Zipkin!

この [demo,1377e768b13a6b72,1377e768b13a6b72] の部分はそれぞれ

  • アプリケーション名(spring.application.name の値)

  • トレース ID

  • スパン ID

になっています。
トレース ID というのはトレースを開始した際に振られる一意の値で、トレースを行うマイクロサービス全体にまたがって一意に付与されます。
スパン ID というのは特定のコンテキスト(例えば SQL の発行処理や特定の関数呼び出し)に対して自由に振れる一意の ID です。

Brave を使う

次に Brave を使って Zipkin サーバーにトレースデータを送ってみようと思います。先程の HelloController.java の hello() メソッドに @NewSpan アノテーションを付けます。

@GetMapping("/")
@NewSpan  // これを付与
public Map<String, String> hello() {
    ...
}

アノテーションを付けたら再度サーバーを起動してリクエストを投げると Zipkin サーバーにトレース結果が送信されます。

Zipkin サーバー上で RUN QUERY をクリックするとトレースしたデータ一覧が表示されます。

トレース一覧

SHOW をクリックすると詳細が表示されます。

トレース結果の詳細

トレース結果の各行はスパンと言って @NewSpan アノテーションを定義するたびに新しいスパンが増えていきます。右側にある Tags は各スパンの内容を説明するためのタグ情報です。


もう少し詳しい実装をしてみます。
@NewSpan を使ってスパンを作成する代わりに、メソッド内でスパンを作成することもできます。

package com.example.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class HelloController {

    private static final Logger log = LoggerFactory.getLogger(HelloController.class);

    @Autowired
    private Tracer tracer;

    @GetMapping("/")
    public Map<String, String> hello() {
        log.info("Hello, Zipkin!");

        Span newSpan = this.tracer.nextSpan().name("hello");

        try (Tracer.SpanInScope ws = this.tracer.withSpan(newSpan.start())) {
            newSpan.tag("name", "demo");

            HashMap<String, String> map = new HashMap<>();
            map.put("message", "Hello, Zipkin!");

            return map;
        } finally {
            newSpan.end();
        }
    }

}

作成したスパンに対してタグを追加したので Zipkin サーバーでもタグが確認できると思います。

OkHttp のトレース

Java でよく利用される HTTP クライアントの OkHttp と Brave を連携させて HTTP のトレースをすることもできます。

OkHttp にはリクエスト・レスポンスの送受信前後で独自の処理を追加させるための Interceptor インターフェースが用意されているので、この仕組みを使ってトレース機能を実装してあげます。

package com.example.demo;

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class OkHttpTracingInterceptor implements Interceptor {

    @Autowired
    private Tracer tracer;

    @NotNull
    @Override
    public Response intercept(@NotNull Chain chain) throws IOException {
        Request request = chain.request();
        Span newSpan = this.tracer.nextSpan().name("http");

        try (Tracer.SpanInScope ws = this.tracer.withSpan(newSpan.start())) {
            newSpan.tag("url", request.url().toString());

            return chain.proceed(request);
        } finally {
            newSpan.end();
        }
    }

}

リクエストの URL をトレースするためスパンタグに URL を追加しました。HelloController 側でこの Interceptor を登録し OkHttpClient を作成します。

@RestController
public class HelloController {
    // ...

    private final OkHttpClient client;

    public HelloController(OkHttpTracingInterceptor interceptor) {
        client = new OkHttpClient.Builder().addNetworkInterceptor(interceptor).build();
    }

    // ...
}

あとは通常通りの使い方でリクエストを投げると OkHttp がトレースされます。

Request request = new Request.Builder().url(url).build();

try (Response response = this.client.newCall(request).execute()) {
    String body = response.body().string();
    // ...
}

最終的に HelloController のコード全体は下記のようになります。
(JSON をパースするため Jackson を使用しています。)

package com.example.demo;

import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.sleuth.annotation.NewSpan;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.util.Map;

@RestController
public class HelloController {

    private static final Logger log = LoggerFactory.getLogger(HelloController.class);

    private final ObjectMapper objectMapper = new ObjectMapper();

    private final OkHttpClient client;

    public HelloController(OkHttpTracingInterceptor interceptor) {
        client = new OkHttpClient.Builder().addNetworkInterceptor(interceptor).build();
    }

    @GetMapping("/")
    @NewSpan
    public Map<String, Object> hello() throws IOException {
        log.info("Hello, Zipkin!");

        return this.send("https://httpbin.org/json");
    }

    private Map<String, Object> send(String url) throws IOException {
        Request request = new Request.Builder().url(url).build();

        try (Response response = this.client.newCall(request).execute()) {
            String body = response.body().string();
            return objectMapper.readValue(body, Map.class);
        }
    }

}

トレース結果は下記のようになります。

OkHttp のトレース結果

まとめ

Spring で開発されている方はすぐに導入できるので、とりあえず入れてみるだけでもパフォーマンスの計測などが楽になると思います。
Spring を使用されていない方でも Brave を直接インストールすることでトレースできるようになるのでぜひお試しください。

参考文献