Zipkin で分散トレーシング
こんにちは、けんにぃです。ナビタイムジャパンでサーバーサイドの開発やマネージメントを担当しています。
今回はプログラムのトレーシングを行うための Zipkin がとても便利だったのでご紹介しようと思います。
昨年度、『バスNAVITIME』のバスロケーション機能の開発環境を改善する目的で Zipkin を導入しました。プログラム内で作成されている SQL や URL などをトレースすることで不具合の調査にかなり役立っています。
バスロケーション機能の改善については他にも記事がありますので、良かったらご覧ください。
そもそもトレーシングって何?
トレーシングというのはサーバーへリクエストを投げた際にプログラム内で呼び出された関数や DB のクエリなどを記録して、不具合の調査やパフォーマンスの分析などに利用するための仕組みのことです。
分散トレーシングとは
「分散」というのは、リクエストの処理が複数のマイクロサービスで処理されるときに、そのマイクロサービス全体にまたがってトレーシングを行う仕組みのことです。
Zipkin
Zipkin は分散トレーシングシステムの 1 つで、Google が開発した Dapper という分散トレーシングにヒントを得て作られました。
Zipkin は次の 2 つから構成されます。
トレーシングを行うためのトレーサー
トレース結果を確認するための Zipkin サーバー
Zipkin サーバーは Java で実装されています。
トレーサーは下記の通り多くの言語に対応しています。
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 が表示されます。
トレースした結果はこの Web UI 上で確認することができます。
本運用をするときはトレースデータを DB に入れる必要があるのですが、Zipkin サーバーはテスト用にメモリ上にもトレースデータを保存できるので、今回は DB を用意せずに使用してみます。
Spring のインストール
Spring Initializr で下記の通り Spring プロジェクトを作成します。
プロジェクトを 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);
}
}
}
トレース結果は下記のようになります。
まとめ
Spring で開発されている方はすぐに導入できるので、とりあえず入れてみるだけでもパフォーマンスの計測などが楽になると思います。
Spring を使用されていない方でも Brave を直接インストールすることでトレースできるようになるのでぜひお試しください。