分散トレーシングシステムのZipkinを使ってみた話

Java Advent Calendarの22日目担当のmiya10keiです。
今回は分散トレーシングシステムであるZipkinを勉強しながら使ってみた話になります。

# はじめに
まずはじめに、分散トレーシングについて完全に素人だったのでこの辺のスライドやブログを見てみました。

- https://www.slideshare.net/rakutentech/spring-cloudzipkin
- http://www.baeldung.com/tracing-services-with-zipkin

# 分散トレーシングとは
そもそも、分散トレーシングとはなにかという話ですが、分散されたシステムで処理されるリクエストを追跡（トレーシング）するためのものになります。マイクロサービスのような複数のサービスで構成されるシステム（分散システム）では、１つのリクエストが複数のサービスをまたいで処理されることにより、リクエスト全体の処理の流れを把握する（トレーサビリティ）ことが難しくなります。また、トレーサビリティが低下することによって障害やリクエストのレイテンシーが悪化した際に、どのサービスに問題があるのかを見つけることも難しくなります。このような問題をサービスの依存関係やサービス単位のレイテンシーを可視化することによって解決の手助けをするシステムを分散トレーシングシステムといいます。

# 分散トレーシングの用語
分散トレーシングの用語として次の2つがあります。

- **Span**: 1サービス内の処理を表す。
- **Trace**: Requestのstart-endを含むSpanの集合をあらわす。

イメージは以下のようになります。
![span_trace_diagram.png](https://qiita-image-store.s3.amazonaws.com/0/76758/04e7cf73-051b-0ade-877f-cd3072135b08.png)

# Zipkin
それでは本題の「Zipkin」についてです。
ZipkinはGoogleの[Dapper](https://static.googleusercontent.com/media/research.google.com/ja//pubs/archive/36356.pdf)を参考にTwitter社によって開発されたOSSの分散トレーシングシステムになります。
Zipkinでは、各サービス間のAPIコールのデータを収集する機能と、そのデータを可視化するためのUIを提供します。
Zipkinの公式サイトは[こちら](https://zipkin.io/)になります。

# Zipkinのアーキテクチャ
続いてZipkinのアーキテクチャについてです。
まずは[公式サイト](https://zipkin.io/pages/architecture.html)にのっている全体アーキテクチャを見てみます。
![zipkin-architecture.png](https://qiita-image-store.s3.amazonaws.com/0/76758/e6210afd-a70c-3e14-8bfc-d249a99dfcfe.png)

Zipkinでは大きく以下の5つのコンポーネントに分かれるようです。

## Reporter
ReporterはアプリからZipkinサーバにログを転送する役割を担います。
instrumented librariesが各言語用にいくつか提供されています。サポート言語については[こちら](https://zipkin.io/pages/existing_instrumentations.html)を参照ください。

## 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」はサービス名や期間、処理時間からログを検索する画面になります。
![zipkin_ui1.PNG](https://qiita-image-store.s3.amazonaws.com/0/76758/057ca8bc-9886-e155-2749-6d2f2b84f60d.png)

### Drill Down
「Drill Down」は1Traceに含まれる全Spanとその処理時間を可視化した画面になります。
![zipkin_ui2.PNG](https://qiita-image-store.s3.amazonaws.com/0/76758/9e300322-be4a-a7ac-b872-988fa6abac0f.png)

### Dependencies
「Dependencies」はログ情報から各サービスの依存関係を可視化した画面になります。
より依存関係が強い（多くのリクエストが発生している）ものは矢印が太くなります。
![zipkin_ui3.PNG](https://qiita-image-store.s3.amazonaws.com/0/76758/34b5229f-8303-791d-bf01-8b27ccb6039b.png)

# 使ってみる
それでは実際にZipkinを使ってみます。
今回はSpring Bootを使って複数のサービスを構築し、それぞれのログをZipkinサーバに転送します。
Reporterのinstrumented librariesには[spring-cloud-sleuth](https://github.com/spring-cloud/spring-cloud-sleuth)を使用します。
構成は次のようになります。
![zipkin_microservice_diagram.png](https://qiita-image-store.s3.amazonaws.com/0/76758/67ba8325-49bc-f125-a098-afdf70a3abe6.png)


※ DBはVagrant + VirtualBox + Ubuntu 16.04 上にMySQLをインストールして使用します。
また、Zipkinサーバに転送したログも同様のMySQLに永続化することにします。

今回使用したソースは以下になります。
https://github.com/miya10kei/spring-cloud-sleuth_zipkin

## Zipkinサーバの構築
まずは、Zipkinサーバを構築します。
ZipkinサーバはSpring Bootで作成されており、[Quickstart](https://zipkin.io/pages/quickstart.html)で紹介されているように「java -jar」で起動することができます。しかし、今回はせっかくなので違った方法で構築したいと思います。
pom.xmlに次の依存関係を追加します。

```xml: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」のアノテーションを追加します。

```java:SpringApplication.java
@SpringBootApplication
@EnableZipkinServer  // Zipkinサーバを有効化するアノテーションを追加
public class ZipkinServerApplication {
	public static void main(String[] args) {
		SpringApplication.run(ZipkinServerApplication.class, args);
	}
}
```

今回はデータストアにMySQLを使用するのでその設定を追加します。

```properties:application.properties
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に以下の依存関係を追加します。

```xml:pom.xml
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
```

SpringApplicationクラスにService2,3にHTTPでリクエストするために必要なRestTemplateをBeanとして定義します。

```java:SpringApplication.java
@SpringBootApplication
public class Service1Application {

	public static void main(String[] args) {
		SpringApplication.run(Service1Application.class, args);
	}

	@Bean
    RestTemplate restTemplate() {
	    return new RestTemplate();
    }
}
```

ControllerクラスにService2,3にリクエストしてレスポンスを返却するロジックを書きます。

```java:Controller.java
@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にログを転送するための設定を追加します。

```properties:application.properties
# 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](https://projects.spring.io/spring-data-rest/)で構築していきます。
まずはpom.xmlに以下の依存関係を追加します。

```xml: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インタフェースを作成します。

```java:User.java
@Getter
@Setter
@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;
}
```
```java:UserRepository.java
public interface UserRepository extends PagingAndSortingRepository<User, Long> {
}
```

最後にService1と同様にZipkinへのログ転送設定とデータストアの設定を追加します。

```properties:application.properties
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は[こちら](https://github.com/openzipkin/zipkin/blob/master/zipkin-storage/mysql/src/main/resources/mysql.sql)で公開されているので確認してみてください。

```:Vagrant
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をインストールされていない方は以下からインストールしてみてください。

- https://www.vagrantup.com/
- https://www.virtualbox.org/


（注）今回は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]かかりボトルネックになっていることがひと目で把握できます。
![zipkin_operation.PNG](https://qiita-image-store.s3.amazonaws.com/0/76758/b5f85b4c-0c64-70cd-5a90-adcd84f8868f.png)

※　残念ながら現在「spring-cloud-sleuth」ではjdbcのログ転送はサポートされていないようです。
ただ、[brave](https://github.com/openzipkin/brave)はjdbcに対応しているようなので、今度試してみたいと思います。

# まとめ
今回は分散トレーシングシステムであるZipkinを使って、複数のサービスをまたがるリクエストのトレーシングを行いました。
非常に簡単なケースでしたが、分散トレーシングの便利さを多少なりとも紹介できたのではないかと思います。
今後、分散トレーシングを導入される際に、すこしでも参考になれば幸いです。

それでは、良いお年を～