Quarkus で REST API を作って Jaeger で見てみたい
先日の記事で Apollo Server に Opentracing 対応を入れて Jaeger でトレースログをチェックしてみましたが、Quarkus は MicroProfile 対応なので Opentracing 対応も非常に簡単なんですよね!?
…ということなので、本日は Quarkus での REST API 作成とネイティブ化、Jaegerでのトレースログ表示にチャレンジしてみたいと思います。
参考ページなど
Quarkus 本家のマニュアルサイトは相当なボリュームがありますので、今回の手順で参考にしたページのみをご紹介いたします。
以下のページをベースに REST APIの作成を進めます。
永続化については以下のページを参考に、Hibernate-Panache を使ってみます。
Hibernate-Panache で REST API を構築するサンプルは以下の公式リポジトリにありました。
続いて Jaeger と連携する Opentracing 対応については以下のページを参考にします。
Swagger の対応については以下のページです。
最後に Native ビルドに関しては以下のページを参考にします。
対象環境
OS: macOS Mojave 10.14.6
Dokcker: Docker Desktop 2.1.0.4
コンテナの中から親Hostをdocker.for.mac.host.internal
参照するために、Docker Desktop で手抜きをしてしまいました。すいません。。。
それと Windows や Mac で DockerDesktop をお使いの方は メモリ 6G 以上 を割り当ててください。
ネイティブ化のビルドには想像以上のメモリを使います。
またローカルに JDK 8以降、Maven のご用意をお願いします。
それでは参ります!
1. Quarkus プロジェクトの作成
まず、以下のmavenコマンドで新規プロジェクトの作成を行います。
$ mvn io.quarkus:quarkus-maven-plugin:0.27.0:create
...
$ cd my-quarkus-project
で、いくつか質問されますが、REST APIとHelloResourceを生成するオプションを選択してください。あとは適当に答えておけばOKです。
続いて、
$ mvn quarkus:add-extension -Dextensions="quarkus-jdbc-postgresql,quarkus-smallrye-metrics,quarkus-smallrye-openapi,quarkus-smallrye-opentracing,quarkus-hibernate-orm-panache,quarkus-resteasy-jsonb"
でプラグインを追加します。今回の内容的にモリモリです。
2. Hibernate-Panache を使った サービスと API の作成
さて、実際のコーディングに入ってまいりましょう!
2-1. Hibernate の準備
プラグインを追加して早速ですが… pom.xmlの <dependecies>
に以下の依存モジュールを追加します。
...
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>io.opentracing.contrib</groupId>
<artifactId>opentracing-jdbc</artifactId>
</dependency>
...
こういうところが VSCode だけじゃしんどくて、Eclipse などの IDE のサポートが欲しくなるところなんだよね。Java の良くないところだと思いますけど。。。
続いて hibernate の設定を以下のように追加します
quarkus.datasource.url = jdbc:tracing:postgresql://docker.for.mac.host.internal:5432/mydatabase
quarkus.datasource.driver=io.opentracing.contrib.jdbc.TracingDriver
quarkus.hibernate-orm.dialect=org.hibernate.dialect.PostgreSQLDialect
quarkus.datasource.username = postgres
quarkus.datasource.password = postgres
# drop and create the database at startup (use `update` to only update the schema)
quarkus.hibernate-orm.database.generation = drop-and-create
後ほど、Quarkusをコンテナで動かすので、親Hostを見るためにdocker.for.mac.host.internal
を使っています。WindowsやLinuxの方は適宜、修正をお願いいたします。
それと、接続設定にちょこっとおかしな設定が入っていますが・・・種明かしをしてしまうと Jaeger で SQL のトレースログを出力させるために、JDBC にトレース用のドライバを挟んでいます。そして通常、Hibernate はドライバーでDialectを自動判別しますが、トレース用のドライバ挟んでしまうために PostgreSQL かどうかの判別ができなくなってしまいますので、Dialectを直接、指定しています。
トレースログ用のドライバーがなければ下記のようにシンプルな設定でOKです。
quarkus.datasource.url = jdbc:postgresql://docker.for.mac.host.internal:5432/mydatabase
quarkus.datasource.driver = org.postgresql.Driver
quarkus.datasource.username = postgres
quarkus.datasource.password = postgres
また quarkus.hibernate-orm.database.generation
はスキーマのマイグレーション方法の指定です。今回は試行錯誤多めで行くためにdrop-and-create
を選択してます。
2-2. モデルクラスの作成
さて、いよいよ実装です。まずはモデルクラスから。
import javax.persistence.Entity;
import java.time.LocalDate;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
@Entity
public class Person extends PanacheEntity {
public enum Status {
Alive, DECEASED
}
public String name;
@JsonFormat(pattern = "yyyy-MM-dd")
public LocalDate birth;
public Status status;
}
PanacheEntityを親クラスとしてモデルクラスを定義します。この Panache も以前、ご紹介した TypeORM 同様に "Active Record パターン" で簡単にDBアクセスできるようにするライブラリです。
今回は贅沢にも Enum
と LocalDate
をメンバーに使用してみました。
で、LocalDate
のJSONシリアライズ・デシリアライズがデフォルトではできないのでJSONFormatアノテーションで書式を指示しています。
また、特に何も指定してない"Enum 型"のStatus
ですが、問題なく JSON に交換できるようです。。。とんでもない。
2-3. REST API インタフェース部分の実装
続いてのコーディングは REST APIサービスのインタフェース部分です。
package org.acme.quarkus.sample;
import javax.transaction.Transactional;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.eclipse.microprofile.metrics.MetricUnits;
import org.eclipse.microprofile.metrics.annotation.Timed;
import org.eclipse.microprofile.metrics.annotation.Counted;
import org.acme.quarkus.sample.model.Person;
@Path("/person")
@Produces(MediaType.APPLICATION_JSON)
public class PersonResource {
@POST
@Transactional
@Counted(name = "performed_create", description = "How many it have been called.")
@Timed(name = "checksTimer_create", description = "A measure of how long it takes to perform creating person.", unit = MetricUnits.MILLISECONDS)
public Person create(Person person) {
person.persist();
return person;
}
@GET
@Path("/{id}")
@Transactional
@Counted(name = "performed_get", description = "How many it have been called.")
@Timed(name = "checksTimer_get", description = "A measure of how long it takes to perform getting person.", unit = MetricUnits.MILLISECONDS)
public Person get(@PathParam("id") Long id) {
return Person.findById(id);
}
}
とりあえず create
と get
だけのシンプルな動作確認用APIを作りましたが。。。
create
の引数で Person
を直接、受け取っています。JSON から Person
の交換は自動でやってくれるということです。
で、JSONを変換しただけの Person person
はまだDBに書き込まれていないので person.persist()
で DBに保存します。ここでIDを自動発番してperson
に入れておいてくれます。そしてそれを返すだけ。楽チン。
get
では Person
のクラスメソッドとなるfindById
を使用しています。Lombokもびっくりです。
ついでに見慣れない@Timed
、@Counted
アノテーションですが、これが Prometheus で記録するためのメトリクスを定義となります。
メソッド内のコードはあくまで純粋にビジネスロジックを記述することに集中でき、アノテーションで URLパスやURLパラメタとのマッピング、Content-Type、トランザクション、メトリクスなどの定義ができてしまいます。これが JavaEE だー!
2-4. Jaeger 設定の追加
さて、トレースログを採取するJaegerとの接続先とサンプラーの設定を行います。
ここでも接続先は親ホストである docker.for.mac.host.internal
となります。
quarkus.jaeger.endpoint=http://docker.for.mac.host.internal:14268/api/traces
quarkus.jaeger.service-name=sample.quarkus
quarkus.jaeger.sampler-type=const
quarkus.jaeger.sampler-param=1
service-name
はJaegerでの識別子となります。ここでは sample.quarkus
とします。
quarkus.jaeger.sampler-type
と quarkus.jaeger.sampler-param
は適当です。
2-5. Quarkus サーバーのポート設定
最後に Quarkus が待ち受けるポートを以下の設定で指定します。
quarkus.http.port=8082
Quarkus 側の作業は、以上です!
3. Jager、PostgreSQL の準備
動作確認に必要な Jaegerや PostgreSQLをdocker-composeでサクッと用意してしまいます。
3-1. docker-compose.yml の作成
以下のような docker-compose.yml
ファイルを作成します。
version : "3"
services:
db:
image: postgres
ports:
- 5432:5432
environment:
- POSTGRES_DB=mydatabase
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
adminer:
image: adminer
restart: always
ports:
- 8080:8080
jaeger:
image: jaegertracing/all-in-one
environment:
- COLLECTOR_ZIPKIN_HTTP_PORT=9411
ports:
- 5775:5775/udp
- 6831:6831/udp
- 6832:6832/udp
- 5778:5778
- 16686:16686
- 14268:14268
- 9411:9411
以下のコマンドで起動します。
$ docker-compose up -d
3-2. とりあえず Quarkus を動かしてみる
さて、ここまで来たところでとりあえず開発サーバー?を起動させてみましょう。以下のコマンドを叩きます。
$ mvn compile qurkus:dev
...
ブラウザで http://localhost:8082
を開いてみましょう。Quarkus のスタートページが表示されればとりあえずOKです!
4. ネイティブ化
開発サーバーでの動作確認ができましたらいよいよネイティブ化をしてみます。
Creating a container with a multi-stage Docker build の箇所を参考に、以下のようなネイティブコンパイル用のコンテナを作成いたします。
## Stage 1 : build with maven builder image with native capabilities
FROM quay.io/quarkus/centos-quarkus-maven:19.2.1 AS build
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
USER root
RUN chown -R quarkus /usr/src/app
USER quarkus
RUN mvn -f /usr/src/app/pom.xml -Pnative -Dmaven.test.skip=true clean package
## Stage 2 : create the docker final image
FROM registry.access.redhat.com/ubi8/ubi-minimal
WORKDIR /work/
COPY --from=build /usr/src/app/target/*-runner /work/application
RUN chmod 775 /work
EXPOSE 8080
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]
そして、以下のコマンドでビルドします!
$ docker build -t quarkus-rest-demo .
...
ビルドが無事に終われば、quarkus-rest-demo
イメージにネイティブ化されたバイナリが入っています!
このコンテナ単体で4GB程度のメモリを使いますのでメモリ不足や他のコンテナが起動している場合はビルドが失敗する可能性大です。(というか、かなりメモリに余裕がないとビルドに失敗します。)
これだけのコードでも環境によっては10分程度の時間がかかります。
5. 起動と動作確認
それでは、実際にコンテナを起動してましょう。PostgreSQLやJaegerのコンテナを止めている場合は
そちらを先に起動させておいてください。
5-1. ネイティブ Quarkus サービスの起動
以下のコマンドで起動します。
$ docker run -it quarkus-rest-demo
5-2. ターミナルで POST
別のターミナルを開き、以下のコマンドでPersonResource#create
メソッドにJSONをPOSTしてみます。
$ curl -H 'Content-Type:application/json' -d '{"name":"Alice","birth":"2010-10-11","status":"Alive"}' http://localhost:8082/person
{"id":1,"birth":"2010-10-11","name":"Alice","status":"Alive"}
と、id
が追加されてJSONが返却されたら成功です!
5-3. ブラウザで GET
ブラウザで http://localhost:8082/person/1
を開いてみましょう。
こちらでも同様に {"id":1,"birth":"2010-10-11","name":"Alice","status":"Alive"}
が取得できたでしょうか?
5-4. Adminer で確認
例によって Adminer で確認してみましょう。
ブラウザで http://locahost:8080
を開き、Adminerにアクセスします。上記の設定で指定したpostgres/postgres/mydatabase
でログインし、 SQLコマンドにてselect * from person;
を投げてみると・・・
(連打したので複数件の)レコードが取得できました!ちゃんと ID がインクリメントされているのがわかります。
このあたりの発番処理は Panache が自動でやってくれています。
5-5. Jaeger で確認
いよいよ本題の、Jaeger でのトレースログを確認してみましょう。
ブラウザで http://localhost:16686
を開き、Jaeger のコンソールを開きます。
サービス名からsample.quarkus
を選択し、PersonResouce.create
のログをクリックしてみましょう。
このように、リクエスト全体で 27ms
、IDの発番に 11ms
、Insert文に 2ms
というのがわかってしまいます。
また発行されたSQL文も確認できるので、より一層ボトルネックのチェックが簡単ですね〜!
6. Prometheus + cAdvisor + Grafana でも見てみる
さて、以前の記事で使ってみた cAdvisor
ですが今回もしっかりとチェックしてみましょう。
6-1. dockprom の準備
今回はちょと設定に手を加えたので、本家ではなく私のリポジトリから clone
してきます。
$ git@github.com:Yoshinori-Koide-PRO/dockprom.git
...
$ cd dockprom
で、prometheus/prometheus.yml
の設定で Quarkus 側(親ホスト)のアドレスを調整します。
...
- job_name: 'quarkus'
scrape_interval: 5s
static_configs:
- targets: ['docker.for.mac.host.internal:8082']
...
今回は Docker Desktop を使っているので docker.for.mac.host.internal
を使っていますが、linux の場合は、以下のような .env
ファイルを作成し、
# ip -4 addr show docker0 | grep -Po 'inet \K[\d.]+' で取得できた IPを以下に記載
HOME_IP=172.17.0.1
.env
で定義したHOME_IP
をdocker-compose.ymlでコンテナのextra_host
に渡します。
...
prometheus:
image: prom/prometheus:v2.13.1
container_name: prometheus
volumes:
- ./prometheus/:/etc/prometheus/
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
- '--storage.tsdb.retention.time=200h'
- '--web.enable-lifecycle'
restart: unless-stopped
extra_hosts:
- "docker_host:$HOME_IP"
expose:
- 9090
networks:
- monitor-net
labels:
org.label-schema.group: "monitoring"
...
これで親ホストのIPをdocker_host
というホスト名で解決できるようになります。
Prometheusの設定ファイルでは環境変数など使えないようなので、苦肉の作です。。。
で、起動は docker-compose up -d
でOKです!
6-2. Prometheus での表示
まずは Prometheus です。
ブラウザで http://localhost:9090
を開き、admin/admin
で Prometheus にログインします。
Expression
の欄に personresource
と打つと候補がリスト表示されます。
適当に選択し、"Graph"タブをクリックすると、取得した値のグラフが表示されます!
グラフ下の凡例?の文字列が Prometheus のクエリとなるようです。これをコピペすればこのグラフが Grafanaで表示できます。
6-3. cAdvisor + Grafana でチェック!
最後に Grafana でコンテナの状況を確認してみましょう。
ブラウザで http://localhost:3000
を開き、admin/admin
でログインします。
左上の+
からメニューを開きDocker Containers
を選択すると、立ち上がっているコンテナ群のダッシュボードが表示されます!
上記のスクショで recurcing_blackwall
と表示されているのが quarkus-rest-demo のコンテナです。(コンテナ名、指定しないから・・・)
test_env_db_1
が PostgreSQL のコンテナです。
以下ではメモリ使用量のグラフです。
まだコンテナを起動した直後ですが、PostgreSQLよりも圧倒的に少ないメモリ量で起動しているようです。。。
Javaで書いたアプリとは信じられません。。。
何と言っても単位が10MB単位っていう箇所ですね・・・。最近のJavaアプリケーションサーバーはGB単位でメモリ当てますよね?!
全体的なフットプリントが軽いのが安心感ありますね〜!
7. OpenAPIのエンドポイントの確認
さて、メトリクス系のエンドポイントについてはチェックができました。続いて OpenAPIのエンドポイントもチェックしてみましょう!
7-1. Swagger UI
Swagger UI は開発サーバーモードでなくては起動しません。こちらは mvn quarkus:dev
で起動したのち、ブラウザでhttp://localhost:8082/swagger-ui
を開いてみましょう。
おなじみのSwagger UIですが、公開しているAPIの説明文と直接、叩けるコンソールが表示されます。
ネイティブ化した後でもSwagger UIを公開する場合は以下のオプションを設定するようです。
quarkus.swagger-ui.always-include=true
7-2. Open API
こちらはネイティブ化した後でも有効な(当然です)、OpenAPIのエンドポイントです。
ブラウザで http://localhost:8082/openapi
を開くと以下のようなテキストファイルがダウンロードされます。
---
openapi: 3.0.1
info:
title: Generated API
version: "1.0"
paths:
/hello:
get:
responses:
200:
description: OK
content:
text/plain:
schema:
type: string
/person:
post:
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Person'
/person/{id}:
get:
parameters:
...
はい、OpenAPIバッチリですね!
まとめ
Quarkus についてちょっとした設定とアノテーションの追加で非常に多くのエンドポイントとの連携ができるのが確認できました。
ネイティブ化しても動くかどうか疑問だったのですが、想像以上にサポートが厚くて驚きましたね。
特に Hibernateがらみは JDBC もあるし、ネイティブコンパイルとか大変なんじゃなかろうか?と疑っていましたが、バッチリ動いてしまいました。こりゃ参ったなぁ。。。
ネイティブビルドにはメモリ量とコンパイル時間という敷居はありますが、ここまで軽量なのいろいろできてしまうと・・・有り寄りの有り、なんじゃないでしょうか?!
上記の成果物は github にあげました。こちらをどうぞ〜
→ 起動方法がイマイチでしたので、構成を変更いたしました。詳しくは次の記事:"【2019年度11月版】複数のdocker-composeをまたいでコンテナの名前解決をする"をご覧ください!
本日は以上です!