Java Advent Calendarの22日目担当のmiya10keiです。
今回は分散トレーシングシステムであるZipkinを勉強しながら使ってみた話になります。
はじめに
まずはじめに、分散トレーシングについて完全に素人だったのでこの辺のスライドやブログを見てみました。
- https://www.slideshare.net/rakutentech/spring-cloudzipkin
- http://www.baeldung.com/tracing-services-with-zipkin
分散トレーシングとは
そもそも、分散トレーシングとはなにかという話ですが、分散されたシステムで処理されるリクエストを追跡(トレーシング)するためのものになります。マイクロサービスのような複数のサービスで構成されるシステム(分散システム)では、1つのリクエストが複数のサービスをまたいで処理されることにより、リクエスト全体の処理の流れを把握する(トレーサビリティ)ことが難しくなります。また、トレーサビリティが低下することによって障害やリクエストのレイテンシーが悪化した際に、どのサービスに問題があるのかを見つけることも難しくなります。このような問題をサービスの依存関係やサービス単位のレイテンシーを可視化することによって解決の手助けをするシステムを分散トレーシングシステムといいます。
分散トレーシングの用語
分散トレーシングの用語として次の2つがあります。
- Span: 1サービス内の処理を表す。
- Trace: Requestのstart-endを含むSpanの集合をあらわす。
Zipkin
それでは本題の「Zipkin」についてです。
ZipkinはGoogleのDapperを参考にTwitter社によって開発されたOSSの分散トレーシングシステムになります。
Zipkinでは、各サービス間のAPIコールのデータを収集する機能と、そのデータを可視化するためのUIを提供します。
Zipkinの公式サイトはこちらになります。
Zipkinのアーキテクチャ
続いてZipkinのアーキテクチャについてです。
まずは公式サイトにのっている全体アーキテクチャを見てみます。
Zipkinでは大きく以下の5つのコンポーネントに分かれるようです。
Reporter
ReporterはアプリからZipkinサーバにログを転送する役割を担います。
instrumented librariesが各言語用にいくつか提供されています。サポート言語についてはこちらを参照ください。
Collector
Collectorは各アプリから転送されたログを収集する役割を担います。
収集方法としてはHTTP, Kafka, RabbitMQ, Scribeがサポートされているようです。
https://zipkin.io/pages/existing_instrumentations.html
Storage
Storageは転送されたログの永続化の役割を担います。
データストアとして、In-Memory, MySQL, Cassandra, Elasticsearchがサポートされているようです。
https://github.com/openzipkin/zipkin/tree/master/zipkin-storage
API
APIはStorageからいろいろな方法でデータを抽出する機能を提供します。
UI
Storageに永続化されているログデータをブラウザ上で可視化するUIになります。
Zipkin UIには3つの画面があります。
Find Trace
「Find Trace」はサービス名や期間、処理時間からログを検索する画面になります。
Drill Down
「Drill Down」は1Traceに含まれる全Spanとその処理時間を可視化した画面になります。
Dependencies
「Dependencies」はログ情報から各サービスの依存関係を可視化した画面になります。
より依存関係が強い(多くのリクエストが発生している)ものは矢印が太くなります。
使ってみる
それでは実際にZipkinを使ってみます。
今回はSpring Bootを使って複数のサービスを構築し、それぞれのログをZipkinサーバに転送します。
Reporterのinstrumented librariesにはspring-cloud-sleuthを使用します。
構成は次のようになります。
※ DBはVagrant + VirtualBox + Ubuntu 16.04 上にMySQLをインストールして使用します。
また、Zipkinサーバに転送したログも同様のMySQLに永続化することにします。
今回使用したソースは以下になります。
https://github.com/miya10kei/spring-cloud-sleuth_zipkin
Zipkinサーバの構築
まずは、Zipkinサーバを構築します。
ZipkinサーバはSpring Bootで作成されており、Quickstartで紹介されているように「java -jar」で起動することができます。しかし、今回はせっかくなので違った方法で構築したいと思います。
pom.xmlに次の依存関係を追加します。
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-server</artifactId>
<version>2.4.1</version>
</dependency>
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-autoconfigure-ui</artifactId>
<version>2.4.1</version>
</dependency>
そして、SpringApplicationクラスに「@EnableZipkinServer」のアノテーションを追加します。
@SpringBootApplication
@EnableZipkinServer // Zipkinサーバを有効化するアノテーションを追加
public class ZipkinServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZipkinServerApplication.class, args);
}
}
今回はデータストアにMySQLを使用するのでその設定を追加します。
server.port=9411
# Storage
zipkin.storage.type=mysql
zipkin.storage.mysql.host=192.168.33.10
zipkin.storage.mysql.port=3306
zipkin.storage.mysql.username=root
zipkin.storage.mysql.password=password
zipkin.storage.mysql.db=zipkin
以上でZipkinサーバの構築は終了です。
Service1
次はService1の構築に移ります。
pom.xmlに以下の依存関係を追加します。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
SpringApplicationクラスにService2,3にHTTPでリクエストするために必要なRestTemplateをBeanとして定義します。
@SpringBootApplication
public class Service1Application {
public static void main(String[] args) {
SpringApplication.run(Service1Application.class, args);
}
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
}
ControllerクラスにService2,3にリクエストしてレスポンスを返却するロジックを書きます。
@RestController
public class Service1Controller {
private RestTemplate restTemplate;
private ObjectMapper mapper;
public Service1Controller(final RestTemplate restTemplate, final ObjectMapper mapper) {
this.restTemplate = restTemplate;
this.mapper = mapper;
}
@GetMapping("/hello/{id}")
public String hello(@PathVariable("id") final String id) {
try {
final ResponseEntity<String> res = restTemplate.getForEntity("http://localhost:8082/users/" + id, String.class);
final String date = restTemplate.getForEntity("http://localhost:8083/date/", String.class).getBody();
final String time = restTemplate.getForEntity("http://localhost:8083/time/", String.class).getBody();
return "Hello " + mapper.readTree(res.getBody()).get("name") + " @ " + date + " " + time;
} catch (IOException | HttpClientErrorException e) {
return "Hello World!";
}
}
}
最後にZipkinにログを転送するための設定を追加します。
# port
server.port=8081
# サービスを一意に識別するための名称
spring.zipkin.service.name=service1
# ZipkinサーバのURL(デフォルト:http://localhost:9411)
spring.zipkin.base-url=http://localhost:9411/
# リクエストの何%のログをZipkinサーバに転送するか
spring.sleuth.sampler.percentage=1.0
以上でService1の構築は完了です。
Service2
続いてService2に移ります。
Sevice2はMySQLからデータを取得しレスポンスするだけなので、今回はSpring Data Restで構築していきます。
まずはpom.xmlに以下の依存関係を追加します。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
EntityクラスとRepositoryインタフェースとしてUserクラスとUserRepositoryインタフェースを作成します。
@Getter
@Setter
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
}
public interface UserRepository extends PagingAndSortingRepository<User, Long> {
}
最後にService1と同様にZipkinへのログ転送設定とデータストアの設定を追加します。
server.port=8082
# Zipkin settings
# サービスを一意に識別するための名称
spring.zipkin.service.name=service2
# ZipkinサーバのURL(デフォルト:http://localhost:9411)
spring.zipkin.base-url=http://localhost:9411
# リクエストの何%のログをZipkinサーバに転送するか
spring.sleuth.sampler.percentage=1.0
# spring data settings
spring.datasource.url=jdbc:mysql://192.168.33.10/zipkin?useSSL=false
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.database=MYSQL
以上でService2が完成です。
Service3
続いてService3に移りますが、Service1とほとんど設定は変わらないので割愛します。
DB
最後にDBです。
Vagrantファイルに以下を記載の上、「vagrant up」コマンドで起動してください。
Vagrantファイル内のスクリプトでMySQLのインストールとZipkinに必要なテーブルの作成を行っています。
DDLはこちらで公開されているので確認してみてください。
Vagrant.configure("2") do |config|
config.vm.box = "bento/ubuntu-16.04"
config.vm.network "private_network", ip: "192.168.33.10"
config.vm.provision "shell", inline: <<-SHELL
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade
localectl set-locale LANG=ja_JP.UTF-8 LANGUAGE="ja_JP:ja"
source /etc/default/locale
timedatectl set-timezone Asia/Tokyo
debconf-set-selections <<< 'mysql-server mysql-server/root_password password password'
debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password password'
apt-get install -y mysql-server
mysql -uroot -ppassword -e 'grant all privileges on *.* to root@"%" identified by "password" with grant option;'
mysql -uroot -ppassword -e 'create database zipkin;'
mysql -uroot -ppassword zipkin < /vagrant/zipkin-mysql.sql
mysql -uroot -ppassword zipkin < /vagrant/app-mysql.sql
sed -ie 's/^bind-address/# bind-address/g' /etc/mysql/mysql.conf.d/mysqld.cnf
systemctl restart mysql
SHELL
end
VagrantとVirtualBoxをインストールされていない方は以下からインストールしてみてください。
(注)今回はMySQLを簡易的に使用するためrootユーザをそのまま使用しています。
本番ではしっかりと設定しましょう。
動かしてみる
Zipkinサーバ、Service1-3、DBを起動後、http://localhost:8081/hello/1/ にアクセスしてください。
次のようなレスポンスが表示されるかと思います。
Hello "suzuki" @ 2017-12-20 00:58:55.457
それでは、http://localhost:9411/ にアクセスしてみましょう。
先程、アーキテクチャのUIのところで見た画面が確認できるかと思います。
このケースでは全体の処理に3.712[s]かかっており、その内、Serivce3の「/time」というAPIが3.037[s]かかりボトルネックになっていることがひと目で把握できます。
※ 残念ながら現在「spring-cloud-sleuth」ではjdbcのログ転送はサポートされていないようです。
ただ、braveはjdbcに対応しているようなので、今度試してみたいと思います。
まとめ
今回は分散トレーシングシステムであるZipkinを使って、複数のサービスをまたがるリクエストのトレーシングを行いました。
非常に簡単なケースでしたが、分散トレーシングの便利さを多少なりとも紹介できたのではないかと思います。
今後、分散トレーシングを導入される際に、すこしでも参考になれば幸いです。
それでは、良いお年を~