Help us understand the problem. What is going on with this article?

【2019年11月版】Quarkus で Hibernate-Panache の REST API 作ってネイティブ化し Jaeger でトレース

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> に以下の依存モジュールを追加します。

pom.xml
...
    <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 の設定を以下のように追加します

src/main/resources/application.properties
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です。

src/main/resources/application.properties
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. モデルクラスの作成

さて、いよいよ実装です。まずはモデルクラスから。

src/main/java/org/acme/quarkus/sample/model/Person.java
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アクセスできるようにするライブラリです。

今回は贅沢にも EnumLocalDate をメンバーに使用してみました。
で、LocalDateのJSONシリアライズ・デシリアライズがデフォルトではできないのでJSONFormatアノテーションで書式を指示しています。

また、特に何も指定してない"Enum 型"のStatusですが、問題なく JSON に交換できるようです。。。とんでもない。

2-3. REST API インタフェース部分の実装

続いてのコーディングは REST APIサービスのインタフェース部分です。

src/main/java/org/acme/quarkus/sample/PersonResource.java
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);
    }
}

とりあえず createget だけのシンプルな動作確認用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 となります。

src/main/resources/application.properties
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-typequarkus.jaeger.sampler-paramは適当です。

2-5. Quarkus サーバーのポート設定

最後に Quarkus が待ち受けるポートを以下の設定で指定します。

src/main/resources/application.properties
quarkus.http.port=8082

Quarkus 側の作業は、以上です!

3. Jager、PostgreSQL の準備

動作確認に必要な Jaegerや PostgreSQLをdocker-composeでサクッと用意してしまいます。

3-1. docker-compose.yml の作成

以下のような 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; を投げてみると・・・

SQLコマンド - db - Adminer.png

(連打したので複数件の)レコードが取得できました!ちゃんと ID がインクリメントされているのがわかります。
このあたりの発番処理は Panache が自動でやってくれています。

5-5. Jaeger で確認

いよいよ本題の、Jaeger でのトレースログを確認してみましょう。

ブラウザで http://localhost:16686 を開き、Jaeger のコンソールを開きます。
サービス名からsample.quarkusを選択し、PersonResouce.create のログをクリックしてみましょう。

Jaeger UI native.png

このように、リクエスト全体で 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 側(親ホスト)のアドレスを調整します。

prometheus/prometheus.yml
...
  - 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に渡します。

docker-compose.yml
...
  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と打つと候補がリスト表示されます。
Prometheus Time Series Collection and Processing Server.png

適当に選択し、"Graph"タブをクリックすると、取得した値のグラフが表示されます!

Prometheus Time Series Collection and Processing Server (1).png

グラフ下の凡例?の文字列が Prometheus のクエリとなるようです。これをコピペすればこのグラフが Grafanaで表示できます。

6-3. cAdvisor + Grafana でチェック!

最後に Grafana でコンテナの状況を確認してみましょう。

ブラウザで http://localhost:3000 を開き、admin/admin でログインします。
左上の+からメニューを開きDocker Containersを選択すると、立ち上がっているコンテナ群のダッシュボードが表示されます!
Docker Containers - Grafana.png

上記のスクショで recurcing_blackwall と表示されているのが quarkus-rest-demo のコンテナです。(コンテナ名、指定しないから・・・)
test_env_db_1 が PostgreSQL のコンテナです。

以下ではメモリ使用量のグラフです。

Docker Containers - Grafana (1).png

まだコンテナを起動した直後ですが、PostgreSQLよりも圧倒的に少ないメモリ量で起動しているようです。。。
Javaで書いたアプリとは信じられません。。。
何と言っても単位が10MB単位っていう箇所ですね・・・。最近のJavaアプリケーションサーバーはGB単位でメモリ当てますよね?!
全体的なフットプリントが軽いのが安心感ありますね〜!

7. OpenAPIのエンドポイントの確認

さて、メトリクス系のエンドポイントについてはチェックができました。続いて OpenAPIのエンドポイントもチェックしてみましょう!

7-1. Swagger UI

Swagger UI は開発サーバーモードでなくては起動しません。こちらは mvn quarkus:dev で起動したのち、ブラウザでhttp://localhost:8082/swagger-uiを開いてみましょう。

Swagger UI.png

おなじみのSwagger UIですが、公開しているAPIの説明文と直接、叩けるコンソールが表示されます。
ネイティブ化した後でもSwagger UIを公開する場合は以下のオプションを設定するようです。

src/main/resources/application.properties
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をまたいでコンテナの名前解決をする"をご覧ください!

本日は以上です!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away