この記事は、「Java EE Advent Calendar 2016 - Qiita」の14日目の記事となります。
昨日は、@n_agetsuさんの「Commons Lang 3.5 でJava EEにBreakerを組み込む - 見習いプログラミング日記」でした。
明日は、@khasunumaさんのご担当となります。
JAX-RSでもZipkin
今回のお題には、JAX-RSとZipkinを使います。Zipkinを使うのは初めてなのですが、JAX-RS用の
モジュールも用意されているようなので、これを機に試してみようと思いまして。
なんとなく、このあたりを使うならSpring Cloud〜なイメージもありますが、今回はJAX-RSの素体で押し通します!
※ZipkinはSpring Bootを使って作られていますが
Zipkinって?
最近のMicroservicesの話題でよく出てくる、Distributed Tracingを実現するためのライブラリです。
OpenZipkin · A distributed tracing system
アーキテクチャについてはこちらに図がありますが、リクエストとレスポンス時にZipkin Serverに
メタデータを記録する仕組みみたいです。
この集められたトレースデータを、Spanと呼ぶそうです。また、Zipkinにデータを送る役割を
受け持つ人を、Reporterと呼ぶと。
このあたりで使うライブラリが、Braveというものにまとめられているようなので、こちらを
使って簡単なDistributed Tracingを行ってみます。
GitHub - openzipkin/brave: Java distributed tracing implementation compatible with Zipkin backend services.
では、試してみましょう。
構成とゴール
今回は、以下の構成でアプリケーションを作成します。
せっかくなので、JPAを使ってMySQLにアクセスして、ClientからMySQLまでのアクセスを
Zipkinに記録し、Zipkin UIで見るところまでをゴールにしたいと思います。
別にDistributedな感じはしませんけど…。
Zipkin Server & Zipkin UI
Zipkin ServerとUIについては、Zipkin素体を使ってさっくりと起動します。
※最初、Zipkin ServerのDockerイメージとExperimental new UI for Zipkinを使っていたのですが、他と割と違うことに気づきやめました…
2016/12/22追記)
最初、Spring Cloud Sleuthを使っていたのですが、@makingさんに要らないと突っ込まれたので、外してZipkinのみで
いく感じにしました。
@kazuhira_r spring-cloud-starter-zipkinはinstrumentsにsleuthを使うためのstarterだから、Zipkin Serverには入らないかと思います
確かに不要でした、ありがとうございましたー。
Maven依存関係を、このように定義。
<dependencies> <dependency> <groupId>io.zipkin.java</groupId> <artifactId>zipkin-server</artifactId> <version>1.17.1</version> </dependency> <dependency> <groupId>io.zipkin.java</groupId> <artifactId>zipkin-autoconfigure-ui</artifactId> <version>1.17.1</version> </dependency> </dependencies>
Spring Bootのプラグインも入れておきます。
<plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>1.4.2.RELEASE</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin>
アプリケーションは、これだけ。
src/main/java/javaeeadventcalendar/ZipkinServerApp.java
package javaeeadventcalendar; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import zipkin.server.EnableZipkinServer; @SpringBootApplication @EnableZipkinServer public class ZipkinServerApp { public static void main(String... args) { SpringApplication.run(ZipkinServerApp.class, args); } }
Zipkin Serverは、9411ポートでリッスンさせるのが通常のようなのでパッケージング後に「--server.port」指定で起動します。
$ mvn package $ java -jar zipkin-ui/target/zipkin-ui-0.0.1-SNAPSHOT.jar --server.port=9411
「http://localhost:9411/」にアクセスすると、Zipkin UIが見れます。
ここまでで、Zipkin ServerとUIの用意が整いました。
JAX-RS Server & JPA
JAX-RS Server側とJPAのサンプルプログラムを作成します。Brave MySQLの都合上、
アプリケーションサーバー(特にモジュールシステムのあるWildFly)を持ってくると
手間がかかることがわかったので、今回はRESTEasy+Netty 4で実装します。
Maven依存関係
使用したMaven依存関係は、こちら。
<!-- JAX-RS / RESTEasy and Netty 4 --> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-netty4</artifactId> <version>3.0.19.Final</version> <exclusions> <exclusion> <groupId>org.jboss.logging</groupId> <artifactId>jboss-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.0.42.Final</version> </dependency> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-jackson2-provider</artifactId> <version>3.0.19.Final</version> </dependency> <!-- JPA --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>5.2.5.Final</version> </dependency> <!-- Brave Libraries --> <!-- Brave JAXRS2 --> <dependency> <groupId>io.zipkin.brave</groupId> <artifactId>brave-jaxrs2</artifactId> <version>3.16.0</version> </dependency> <!-- Brave MySQL --> <dependency> <groupId>io.zipkin.brave</groupId> <artifactId>brave-mysql</artifactId> <version>3.16.0</version> </dependency> <!-- Reporter --> <dependency> <groupId>io.zipkin.reporter</groupId> <artifactId>zipkin-sender-urlconnection</artifactId> <version>0.6.9</version> </dependency> <!-- MySQL JDBC Driver --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.40</version> </dependency>
JAX-RSとJPAの部分ははしょりますが、Braveの部分だけ説明します。
Zipkinとの連携用に、JAX-RS、MySQL用にそれぞれBraveのライブラリを追加します。
<!-- Brave Libraries --> <!-- Brave JAXRS2 --> <dependency> <groupId>io.zipkin.brave</groupId> <artifactId>brave-jaxrs2</artifactId> <version>3.16.0</version> </dependency> <!-- Brave MySQL --> <dependency> <groupId>io.zipkin.brave</groupId> <artifactId>brave-mysql</artifactId> <version>3.16.0</version> </dependency>
ただ、これだけだとZipkinにデータを送信する人がいないので、Reporterとして今回は
URLConnectionを使う実装を追加します。
<!-- Reporter --> <dependency> <groupId>io.zipkin.reporter</groupId> <artifactId>zipkin-sender-urlconnection</artifactId> <version>0.6.9</version> </dependency>
JavaでのRepoterは、こちら。
GitHub - openzipkin/zipkin-reporter-java: Shared library for reporting zipkin spans on transports such as http or kafka
依存関係は以上です。
JPA Entity
とりあえず、話が単純なJPAのEntityから。
src/main/java/javaeeadventcalendar/Book.java
package javaeeadventcalendar; import java.io.Serializable; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name = "book") public class Book implements Serializable { private static final long serialVersionUID = 1L; @Id private String isbn; @Column private String title; @Column private int price; // getter/setterは省略 }
テーブル定義のSQLは、テーブルの自動生成が有効な場合にHibernateが出力するものをまんま使用しました。
create table book ( isbn varchar(255) not null, price integer, title varchar(255), primary key (isbn) ) ENGINE=InnoDB;
persistence.xmlとEntityManager
persistence.xmlはこちら。EE環境ではないので、transaction-typeはRESOURCE_LOCALです。
src/main/resources/META-INF/persistence.xml
<?xml version="1.0" encoding="UTF-8"?> <persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd" version="2.1"> <persistence-unit name="brave.jaxrs.pu" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider> <properties> <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/> <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/practice?statementInterceptors=com.github.kristofa.brave.mysql.MySQLStatementInterceptor&zipkinServiceName=myJaxrsMySqlService&useUnicode=true&characterEncoding=utf-8&characterSetResults=utf-8&useServerPrepStmts=true&useLocalSessionState=true&elideSetAutoCommits=true&alwaysSendSetIsolation=false&useSSL=false"/> <property name="javax.persistence.jdbc.user" value="kazuhira"/> <property name="javax.persistence.jdbc.password" value="password"/> <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL57InnoDBDialect"/> <property name="hibernate.show_sql" value="true"/> <property name="hibernate.format_sql" value="true"/> </properties> </persistence-unit> </persistence>
で、JDBCの接続文字列をずらずらと書いているのですが、今回のテーマで重要なのは次の2つです。
statementInterceptors=com.github.kristofa.brave.mysql.MySQLStatementInterceptor&zipkinServiceName=myJaxrsMySqlService
https://github.com/openzipkin/brave/tree/master/brave-mysql
Brave MySQLは、MySQL JDBC Driver(Connector/J)のStatementInterceptorの仕組みで
トレーシングを実現しているようですね。
あと、Java EE環境ではないのでEntityManagerを使うための簡易クラスを用意しておきました。
src/main/java/javaeeadventcalendar/EntityManagerProvider.java
package javaeeadventcalendar; import java.util.function.Consumer; import java.util.function.Function; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; public class EntityManagerProvider { private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("brave.jaxrs.pu"); public static <T> T call(Function<EntityManager, T> func) { EntityManager entityManager = emf.createEntityManager(); try { return func.apply(entityManager); } finally { entityManager.close(); } } public static void run(Consumer<EntityManager> func) { EntityManager entityManager = emf.createEntityManager(); try { func.accept(entityManager); } finally { entityManager.close(); } } }
JAX-RS側のクラス
JAX-RSリソースクラスは、単純なものにしています。
src/main/java/javaeeadventcalendar/BookResource.java
package javaeeadventcalendar; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.EntityTransaction; import javax.persistence.Persistence; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; @Path("book") public class BookResource { @GET @Path("{isbn}") @Produces(MediaType.APPLICATION_JSON) public Book find(@PathParam("isbn") String isbn) { return EntityManagerProvider.call(em -> em.find(Book.class, isbn)); } @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Response register(Book book, @Context UriInfo uriInfo) { EntityManagerProvider.run(em -> { EntityTransaction tx = em.getTransaction(); try { tx.begin(); em.persist(book); tx.commit(); } catch (Exception e) { tx.rollback(); throw new RuntimeException(e); } }); return Response.created(uriInfo.getRequestUriBuilder().path(book.getIsbn()).build()).build(); } }
ちょっと変わるのがApplicationのサブクラスで、こちらのgetSingletonsメソッドでBraveに関する
設定をしたインスタンスを返却します。
src/main/java/javaeeadventcalendar/JaxrsActivator.java
package javaeeadventcalendar; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import javax.ws.rs.ApplicationPath; import javax.ws.rs.core.Application; import com.github.kristofa.brave.Brave; import com.github.kristofa.brave.jaxrs2.BraveTracingFeature; import zipkin.Span; import zipkin.reporter.AsyncReporter; import zipkin.reporter.urlconnection.URLConnectionSender; @ApplicationPath("") public class JaxrsActivator extends Application { @Override public Set<Class<?>> getClasses() { return new HashSet<>(Arrays.asList(BookResource.class)); } @Override public Set<Object> getSingletons() { // Setup Zipkin / JAX-RS Server AsyncReporter<Span> asyncReporter = AsyncReporter .builder(URLConnectionSender.create("http://localhost:9411/api/v1/spans")).build(); Brave brave = new Brave.Builder("myJaxrsServerService").reporter(asyncReporter).build(); BraveTracingFeature tracingFeature = BraveTracingFeature.create(brave); return new HashSet<>(Arrays.asList(tracingFeature)); } }
他でもこういうコードは登場するので、ここで1度説明しておきます。
Reporterを作成しますが、SenderとしてURLConnectionSenderを指定します。Zipkinの
URLは「http://localhost:9411/api/v1/spans」を指定します。
AsyncReporter<Span> asyncReporter =
AsyncReporter
.builder(URLConnectionSender.create("http://localhost:9411/api/v1/spans")).build();
作成したReporterをBrave.Builderに設定してBraveのインスタンスを作成し、BraveTracingFeatureを
作成、getSingletonsの結果に含めるようにします。
Brave brave = new Brave.Builder("myJaxrsServerService").reporter(asyncReporter).build(); BraveTracingFeature tracingFeature = BraveTracingFeature.create(brave);
なお、Brave.Builderの引数に与える名前がZipkin UI上で見る時の名前として重要に
なるので、意味のわかる名前を設定しましょう。
起動クラス
JAX-RS Server側の起動クラスは、以下のように用意。
src/main/java/javaeeadventcalendar/ServerBootstrap.java
package javaeeadventcalendar; import com.github.kristofa.brave.Brave; import com.github.kristofa.brave.mysql.MySQLStatementInterceptorManagementBean; import org.jboss.resteasy.plugins.server.netty.NettyJaxrsServer; import org.jboss.resteasy.spi.ResteasyDeployment; import zipkin.Span; import zipkin.reporter.AsyncReporter; import zipkin.reporter.urlconnection.URLConnectionSender; public class ServerBootstrap { public static void main(String... arsg) throws Exception { // Setup Zipkin / MySql AsyncReporter<Span> asyncReporter = AsyncReporter .builder(URLConnectionSender.create("http://localhost:9411/api/v1/spans")).build(); Brave brave = new Brave.Builder("myJaxrsMySqlService").reporter(asyncReporter).build(); new MySQLStatementInterceptorManagementBean(brave.clientTracer()); // Setup JAX-RS / RESTEasy & Netty 4 NettyJaxrsServer jaxrsServer = new NettyJaxrsServer(); ResteasyDeployment deployment = jaxrsServer.getDeployment(); deployment.setApplicationClass(JaxrsActivator.class.getName()); jaxrsServer.setRootResourcePath(""); jaxrsServer.setPort(8080); jaxrsServer.setDeployment(deployment); jaxrsServer.start(); } }
後半はRESTEasy+Netty 4を使ったJAX-RS Server起動のコードですが、前半はBrave MySQLのためのコードになっています。
// Setup Zipkin / MySql AsyncReporter<Span> asyncReporter = AsyncReporter .builder(URLConnectionSender.create("http://localhost:9411/api/v1/spans")).build(); Brave brave = new Brave.Builder("myJaxrsMySqlService").reporter(asyncReporter).build(); new MySQLStatementInterceptorManagementBean(brave.clientTracer());
内容自体は、JAX-RSのApplicationクラスを紹介した時と似た感じです。ただ、最後に次の1行が入っている
ところがポイントです。
new MySQLStatementInterceptorManagementBean(brave.clientTracer());
これがないと、JDBC接続文字列にStatementInterceptorを設定しても、Zipkinにトレースデータが
送信されません。
JAX-RS Client
続いては、JAX-RS Client。こちらにも、Brave JAXRS2を加えます。JAX-RS Clientの
実装には、やっぱりRESTEasyを利用。
Maven依存関係
使用したMaven依存関係は、こちら。Server側を見たあとだと、JAX-RSがClient用になっていること以外は、
そう迷うものはないかなと思います。
<!-- JAX-RS / RESTEasy --> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-client</artifactId> <version>3.0.19.Final</version> </dependency> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-jackson2-provider</artifactId> <version>3.0.19.Final</version> </dependency> <!-- Brave Libraries --> <!-- Brave JAXRS2 --> <dependency> <groupId>io.zipkin.brave</groupId> <artifactId>brave-jaxrs2</artifactId> <version>3.16.0</version> </dependency> <!-- Reporter --> <dependency> <groupId>io.zipkin.reporter</groupId> <artifactId>zipkin-sender-urlconnection</artifactId> <version>0.6.9</version> </dependency>
Entity
Server側とは別コードで作成したので、JSONのやり取りに使用するEntityクラスを作成。
src/main/java/javaeeadventcalendar/Book.java
package javaeeadventcalendar; public class Book { private String isbn; private String title; private int price; public static Book create(String isbn, String title, int price) { Book book = new Book(); book.setIsbn(isbn); book.setTitle(title); book.setPrice(price); return book; } public Book() { } // getter/setterは省略 }
JAX-RS Clientと起動クラス
JAX-RS Clientの実装は、こんな感じで用意。
src/main/java/javaeeadventcalendar/ClientApp.java
package javaeeadventcalendar; import java.net.URI; import java.util.concurrent.TimeUnit; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.Entity; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import com.github.kristofa.brave.Brave; import com.github.kristofa.brave.jaxrs2.BraveTracingFeature; import zipkin.Span; import zipkin.reporter.AsyncReporter; import zipkin.reporter.urlconnection.URLConnectionSender; public class ClientApp { public static void main(String... args) throws InterruptedException { // Setup Zipkin / JAX-RS Client AsyncReporter<Span> asyncReporter = AsyncReporter .builder(URLConnectionSender.create("http://localhost:9411/api/v1/spans")).build(); Brave brave = new Brave.Builder("myJaxrsClientService").reporter(asyncReporter).build(); Client client = ClientBuilder .newBuilder() .register(BraveTracingFeature.create(brave)) .build(); Response registerResponse = client .target("http://localhost:8080/book") .request() .put(Entity.entity(Book.create("978-4774183169", "パーフェクト Java EE", 3456), MediaType.APPLICATION_JSON_TYPE)); URI location = registerResponse.getLocation(); System.out.println("Location = " + location); registerResponse.close(); System.out.println("====="); Response findResponse = client .target(location) .request() .get(); Book responseBook = findResponse.readEntity(Book.class); System.out.println("isbn = " + responseBook.getIsbn()); System.out.println("title = " + responseBook.getTitle()); System.out.println("price = " + responseBook.getPrice()); findResponse.close(); client.close(); // wait Zipkin request... System.out.println("wait Zipkin request..."); TimeUnit.SECONDS.sleep(2L); } }
通常のJAX-RS Clientを使う時と違うところは、ClientBuilder#registerでBraveTracingFeatureを追加しているところくらいです。
// Setup Zipkin / JAX-RS Client AsyncReporter<Span> asyncReporter = AsyncReporter .builder(URLConnectionSender.create("http://localhost:9411/api/v1/spans")).build(); Brave brave = new Brave.Builder("myJaxrsClientService").reporter(asyncReporter).build(); Client client = ClientBuilder .newBuilder() .register(BraveTracingFeature.create(brave)) .build();
BraveおよびReporterを作成しているところは、今までのパターンとそう変わりません。
あと、Zipkinに非同期にデータを送信している都合上、プログラムを即終了してしまうと
Zipkinにデータが渡りきらないため、ちょっとsleepを入れています。
// wait Zipkin request... System.out.println("wait Zipkin request..."); TimeUnit.SECONDS.sleep(2L);
最初、これでClient側のトレースデータが入らずに「なんでだろう?」とか思っていました…。
ここまでで、サンプルコードはできあがりです。
動作確認
それでは、作成したコードをパッケージングします。
今回作成したコードは、Mavenのマルチプロジェクトになっていますので、トップレベルのディレクトリでパッケージングします。
$ find brave-jaxrs-* pom.xml -type f brave-jaxrs-client/pom.xml brave-jaxrs-client/src/main/java/javaeeadventcalendar/ClientApp.java brave-jaxrs-client/src/main/java/javaeeadventcalendar/Book.java brave-jaxrs-server/pom.xml brave-jaxrs-server/src/main/java/javaeeadventcalendar/EntityManagerProvider.java brave-jaxrs-server/src/main/java/javaeeadventcalendar/Book.java brave-jaxrs-server/src/main/java/javaeeadventcalendar/BookResource.java brave-jaxrs-server/src/main/java/javaeeadventcalendar/JaxrsActivator.java brave-jaxrs-server/src/main/java/javaeeadventcalendar/ServerBootstrap.java brave-jaxrs-server/src/main/resources/META-INF/persistence.xml pom.xml
パッケージング。
$ mvn package
さらに言うと、両方ともSpring BootのMaven Pluginを使っているので、Uber JARになります。
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>1.4.2.RELEASE</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin>
では、実行。まずはServer側を起動します。めちゃくちゃ高速に起動します(笑)。
$ java -jar brave-jaxrs-server/target/brave-jaxrs-server-0.0.1-SNAPSHOT.jar
そして、Client側を実行。
$ java -jar brave-jaxrs-client/target/brave-jaxrs-client-0.0.1-SNAPSHOT.jar Location = http://localhost:8080/book/978-4774183169 isbn = 978-4774183169 title = パーフェクト Java EE price = 3456 wait Zipkin request...
プログラムの実行が終わったら、Zipkin UIをブラウザで参照します。すると、左のプルダウンで
Zipkinに送信したサービスが選べるようになっています。
今回は「myjaxrsclientservice」を選び、「Start time」に適当な値を設定して「Find Traces」を実行。
トレースの結果が2つ確認できます。
示されたトレース情報は、選択できるのでクリックします。すると、さらに内訳を見ることができます。
展開された後に表示される、棒グラフの部分をクリックすると、もっと細かい情報が。
なんとなく、可視化された感じになりますね?(笑)
まとめ
Brave JAXRS2とBrave MySQLを使って、Distributed Tracing(分散してないけど)を試してみました。
組み込みには慣れが要りそうな感じ?ではありますが、まず使ってみる分にはそう困らないのではないでしょうか?
少しでも興味を持っていただけると、幸いです。
今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee-advent-calendar/tree/master/2016
明日は、@khasunumaさんのご担当です。よろしくお願いします!