Jaegerでk8s+Istio上のアプリ(Quarkus、Spring)を分散トレーシングする

はじめに

分散トレーシングをやる際にJaegerというツールがあって、試してみたいと思って試せていなかったのやってみようと思います。今回はMinikubeで作ったクラスターにIstioをデプロイして、 その環境でのトレーシングを行ってみようと思います。

Istioにおけるトレーシングについて

どんなふうに実現されるか

Istioにおける分散トレーシングがどのように実現されているのかは、ドキュメントにいろいろ書かれてました。

まず、IstioはEnvoyベースのトレーシングを行います。その際にアプリケーションはB3 trace headersなどのヘッダーを転送していく必要があるようです。具体的には以下のようなヘッダーを転送して行くみたいです。

  • x-request-id
  • x-b3-traceid
  • x-b3-spanid
  • x-b3-parentspanid
  • x-b3-sampled
  • x-b3-flags
  • b3

さらに、Lightstepを利用する場合はx-ot-span-contextもつける必要があるみたいです。
これらのヘッダーはマニュアルで転送していくことも可能ですが、ZipkinJaegerのクライアントを使うことで自動的に拡散することも可能なようです。
ちなみに、なぜ、Istio自身がこれらのヘッダーをフォワーディングできないのかについてですが、アプリケーションのアウトバウンドリクエストがどのインバウンドリクエストによって発生したものかを特定するすべが、Istio側には存在しないからです。

また、Envoyベースのトレーシングに置いてEnvoyは以下のようなことを行ってくれます。

  • リクエストIDとトレーシングヘッダー(B3 Header等)を生成し送信する
  • リクエストとレスポンスのメタデータからTrace Spanを生成する
  • トレーシングバックエンドにSpanを送信する
  • プロキシ先のアプリケーションにヘッダーを送信する

Jaegerとは

Jaeger JaergerはDapper、OpenZipkinにインスパイヤーされた分散トレーシングシステムです。マイクロサービスに置いて以下のような用途で用いられます。

  • 分散トレーシングのモニタリング
  • 根本原因解析
  • サービスの依存解析
  • パフォーマンス、レイテンシの最適化

また、以下のようなコンポーネントで構成されます。

  • Goで作られたバックエンドコンポーネント
  • React UI
  • ストレージ
    • Cassandra 3.4+
    • Elasticserch 5.x, 6.x, 7.x
    • Kafka
    • メモリ

やってみる

環境

今回はKubernetesクラスターはMinikube(driver=none)を用います。

$ minikube version 
minikube version: v1.16.0
commit: 9f1e482427589ff8451c4723b6ba53bb9742fbb1

$ kubectl version -o yaml
clientVersion:
  buildDate: "2020-10-15T01:52:24Z"
  compiler: gc
  gitCommit: 62876fc6d93e891aa7fbe19771e6a6c03773b0f7
  gitTreeState: clean
  gitVersion: v1.18.10
  goVersion: go1.13.15
  major: "1"
  minor: "18"
  platform: linux/amd64
serverVersion:
  buildDate: "2020-10-15T01:43:56Z"
  compiler: gc
  gitCommit: 62876fc6d93e891aa7fbe19771e6a6c03773b0f7
  gitTreeState: clean
  gitVersion: v1.18.10
  goVersion: go1.13.15
  major: "1"
  minor: "18"

$ istioctl version
client version: 1.8.2
control plane version: 1.8.2
data plane version: 1.8.2 (1 proxies)

$ uname -srvmpio
Linux 5.4.0-64-generic #72-Ubuntu SMP Fri Jan 15 10:27:54 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

$ docker version 
(Client略)

Server: Docker Engine - Community
 Engine:
  Version:          20.10.2
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       8891c58
  Built:            Mon Dec 28 16:15:19 2020
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          1.4.3
  GitCommit:        269548fa27e0089a8b8278fc4fc781d7f65a939b
 runc:
  Version:          1.0.0-rc92
  GitCommit:        ff819c7e9184c13b7c2607fe6c30ae19403a7aff
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0


$ java --version
openjdk 11.0.10 2021-01-19
OpenJDK Runtime Environment 18.9 (build 11.0.10+9)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.10+9, mixed mode)

$ mvn --version
Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 11.0.10, vendor: Oracle Corporation, runtime: /home/someone/.sdkman/candidates/java/11.0.10-open
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-65-generic", arch: "amd64", family: "unix"

もろもろを構築しておく

今回は以下のような環境を構築して分散トレーシングをやってみようと思います。

f:id:yuya_hirooka:20210131213754p:plain

Jaegerでトレーシングするのは以下の3つになります。

利用するアプリはSpringとQuarkusと利用していますが、この記事に置いては深い意味は無く、別で試したことがあったので採用しました。
アプリのロジックに関しても複雑なことは市内想定で、QuarkusアプリがSpringアプリに対して、Hello, Tracingの文字列を取得してそのままフロント側に返すようにしようと思います。

Sidecar InjectionをTrueにしてネームスペースを作成

まずは、IstioのインジェクションをTrueにしておきます。 今回は余計な複雑さをなくすために、新たにネームスペースを作成はせずにクラスタdefaultに対してインジェクションをTrueにします。
以下のコマンドを実行します。

