Jaeger開發入門(java版)

程式設計師欣宸發表於2021-12-17

歡迎訪問我的GitHub

https://github.com/zq2599/blog_demos

內容:所有原創文章分類彙總及配套原始碼,涉及Java、Docker、Kubernetes、DevOPS等;

本篇概覽

  • 前文《分散式呼叫鏈跟蹤工具Jaeger?兩分鐘極速體驗》我們們體驗了Jaeger的基本能力,今天就來編碼實踐,瞭解如何將讓自己的應用整合Jaeger;
  • 本文的目標:今天我們們要在一個分散式系統中部署和使用jaeger,使用方式包括兩種:首先是SDK內建的span,例如web請求、mysql或redis的操作等,這些會自動上報,第二種就是自定義span;
  • 總的來說,今天的實戰步驟如下:
  1. 今天我們們要從零開發一個迷你的分散式系統,該系統架構如下圖所示,可見有兩個web應用:服務提供方<font color="blue">jaeger-service-provider</font>和服務呼叫方<font color="blue">jaeger-service-consumer</font>,再加一個redis:

在這裡插入圖片描述

  1. jaeger-service-consumer收到使用者通過瀏覽器發來的http請求時,會呼叫<font color="blue">jaeger-service-provider</font>提供的web服務,而<font color="blue">jaeger-service-provider</font>又會操作一次redis,整個流程與典型的分散式系統類似
  2. <font color="blue">jaeger-service-consumer</font>和<font color="blue">jaeger-service-provider</font>在響應服務的過程中,都會將本次服務相關的資料上報到jaeger,這樣我們們在jaeger的web頁面就能觀察到客戶的一次請求會經過那些應用,關鍵位置耗時多少,關鍵引數是哪些等等;
  3. 將所有應用製作成映象,再編寫docker-compose.yml檔案整合它們
  4. 執行,驗證

參考文章

jaeger接入套路

  • 先提前總結Spring Cloud應用接入jaeger的套路,以方便您的使用:
  • 新增依賴庫<font color="blue">opentracing-spring-jaeger-cloud-starter</font>,我這裡是3.3.1版本
  • 配置jaeger遠端埠
  • 建立配置類,向spring環境註冊TracerBuilderCustomizer例項
  • 在需要使用自定義span的程式碼中,用@Autowired註解引入Trace,使用它的API定製span
  • 可以建立span,還可以基於已有span建立子span
  • 除了指定span的名字,還能借助Trace的API給span增加標籤(tag)和日誌(log),這些都會在jaeger的web頁面展示出來
  • 以上六步就是常規接入套路,接下來的實戰就是按照此套路進行的

原始碼下載

名稱連結備註
專案主頁https://github.com/zq2599/blo...該專案在GitHub上的主頁
git倉庫地址(https)https://github.com/zq2599/blo...該專案原始碼的倉庫地址,https協議
git倉庫地址(ssh)git@github.com:zq2599/blog_demos.git該專案原始碼的倉庫地址,ssh協議
  • 這個git專案中有多個資料夾,本篇的原始碼在<font color="blue">spring-cloud-tutorials</font>資料夾下,如下圖紅框所示:

在這裡插入圖片描述

  • <font color="blue">spring-cloud-tutorials</font>資料夾下有多個子工程,本篇的程式碼是<font color="red">jaeger-service-consumer</font>和<font color="red">jaeger-service-provider</font>,如下圖紅框所示:

在這裡插入圖片描述

建立web工程之一:jaeger-service-provider

  • 為了方便管理依賴庫版本,<font color="blue">jaeger-service-provider</font>工程是作為spring-cloud-tutorials的子工程建立的,其pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring-cloud-tutorials</artifactId>
        <groupId>com.bolingcavalry</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>jaeger-service-provider</artifactId>

    <dependencies>

        <dependency>
            <groupId>com.bolingcavalry</groupId>
            <artifactId>common</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>io.opentracing.contrib</groupId>
            <artifactId>opentracing-spring-jaeger-cloud-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <layers>
                        <enabled>true</enabled>
                    </layers>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  • 配置檔案application.yml,注意由於後面會用到docker-compose,因此redis和jaeger的地址都無需填寫具體的IP,只要填寫它們的容器名即可:
spring:
  application:
    name: jaeger-service-provider
  redis:
    database: 0
    # Redis伺服器地址 寫你的ip
    host: redis
    # Redis伺服器連線埠
    port: 6379
    # Redis伺服器連線密碼(預設為空)
    password:
    # 連線池最大連線數(使用負值表示沒有限制  類似於mysql的連線池
    jedis:
      pool:
        max-active: 10
        # 連線池最大阻塞等待時間(使用負值表示沒有限制) 表示連線池的連結拿完了 現在去申請需要等待的時間
        max-wait: -1
        # 連線池中的最大空閒連線
        max-idle: 10
        # 連線池中的最小空閒連線
        min-idle: 0
    # 連線超時時間(毫秒) 去連結redis服務端
    timeout: 6000

