Skip to main content

Distributed Tracing với Zipkin + Kafka + Docker + Spring Boot: Tracing - Hành trình trở thành Sherlock Dev

00:07:38:39

Hành trình trở thành một thám tử chuyên nghiệp của một developer.

Mời bạn tham khảo bài viết trước về lý thuyết tracing: Distributed Tracing Concepts

Lưu ý: Mình dùng Spring Boot 3 nhé ^.^ Nếu các bạn dùng Spring Boot 2 có thể tham khảo thư viện Spring Cloud Sleuth

Ok, vậy là bạn có một ứng dụng microservice thật lớn, trong đó bạn có nhiều service cộng tác với nhau để đạt được một điều gì đó. Nó thật tuyệt vời!

Microservices sẽ giống như: "Con bé đó dễ thương ❤️".

Tuy nhiên, việc gỡ lỗi và khắc phục sự cố trở nên ngày càng khó khăn khi quy mô của ứng dụng ngày càng lớn:

  • multiple services: nhiều service và mỗi service làm một công việc độc lập với nhau
  • multiple instance per service: một service có nhiều phiên bản, các dịch vụ thì đều không có trạng thái và có thể mở rộng theo chiều ngang.
  • và đôi khi bạn còn không có quyền truy cập vào máy chủ, node để lấy log xem ứng dụng của chúng ta gặp lỗi gì, lỗi ở đâu.

"Con bé đó dễ thương ❤️" -> "Mắt nó đẹp nhưng biếc. Nội sợ sau này nó sẽ khổ 💘".

Không có gì sai với những hạn chế trên - trên thực tế, những vấn đề trên là không thể tránh khỏi, đặc biệt là khi chúng chạy trong môi trường PaaS (Platform as a service).

Vậy làm sao để khiến mọi thứ trở nên dễ dàng và dễ quản lý hơn khi nói đến khả năng hiển thị tất cả những thông tin chuyên sâu ở tầng ứng dụng?

Không có viên đạn bạc ("silver bullet") nào như vậy? Nhưng chúng ta có những công cụ có thể giúp chúng ta quản lý một phần nào đó.

Blog này trình bày về các ứng dụng Spring Boot tận dụng thư viện Micrometer Tracing để theo dõi các yêu cầu/ giao dịch (transaction) ở cấp ứng dụng và gửi thông tin theo dõi đến máy chủ Zipkin từ xa thông qua Kafka.

Tất cả được triển khai trên Docker

Mặc dù mã nguồn được triển khai trên Java, tuy nhiên những khái niệm ở đây đều có thể áp dụng đối với bất kỳ hệ thống nào có thể tạo ra dữ liệu theo dõi cùng định dạng OpenZipkin.

Đọc thêm về Zipkin tại: https://github.com/openzipkin/zipkin

Kiến trúc sử dụng trong bài viết

zipkin-kafka-docker

Rất dễ hiểu đối với kiến trúc trên, nhờ có micrometer-tracing, các service Spring Boot riêng lẻ sẽ gửi dữ liệu span/ trace tới Kafka. Sau đó Zipkin sẽ sử dụng lib collector-kafka để nhận message từ kafka mỗi 1s sau đó tổng hợp và lưu vào mysql. Dữ liệu sẽ được trực quan hóa trên Zipkin Dashboard.

Build & Deployment

Các thành phần Docker được tạo theo file docker-compose sau: kafka-compose.yml