$ kubectl --context=minikube label namespace default istio-injection=enabled
namespace/default labeled

minikubeのコンテクストでイメージをビルドするように設定

今回はローカルでビルドしたしたイメージを使うようにしておきます。
いかのコマンドでminikubeのコンテクストでイメージをビルドするように設定します。

$ eval $(minikube docker-env)

サンプルアプリケーションを作成しコンテナ化

Quarkusのアプリを作成

まずはQuarkusの方を作っていこうと思います。
プロジェクトはQuarkus - Start coding with code.quarkus.ioを使って作成します。
プロジェクトの設定は以下の通り。

f:id:yuya_hirooka:20210131164559p:plain

依存はRESTEasyRest Clientだけ追加しておきます。
プロジェクトが作成できたら、まずはHTTPクライアントを作成します。

@RegisterRestClient
@RegisterClientHeaders
public interface GreetingClient {

    @GET
    @Path("/hello")
    String fetchHello();
}

基本的には、なんの変哲の無いRestClientですがひとつだけポイントがあります。
前述の通り、Istioを使った分散トレーシングではヘッダーを転送していく必要があります。そのフォワーディングを行なうために、@RegisterClientHeadersを利用しています。このヘッダーはデフォルトで、指定されたJAX-RSのインバウンドリクエストヘッダーをアウトバウンドのリクエスト時に付与することができます。

RestClientの設定と転送するヘッダー設定を以下のようにappllication.propertiesに記述しておきます。

quarkus.http.port=8081
dev.hirooka.GreetingClient/mp-rest/url=http://${SPRING_SERVICE:localhost:8082}
dev.hirooka.GreetingClient/mp-rest/scope=javax.inject.Singleton
org.eclipse.microprofile.rest.client.propagateHeaders=x-request-id,x-b3-traceid,x-b3-spanid,x-b3-parentspanid,x-b3-sampled,x-b3-flags,b3,x-ot-span-context

設定まで記述できたら、次はハンドラーを記述します。今回はHTTPクライアントをハンドラーから直接利用するようにします。

@Path("/hello")
public class GreetingResource {

    @Inject
    @RestClient
    GreetingClient greetingClient;

    static final Logger logger = Logger.getLogger(GreetingResource.class);

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello(@Context HttpHeaders headers) {
        logger.info(headers.getRequestHeaders());
        return greetingClient.fetchHello();
    }
}

これでQuarkusの方のアプリは完成しました。
最後に作ったアプリをDockerイメージ化しておきます。

$ ./mvnw package
$ docker build -f src/main/docker/Dockerfile.jvm -t quarkus/open-tracing-jvm .
$ docker images | grep quarkus
quarkus/open-tracing                                           latest                  f2c29c0c8e21   2 minutes ago   385MB

Springのアプリを作成

次にSpringのアプリを作成します。
プロジェクトはSpring Initializrを使って作成します。
設定は以下のように。

f:id:yuya_hirooka:20210131173540p:plain

今回の構成だと、Springのアプリは外部アクセスを行わないため、ヘッダーをフォワーディングする必要が無いので、Spring Webだけで良いのですが、一応フォワーディングのやり方を示すために依存にSleuthZipkin Clientを追加してます。

なにはともあれ、まずはコントローラーを作成します。

@RestController
public class GreetingController {

    private final Logger logger = LoggerFactory.getLogger(GreetingController.class);

    @GetMapping("/hello")
    String greeting(@RequestHeader Map<String, String> header) {
        logger.info(header.toString());
        return "Hello, Tracing";
    }
}

今回必要な部分はこれだけです。
設定は以下の通り

server.port=8082
spring.zipkin.enabled=false

Spring Sleuthはデフォルトでlocalhost:9411にメトリクスを送信してしまうので、今回は無効化して置きます。

Springも外部へのHTTPコールを行いヘッダーを転送したい場合はspring.sleuth.propagation-keysを以下のように設定すれば良さそうです。

spring.sleuth.propagation-keys=x-request-id,x-b3-traceid,x-b3-spanid,x-b3-parentspanid,x-b3-sampled,x-b3-flags,b3,x-ot-span-context

x-b3-traceidなどのB3のヘッダーなどはこの値をセットしていない場合は、Spring Sleuthが自分で生成した値をヘッダーで利用してしまうみたいです。(もしかしたらもっといい方法があるかも...)

まぁ、今回はspring.sleuth.propagation-keysに関しては置いておいて、できたアプリをDockerイメージ化します。

$ mvn spring-boot:build-image -Dspring-boot.build-image.imageName=spring/open-tracing

$ docker images | grep spring/open-tracing
spring/open-tracing                                                latest                  0dbe73ec4359   41 years ago        268MB

イメージが作成されました。

DeploymentとServiceを作成してmimikubeにデプロイする

それではDeploymentとServiceを作成してminikubeにデプロイしておきます。
まずはベースとなるdeployment.yamlを作成します。