opentracing:
  jaeger:
    enabled: true
    udp-sender:
      host: jaeger
      port: 6831
  • 配置類:
package com.bolingcavalry.jaeger.provider.config;

import io.jaegertracing.internal.MDCScopeManager;
import io.opentracing.contrib.java.spring.jaeger.starter.TracerBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JaegerConfig {
    @Bean
    public TracerBuilderCustomizer mdcBuilderCustomizer() {
        // 1.8新特性,函式式介面
        return builder -> builder.withScopeManager(new MDCScopeManager.Builder().build());
    }
}
  • 另外,由於本篇的重點是jaeger,因此redis相關程式碼就不貼出來了,有需要的讀者請在此檢視:RedisConfig.javaRedisUtils.java
  • 接下來看看如何使用Trace的例項來定製span,下面是定了span及其子span的web介面類,請注意trace的API的使用,程式碼中已有詳細註釋,就不多贅述了:
package com.bolingcavalry.jaeger.provider.controller;

import com.bolingcavalry.common.Constants;
import com.bolingcavalry.jaeger.provider.util.RedisUtils;
import io.opentracing.Span;
import io.opentracing.Tracer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.text.SimpleDateFormat;
import java.util.Date;

@RestController
@Slf4j
public class HelloController {

    @Autowired
    private Tracer tracer;

    @Autowired
    private RedisUtils redisUtils;

    private String dateStr(){
        return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date());
    }

    /**
     * 模擬業務執行,耗時100毫秒
     * @param parentSpan
     */
    private void mockBiz(Span parentSpan) {
        // 基於指定span,建立其子span
        Span span = tracer.buildSpan("mockBizChild").asChildOf(parentSpan).start();

        log.info("hello");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        span.finish();
    }

    /**
     * 返回字串型別
     * @return
     */
    @GetMapping("/hello")
    public String hello() {
        long startTime = System.currentTimeMillis();

        // 生成當前時間
        String timeStr = dateStr();

        // 建立一個span,在建立的時候就新增一個tag
        Span span = tracer.buildSpan("mockBiz")
                    .withTag("time-str", timeStr)
                    .start();

        // span日誌
        span.log("normal span log");

        // 模擬一個耗時100毫秒的業務
        mockBiz(span);

        // 增加一個tag
        span.setTag("tiem-used", System.currentTimeMillis()-startTime);

        // span結束
        span.finish();

        // 寫入redis
        redisUtils.set("Hello",  timeStr);
        // 返回
        return Constants.HELLO_PREFIX + ", " + timeStr;
    }
}
  • 編碼已經結束,接下來要將此工程製作成docker映象了,新建Dockerfile檔案,和pom.xml在同一個目錄下:
# 指定基礎映象,這是分階段構建的前期階段
FROM openjdk:8-jdk-alpine as builder

# 設定時區
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo 'Asia/Shanghai' >/etc/timezone

# 執行工作目錄
WORKDIR application
# 配置引數
ARG JAR_FILE=target/*.jar
# 將編譯構建得到的jar檔案複製到映象空間中
COPY ${JAR_FILE} application.jar
# 通過工具spring-boot-jarmode-layertools從application.jar中提取拆分後的構建結果
RUN java -Djarmode=layertools -jar application.jar extract

# 正式構建映象
FROM openjdk:8-jdk-alpine
WORKDIR application
# 前一階段從jar中提取除了多個檔案,這裡分別執行COPY命令複製到映象空間中,每次COPY都是一個layer
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
  • 先在父工程<font color="blue">spring-cloud-tutorials</font>的pom.xml所在目錄執行以下命令完成編譯構建:
mvn clean package -U -DskipTests
  • 再在Dockerfile所在目錄執行以下命令製作docker映象:
docker build -t bolingcavalry/jaeger-service-provider:0.0.1 .
  • 至此,<font color="blue">jaeger-service-provider</font>相關開發已經完成

建立web工程之二:jaeger-service-consumer

  • jaeger-service-consumer工程的建立過程和jaeger-service-provider如出一轍,甚至還要更簡單一些(不操作redis),所以描述其開發過程的內容儘量簡化,以節省篇幅
  • pom.xml相比jaeger-service-provider的,少了redis依賴,其他可以照抄
  • application.yml也少了redis:
spring:
  application:
    name: jaeger-service-consumer
opentracing:
  jaeger:
    enabled: true
    udp-sender:
      host: jaeger
      port: 6831
  • 配置類JaegerConfig.java可以照抄jaeger-service-provider的
  • 由於要遠端呼叫jaeger-service-provider的web介面,因此新增restTemplate的配置類:
package com.bolingcavalry.jaeger.consumer.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
        RestTemplate restTemplate = new RestTemplate(factory);
        return restTemplate;
    }

    @Bean
    public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setReadTimeout(5000);
        factory.setConnectTimeout(15000);
        return factory;
    }
}
  • 關鍵程式碼是web介面的實現,會通過restTemplate呼叫jaeger-service-provider的介面:
package com.bolingcavalry.jaeger.consumer.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@Slf4j
public class HelloConsumerController {

    @Autowired
    RestTemplate restTemplate;

    /**
     * 返回字串型別
     * @return
     */
    @GetMapping("/hello")
    public String hello() {
        String url = "http://jaeger-service-provider:8080/hello";
        ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class);
        StringBuffer sb = new StringBuffer();
        HttpStatus statusCode = responseEntity.getStatusCode();
        String body = responseEntity.getBody();

        // 返回
        return "response from jaeger-service-provider \nstatus : " + statusCode + "\nbody : " + body;
    }
}
  • 接下來是編譯構建制作docker映象,和前面的jaeger-service-provider一樣;

