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
もつける必要があるみたいです。
これらのヘッダーはマニュアルで転送していくことも可能ですが、ZipkinやJaegerのクライアントを使うことで自動的に拡散することも可能なようです。
ちなみに、なぜ、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"
もろもろを構築しておく
今回は以下のような環境を構築して分散トレーシングをやってみようと思います。
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を使って作成します。
プロジェクトの設定は以下の通り。
依存はRESTEasy
とRest 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を使って作成します。
設定は以下のように。
今回の構成だと、Springのアプリは外部アクセスを行わないため、ヘッダーをフォワーディングする必要が無いので、Spring Web
だけで良いのですが、一応フォワーディングのやり方を示すために依存にSleuth
とZipkin 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を作成して疎通確認する
Gataway
とVirtualService
を作っておきます。
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が表示されます。
実はローカルで試している際に何度かリクエストを送ってしまったので、すでにデータが存在してしまっていますが、最初はなにも出力されません。
これは、デフォルトではIstioは1%のリクエストのデータをJaergerに送るためです。
以下のコマンドを何度か叩いて、Jaergerにデータを送るようにしておきます。
$ for i in $(seq 1 100); do curl -s -o /dev/null "http://${INGRESS_HOST}:${INGRESS_PORT}/hello"; done
そうするとそれっぽいデータが見れるようになります。
ちょっとUIを見てみる
ヘッダーのSearch
タブを選択すると以下のような検索用のボックスが表示されていると思います。
Service
をistio-ingressgateway
を選択してFind Traces
を押すと右側にトレースされたリクエストが表示されます。
僕の環境では4回のサンプリングされたリクエストの情報が表示されます。
グラフのすぐ下のリクエスト(7ff3cb9
)をクリックするとリクエストに関する情報がより詳細にみることができます。
ヘッダーのSystem Architecture
タブを選択するとサービスの依存関係やそれぞれに難解リクエストが送られたかを確認することができます。
とりあえず動かすところまでできました。