$ kubectl create deployment quarkus-app --image=quarkus/open-tracing --dry-run=client -o yaml > deployment.yaml
echo --- >> deployment.yaml
$ kubectl create deployment spring-app --image=spring/open-tracing --dry-run=client -o yaml >> deployment.yaml

できた、deplyment.yamlのそれぞれのDeploymentにimagePullPolicy: IfNotPresent(ローカルイメージを使用するようにするため)とquarkus-appには環境変数SPRING_SERVICEを記述しておきます。

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: quarkus-app
  name: quarkus-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: quarkus-app
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: quarkus-app
    spec:
      containers:
      - image: quarkus/open-tracing
        name: open-tracing
        imagePullPolicy: IfNotPresent
        env:
          - name: SPRING_SERVICE
            value: spring-app:8082
        resources: {}
status: {}
---
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: spring-app
  name: spring-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: spring-app
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: spring-app
    spec:
      containers:
      - image: spring/open-tracing
        name: open-tracing
        imagePullPolicy: IfNotPresent
        resources: {}
status: {}

次にサービスを作成します。

$ kubectl create service clusterip quarkus-app --tcp=8081:8081 --dry-run=client -o yaml > service.yaml
$ echo --- >> service.yaml
$ kubectl create service clusterip spring-app --tcp=8082:8082 --dry-run=client -o yaml > service.yaml

Serviceは特にいじることは無いので、作ったマニフェストをapplyしていきます。

$ kubectl --context=minikube apply -f deplyment.yaml
$ kubectl --context=minikube apply -f service.yaml

GatawayとVirtualSerciceを作成して疎通確認する

GatawayVirtualServiceを作っておきます。

gateway.yaml

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: quarkus-app-gateway
spec:
  selector:
    istio: ingressgateway 
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"

virtual-service.yaml

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: quarkus-app-vs
spec:
  hosts:
  - "*"
  gateways:
  - quarkus-app-gateway
  http:
  - match:
    - uri:
        prefix: /hello
    route:
    - destination:
        port:
          number: 8081
        host: quarkus-app

上記のyamlをapplyしておきます。

$ kubectl --context=minikube apply -f gateway.yaml
$ kubectl --context=minikube apply -f virtual-service.yaml

cUrlを使って疎通確認を行います。

$ export INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].nodePort}'

$ export INGRESS_HOST=$(minikube ip)

$ curl ${INGRESS_HOST}:${INGRESS_PORT}/hello -v
*   Trying 192.168.49.2:30019...
* TCP_NODELAY set
* Connected to 192.168.49.2 (192.168.49.2) port 30019 (#0)
> GET /hello HTTP/1.1
> Host: 192.168.49.2:30019
> User-Agent: curl/7.68.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< content-length: 14
< content-type: text/plain;charset=UTF-8
< x-envoy-upstream-service-time: 23
< date: Sun, 31 Jan 2021 11:28:48 GMT
< server: istio-envoy
< 
* Connection #0 to host 192.168.49.2 left intact
Hello, Tracing

ここまででようやく準備完了です。

Jaergerを動かす

さて、ようやくですが。 Jaegerをローカルで動かしておきます。
今回はMinikubeのクラスターにデプロイして動かします。
外部のJaergerにメトリクスを送信する場合は、--set values.global.tracer.zipkin.address=<jaeger-collector-address>:9411をIstioのインストール時に設定しておけば、任意のJaergerにデータを送信することができます。
それでは、Jargerをデプロイします。

$ kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.8/samples/addons/jaeger.yaml

次のコマンドで、UIを動かします。

$  istioctl dashboard jaeger
http://localhost:16686

出力される。アドレスにアクセスすると以下の様なUIが表示されます。

f:id:yuya_hirooka:20210131210234p:plain

実はローカルで試している際に何度かリクエストを送ってしまったので、すでにデータが存在してしまっていますが、最初はなにも出力されません。
これは、デフォルトではIstioは1%のリクエストのデータをJaergerに送るためです。
以下のコマンドを何度か叩いて、Jaergerにデータを送るようにしておきます。

$ for i in $(seq 1 100); do curl -s -o /dev/null "http://${INGRESS_HOST}:${INGRESS_PORT}/hello"; done

そうするとそれっぽいデータが見れるようになります。

f:id:yuya_hirooka:20210131210628p:plain

ちょっとUIを見てみる

ヘッダーのSearchタブを選択すると以下のような検索用のボックスが表示されていると思います。

f:id:yuya_hirooka:20210131211740p:plain

Serviceistio-ingressgatewayを選択してFind Tracesを押すと右側にトレースされたリクエストが表示されます。

f:id:yuya_hirooka:20210131212213p:plain

僕の環境では4回のサンプリングされたリクエストの情報が表示されます。
グラフのすぐ下のリクエスト(7ff3cb9)をクリックするとリクエストに関する情報がより詳細にみることができます。

f:id:yuya_hirooka:20210131212649p:plain

ヘッダーのSystem Architectureタブを選択するとサービスの依存関係やそれぞれに難解リクエストが送られたかを確認することができます。

f:id:yuya_hirooka:20210131213032p:plain

f:id:yuya_hirooka:20210131213114p:plain

とりあえず動かすところまでできました。