dockerfile
version: '2'
services:
  zookeeper-1:
    image: confluentinc/cp-zookeeper:latest
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000
    ports:
      - 22181:2181

  kafka-1:
    container_name: kafka1
    image: confluentinc/cp-kafka:latest
    depends_on:
      - zookeeper-1

    ports:
      - 29092:29092
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper-1:2181
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: EXTERNAL:PLAINTEXT,INTERNAL:PLAINTEXT
      KAFKA_ADVERTISED_LISTENERS: EXTERNAL://localhost:29092,INTERNAL://kafka1:9092
      KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
  kafka-2:
    container_name: kafka2
    image: confluentinc/cp-kafka:latest
    depends_on:
      - zookeeper-1
    ports:
      - 29093:29093
    environment:
      KAFKA_BROKER_ID: 2
      KAFKA_ZOOKEEPER_CONNECT: zookeeper-1:2181
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: EXTERNAL:PLAINTEXT,INTERNAL:PLAINTEXT
      KAFKA_ADVERTISED_LISTENERS: EXTERNAL://localhost:29093,INTERNAL://kafka2:9092
      KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
  kafdrop:
    image: obsidiandynamics/kafdrop
    restart: "no"
    environment:
      KAFKA_BROKERCONNECT: "kafka2:9092,kafka1:9092"
    ports:
      - "9002:9000"
    depends_on:
      - kafka-1
      - kafka-2
  zipkin:
    image: openzipkin/zipkin
    container_name: zipkin
    environment:
      - STORAGE_TYPE=mysql
      # Point the zipkin at the storage backend
      - MYSQL_HOST=host.docker.internal
      - MYSQL_USER=review
      - MYSQL_PASS=Techlens@321
      - MYSQL_DB=zipkin
      - MYSQL_TCP_PORT=3306
      - MYSQL_MAX_CONNECTIONS=10
      # Uncomment to enable scribe
      # - SCRIBE_ENABLED=true
      # Uncomment to enable self-tracing
      # - SELF_TRACING_ENABLED=true
      # Uncomment to enable debug logging
      - JAVA_OPTS=-Dlogging.level.zipkin2=DEBUG -Dlogging.level.org.apache.kafka=DEBUG
      - KAFKA_BOOTSTRAP_SERVERS=kafka-1:9092
      - KAFKA_TOPIC=zipkin
    ports:
      # Port used for the Zipkin UI and HTTP Api
      - 9411:9411
    depends_on:
      - kafka-1
      - kafka-2

Bạn có thể chạy docker-compose theo lệnh

shell
docker-compose -f .\kafka-compose.yml  up -d
shell
docker-compose -f .\kafka-compose.yml ps

Tạo topic cho server zipkin

shell
docker-compose -f .\kafka-compose.yml exec kafka-1 kafka-topics.sh --create --topic zipkin --consumer-property group.id=zipkin --partitions 2 --replication-factor 2 --bootstrap-server kafka-1:9092

Sau khi tạo xong ta sẽ được kết quả như sau:

docker-zipkin-rlt

Dưới đây là bản tóm tắt các thành phần trong hệ thống

Zipkin

Zipkin là một dự án bắt nguồn từ Twitter năm 2010 dựa trên các bài báo về tracing của Google Dapper.

  • MYSQL_HOST=host.docker.internal: với host.docker.internal chúng ta sẽ truy cập được localhost của Linux/ Window để kết nối đến Mariadb
  • Các thông số MYSQL khác được cấu hình khá dễ hiểu nên mình không giải thích lại
  • Trước khi chạy zipkin các bạn phải tạo bảng trong database MariaDB theo script sau MysSQL + Zipkin

Kafka

Ở phiên bản docker-compose này mình chạy 2 node kafka + 1 zookeeper để giả định việc kết nối nhiều node cho hệ thống microservice và zipkin.

Kafdrop

Được dùng để quản lý kafka và theo dõi trực quan các bản ghi được gửi từ các service Spring Boot.

Dựng một service phiên bản code thiếu nhi với micrometer-tracing

1. Setup

Mình đã tạo một service demo theo hướng dẫn trong intellij và thêm các thành phần phụ thuộc bên dưới vào pom.xml:

xml
<!-- https://mvnrepository.com/artifact/io.micrometer/micrometer-tracing -->
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-tracing</artifactId>
  <version>1.2.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.kafka/spring-kafka -->
<dependency>
  <groupId>org.springframework.kafka</groupId>
  <artifactId>spring-kafka</artifactId>
  <version>3.1.0</version>
  </dependency>
<!-- https://mvnrepository.com/artifact/io.zipkin.reporter2/zipkin-sender-kafka -->
<dependency>
  <groupId>io.zipkin.reporter2</groupId>
  <artifactId>zipkin-sender-kafka</artifactId>
  <version>2.16.4</version>
</dependency>

2. Configuration

Đầu tiên, hãy thêm các cấu hình này vào trong file application.yml:

Cấu hình kafka:

yml
spring:
  ...

  kafka:
    bootstrap-servers: localhost:29092
    consumer:
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      group-id: zipkin
    producer:
      acks: 0 # gửi không an toàn, gửi không nhận ack
      batch-size: 4
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer

  zipkin:
    kafka:
      topic: zipkin
      group-id: zipkin

Thay thế các thông số kafka cho phù hợp với ứng dụng của bạn

Cấu hình log format zipkin:

yml
logging:
  pattern:
    level: '%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]'

Ở đây có traceId và spanId tương ứng với mỗi đơn vị công việc cơ bản. Bạn có thể xem lại định nghĩa tại vài viết này: Distributed Tracing Concepts

Cấu hình tỉ lệ lấy mẫu tracing (mặc định là 0.1-10% request đổi thành 1.0-100% request)

yml
management:
  tracing:
    sampling:
      probability: 1.0

Tiếp theo mình tạo một sender zipkin dùng để gửi thông tin đến kafka mỗi khi có một request yêu cầu đến server:

java
@Configuration
@EnableConfigurationProperties(KafkaProperties.class)
public class KafkaConfig {

    static String join(List<?> parts) {
        StringBuilder to = new StringBuilder();
        for (int i = 0, length = parts.size(); i < length; i++) {
            to.append(parts.get(i));
            if (i + 1 < length) {
                to.append(',');
            }
        }
        return to.toString();
    }

    @Bean("zipkinSender")
    Sender kafkaSender(KafkaProperties config, Environment environment) {
        // Need to get property value from Environment
        // because when using @VaultPropertySource in reactive web app
        // this bean is initiated before @Value is resolved
        // See gh-1990
        String topic = environment.getProperty("spring.zipkin.kafka.topic", "zipkin");
        String groupId = environment.getProperty("spring.zipkin.kafka.group-id", "zipkin");
        Map<String, Object> properties = new HashMap<>();
        properties.put("key.serializer", ByteArraySerializer.class);
        properties.put("value.serializer", ByteArraySerializer.class);
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
        // Kafka expects the input to be a String, but KafkaProperties returns a list
        List<String> bootstrapServers = config.getBootstrapServers();
            properties.put("bootstrap.servers", join(bootstrapServers));
        return KafkaSender.newBuilder().topic(topic).overrides(properties).build();
    }
}

Bean mặc định của zipkin là zipkinSender, mình sẽ ghi đè nó. KafkaSender là class trong thư viện zipkin-sender-kafka Code trên thì đơn giản rồi, mình sẽ không giải thích ở đây.

Cuối cùng, mình sẽ tạo 1 api để kiểm tra xem micrometer-tracing có hoạt động hay không.

java
@RestController
public class PingController {

    Logger logger = LoggerFactory.getLogger(PingController.class);
    @GetMapping("/ping")
    public String ping() {
        logger.info("pong");
        return "pong";
    }
}

Kết quả

Khi gửi một request đến backend service

curl --location 'localhost:6000/techlens/ping'

Chúng ta sẽ có log như sau:

zipkin-kafka-docker

Ở đây chúng ta có log:

...[tracing-example,656c52a30c63df1831e9d48fe1f1b9c5,31e9d48fe1f1b9c5]...

TraceId: 656c52a30c63df1831e9d48fe1f1b9c5

SpanId: 31e9d48fe1f1b9c5

các thông tin này (Span Record) được gửi về kafka và chúng ta có thể xem trên kafdrop:

zipkin-kafka-docker

Sau đó zipkin sẽ thu thập thông tin từ kafka mỗi 1s và lưu vào bảng zipkin_spans trong MariaDB:

zipkin-kafka-dockerzipkin-kafka-docker

Tổng kết

Như vậy, bài viết này mình đã trình bày một cách cơ bản để dựng một hệ thống tracing sử dụng Zipkin để theo dõi và kafka để nhận gửi tín hiệu.

Trong bài viết tiếp theo mình sẽ làm một hệ thống gồm cả frontend và backend để theo dõi và trực quan hóa những gì mà zipkin thu thập được. Ứng dụng này sẽ thể hiện một các trực quan hơn, đẹp hơn và thể hiện rõ ràng hơn mối quan hệ giữa các service, instance per service với nhau.

Hết rồi. Nếu anh em có gì thắc mắc thì gửi cho mình thông qua emal [email protected] này nhé.

Bye!