docker-compose.yml檔案編寫

  • 現在我們們要將所有服務都執行起來了,先盤點一共有哪些服務要在docker-compose中啟動的,如下所示,共計四個:
  • jaeger
  • redis
  • jaeger-service-provider
  • jaeger-service-consumer
  • 完整的docker-compose.yml內容如下:
version: '3.0'

networks:
  jaeger-tutorials-net:
    driver: bridge
    ipam:
      config:
        - subnet: 192.168.1.0/24
          gateway: 192.168.1.1

services:
  jaeger:
    image: jaegertracing/all-in-one:1.26
    container_name: jaeger
    # 處理時鐘漂移帶來的計算出負數的問題
    command: ["--query.max-clock-skew-adjustment=100ms"]
    #選擇網路
    networks:
      - jaeger-tutorials-net
    #選擇埠
    ports:
      - 16686:16686/tcp
    restart: always
  redis:
    image: redis:6.2.5
    container_name: redis
    #選擇網路
    networks:
      - jaeger-tutorials-net
    restart: always
  jaeger-service-provider:
    image: bolingcavalry/jaeger-service-provider:0.0.1
    container_name: jaeger-service-provider
    #選擇網路
    networks:
      - jaeger-tutorials-net
    restart: always
  jaeger-service-consumer:
    image: bolingcavalry/jaeger-service-consumer:0.0.1
    container_name: jaeger-consumer-provider
    #選擇埠
    ports:
      - 18080:8080/tcp
    #選擇網路
    networks:
      - jaeger-tutorials-net
    restart: always
  • 至此,開發工作已全部完成,開始驗證

驗證

  • 在docker-compose.yml所在目錄執行命令<font color="blue">docker-compose up -d</font>,即可啟動所有容器:
will$ docker-compose up -d
Creating network "jaeger-service-provider_jaeger-tutorials-net" with driver "bridge"
Creating jaeger-service-provider  ... done
Creating jaeger                   ... done
Creating redis                    ... done
Creating jaeger-consumer-provider ... done

在這裡插入圖片描述

  • 呼叫<font color="blue">jaeger-service-consumer</font>的web服務,瀏覽器訪問<font color="blue">http://localhost:18080/hello</font>:

在這裡插入圖片描述

  • 再去jaeger上可以看到上述訪問的追蹤詳情:

在這裡插入圖片描述

  • 點選上圖紅框3,可以展開此trace的所有span詳情,如下圖,紅框中是我們們程式中自定義的span,綠框中的全是SDK自帶的span,而藍框中是redis的span的tag,該tag的值就是本次寫redis操作的key,藉助tag可以在定位問題的時候提供關鍵線索:

在這裡插入圖片描述

  • 點開上圖紅框中的自定義span,如下圖所示,tag和log都和程式碼對應上了:

在這裡插入圖片描述

  • 至此,Spring Cloud應用接入和使用Jaeger的基本操作就全部完成了,希望如果您正在接入Jaeger,希望本文能給您一些參考,接下來的文章,我們們會繼續深入學習Jaeger,瞭解它的更多特性;

你不孤單,欣宸原創一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 資料庫+中介軟體系列
  6. DevOps系列

歡迎關注公眾號:程式設計師欣宸

微信搜尋「程式設計師欣宸」,我是欣宸,期待與您一同暢遊Java世界...
https://github.com/zq2599/blog_demos

相關文章