歡迎訪問我的GitHub
https://github.com/zq2599/blog_demos
內容:所有原創文章分類彙總及配套原始碼,涉及Java、Docker、Kubernetes、DevOPS等;
本篇概覽
- 前文《分散式呼叫鏈跟蹤工具Jaeger?兩分鐘極速體驗》我們們體驗了Jaeger的基本能力,今天就來編碼實踐,瞭解如何將讓自己的應用整合Jaeger;
- 本文的目標:今天我們們要在一個分散式系統中部署和使用jaeger,使用方式包括兩種:首先是SDK內建的span,例如web請求、mysql或redis的操作等,這些會自動上報,第二種就是自定義span;
- 總的來說,今天的實戰步驟如下:
- 今天我們們要從零開發一個迷你的分散式系統,該系統架構如下圖所示,可見有兩個web應用:服務提供方<font color="blue">jaeger-service-provider</font>和服務呼叫方<font color="blue">jaeger-service-consumer</font>,再加一個redis:
- jaeger-service-consumer收到使用者通過瀏覽器發來的http請求時,會呼叫<font color="blue">jaeger-service-provider</font>提供的web服務,而<font color="blue">jaeger-service-provider</font>又會操作一次redis,整個流程與典型的分散式系統類似
- <font color="blue">jaeger-service-consumer</font>和<font color="blue">jaeger-service-provider</font>在響應服務的過程中,都會將本次服務相關的資料上報到jaeger,這樣我們們在jaeger的web頁面就能觀察到客戶的一次請求會經過那些應用,關鍵位置耗時多少,關鍵引數是哪些等等;
- 將所有應用製作成映象,再編寫docker-compose.yml檔案整合它們
- 執行,驗證
參考文章
- 本文中會將springboot應用製作成docker映象,如果您想了解詳細的製作過程,可以參考以下兩篇文章:
- 《體驗SpringBoot(2.3)應用製作Docker映象(官方方案)》
- 《詳解SpringBoot(2.3)應用製作Docker映象(官方方案)》
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頁面展示出來
- 以上六步就是常規接入套路,接下來的實戰就是按照此套路進行的
原始碼下載
- 本篇實戰中的完整原始碼可在GitHub下載到,地址和連結資訊如下表所示(https://github.com/zq2599/blo...):
名稱 | 連結 | 備註 |
---|---|---|
專案主頁 | 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.java、RedisUtils.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">http://localhost:16686/search</font>,熟悉的jaeger頁面:
- 呼叫<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,瞭解它的更多特性;
你不孤單,欣宸原創一路相伴
歡迎關注公眾號:程式設計師欣宸
微信搜尋「程式設計師欣宸」,我是欣宸,期待與您一同暢遊Java世界...
https://github.com/zq2599/blog_demos