SpringBoot
zipkin
JavaDay 22

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

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

はじめに

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

分散トレーシングとは

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

分散トレーシングの用語

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

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

イメージは以下のようになります。
span_trace_diagram.png

Zipkin

それでは本題の「Zipkin」についてです。
ZipkinはGoogleのDapperを参考にTwitter社によって開発されたOSSの分散トレーシングシステムになります。
Zipkinでは、各サービス間のAPIコールのデータを収集する機能と、そのデータを可視化するためのUIを提供します。
Zipkinの公式サイトはこちらになります。

Zipkinのアーキテクチャ

続いてZipkinのアーキテクチャについてです。
まずは公式サイトにのっている全体アーキテクチャを見てみます。
zipkin-architecture.png

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, Elastic Searchがサポートされているようです。
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

Drill Down

「Drill Down」は1Traceに含まれる全Spanとその処理時間を可視化した画面になります。
zipkin_ui2.PNG

Dependencies

「Dependencies」はログ情報から各サービスの依存関係を可視化した画面になります。
より依存関係が強い(多くのリクエストが発生している)ものは矢印が太くなります。
zipkin_ui3.PNG

使ってみる

それでは実際にZipkinを使ってみます。
今回はSpring Bootを使って複数のサービスを構築し、それぞれのログをZipkinサーバに転送します。
Reporterのinstrumented librariesにはspring-cloud-sleuthを使用します。
構成は次のようになります。
zipkin_microservice_diagram.png

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

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

Zipkinサーバの構築

まずは、Zipkinサーバを構築します。
ZipkinサーバはSpring Bootで作成されており、Quickstartで紹介されているように「java -jar」で起動することができます。しかし、今回はせっかくなので違った方法で構築したいと思います。
pom.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」のアノテーションを追加します。

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

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

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

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

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

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にリクエストしてレスポンスを返却するロジックを書きます。

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

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で構築していきます。
まずはpom.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インタフェースを作成します。

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

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

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

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

application.properties
server.port=8083

# Zipkin settings
# サービスを一意に識別するための名称
spring.zipkin.service.name=service3
# 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
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]かかりボトルネックになっていることがひと目で把握できます。
zipkin_operation.PNG

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

まとめ

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

それでは、良いお年を~