Spring Cloud Sleuth 整合 Zipkin 進行服務鏈路追蹤

爲何要進行服務鏈路追蹤?

在一個微服務系統架構中,一個完整的請求可能涉及到多個微服務的調用,這個調用形成一個鏈路。

比如,下單的請求,需要經過網關去調用業務服務,業務服務去調用訂單服務,而訂單服務同步調用商品服務和用戶服務,用戶服務又去調用積分服務:

調用鏈路

業務要求整個下單的請求要在 1s 內完成,測試發現請求下單接口耗時超過 2s ,這時我們就需要去定位發現是調用鏈路上的哪個環節耗時異常,進而去解決問題。

Spring Cloud 就有這樣一個組件專門做鏈路追蹤,那就是 Spring  Cloud Sleuth ,有如下功能:

這裏提到的另一個組件 Zipkin 是一個能夠收集所有服務監控數據的跟蹤系統。有了 Zipkin 我們可以直觀的查看調用鏈路,並且可以方便的看出服務之間的調用關係以及調用耗時。

Spring Cloud SleuthZipkin 的使用非常簡單,官網上有很詳細的文檔:

Sleuth :https://spring.io/projects/spring-cloud-sleuth

Zipkin :https://zipkin.io/pages/quickstart.html

下面我們來實操一下。

微服務調用鏈路環境搭建

我們以開篇舉的例子來搭建這樣一個環境:

業務模塊

還是以本 Spring Cloud Alibaba 系列文章的代碼 SpringCloudAlibabaDemo 爲例,目前已有 gatwway-serviceorder-serviceuser-service ,我們再創建兩個微服務項目 product-serviceloyalty-service ,並形成一個調用鏈路。

完整代碼倉庫:https://github.com/ChenDapengJava/SpringCloudAlibabaDemo 。

爲了展示,這裏貼出了調用邏輯上的關鍵代碼。

product-service 查詢商品信息:

@RestController
@RequestMapping("/product")
public class ProductController {
    @GetMapping("/price/{id}")
    public BigDecimal getPrice(@PathVariable("id") Long id) {
        if (id == 1) {
            return new BigDecimal("5899");
        }
        return new BigDecimal("5999");
    }
}

loyalty-service 積分服務中獲取用戶積分和增加積分的 API :

@RestController
@Slf4j
public class LoyaltyController {

    /**
     * 獲取用戶當前積分
     * @param id 用戶id
     */
    @GetMapping("/score/{id}")
    public Integer getScore(@PathVariable("id") Long id) {
        log.info("獲取用戶 id={} 當前積分", id);
        return 1800;
    }

    /**
     * 爲當前用戶增加積分
     * @param id 用戶id
     * @param lastScore 用戶當前積分
     * @param addScore 要增加的積分
     */
    @GetMapping("/addScore")
    public Integer addScore(@RequestParam(value = "id") Long id,
                            @RequestParam(value = "lastScore") Integer lastScore,
                            @RequestParam(value = "addScore") Integer addScore) {
        log.info("用戶 id={} 增加 {} 積分", id, addScore);
        return lastScore + addScore;
    }
}

user-service 通過 OpenFeign 調用積分服務:

FeignClient 類:

@Service
@FeignClient("loyalty-service")
public interface LoyaltyService {

    @GetMapping("/score/{id}")
    Integer getScore(@PathVariable("id") Long id);

    @GetMapping("/addScore")
    Integer addScore(@RequestParam(value = "id") Long id,
                     @RequestParam(value = "lastScore") Integer lastScore,
                     @RequestParam(value = "addScore") Integer addScore);
}

Controller 調用:

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
    
    private LoyaltyService loyaltyService;

    @GetMapping("/score/{id}")
    public Integer getScore(@PathVariable("id") Long id) {
        return loyaltyService.getScore(id);
    }

    @GetMapping("/addScore")
    public Integer addScore(@RequestParam Long id,
                            @RequestParam Integer lastScore,
                            @RequestParam Integer addScore) {
        return loyaltyService.addScore(id, lastScore, addScore);
    }

    @Autowired
    public void setLoyaltyService(LoyaltyService loyaltyService) {
        this.loyaltyService = loyaltyService;
    }
}

order-service 訂單服務通過 OpenFeign 調用 user-serviceproduct-service

FeignClient 類

@Service
@FeignClient("product-service")
public interface ProductService {
    BigDecimal getPrice(@PathVariable("id") Long id);
}
@Service
@FeignClient("user-service")
public interface UserService {
    /**
     * 由於 user-service 使用了統一返回結果,所以此處的返回值是 ResponseResult
     * @param id 用戶id
     * @return ResponseResult<Integer>
     */
    @GetMapping("/user/score/{id}")
    ResponseResult<Integer> getScore(@PathVariable("id") Long id);

    /**
     * 由於 user-service 使用了統一返回結果,所以此處的返回值是 ResponseResult
     */
    @GetMapping("/user/addScore")
    ResponseResult<Integer> addScore(@RequestParam(value = "id") Long id,
                     @RequestParam(value = "lastScore") Integer lastScore,
                     @RequestParam(value = "addScore") Integer addScore);
}

Controller 調用

@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {

    private UserService userService;

    private ProductService productService;

    @GetMapping("/create")
    public String createOrder(@RequestParam("userId") Long userId, @RequestParam("productId") Long productId) {
        log.info("創建訂單參數,userId={}, productId={}", userId, productId);
        // 商品服務-獲取價格
        BigDecimal price = productService.getPrice(productId);
        log.info("獲得 price={}", price);
        // 用戶服務-查詢當前積分,增加積分
        Integer currentScore = userService.getScore(userId).getData();
        log.info("獲得 currentScore={}", price);
        // 增加積分
        Integer addScore = price.intValue();
        Integer finalScore = userService.addScore(userId, currentScore, addScore).getData();
        log.info("下單成功,用戶 id={} 最終積分:{}", userId, finalScore);
        return "下單成功,用戶 id=" + userId + " 最終積分:" + finalScore;
    }

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    @Autowired
    public void setProductService(ProductService productService) {
        this.productService = productService;
    }
}

網關 gateway-service 配置 Nacos 註冊中心和路由:

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.242.112:81
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/order/**

啓動網關以及其他四個服務,

然後可以在 Nacos 中看到註冊進來的實例:

所有服務啓動成功之後,通過網關調用下單 API :

整個調用鏈路沒有問題。

Spring Cloud Sleuth 的使用

要想使用 Sleuth ,只需簡單幾個操作即可。

除了 gateway-service 網關服務,其他四個服務均執行以下步驟:

1, 導入 spring-cloud-starter-sleuth 依賴:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

2, org.springframework.web.servlet.DispatcherServlet 日誌級別調整爲 DEBUG

logging:
  level:
    org.springframework.web.servlet.DispatcherServlet: DEBUG

然後重啓這四個服務,再次通過網關訪問下單 API ,看到每個服務都打印了這樣的日誌:

user-service

product-service

loyalty-service

order-service

這樣形式的日誌:

# [服務名,總鏈路ID,子鏈路ID]
[order-service,5eda5d7bdcca0118,5eda5d7bdcca0118]

就是整體的一個調用鏈路信息。

Zipkin 服務部署與使用

部署 Zipkin 服務

簡單來說, Zipkin 是用來圖形化展示 Sleuth 收集來的信息的。

Zipkin 需要單獨安裝,它是一個 Java 編寫的 Web 項目,我們使用 Docker Compose 進行部署安裝 Zipkin

Tip: 我已經非常體貼的把 Docker Compose 的使用分享了,詳見:用 docker-compose 部署服務真是好用,根本停不下來! 。

部署步驟:

1, 創建 /usr/local/zipkin 目錄,進入到該目錄:

mkdir /usr/local/zipkin
cd /usr/local/zipkin

2, 創建 docker-compose.yml 文件,文件內容如下:

version: "3"
services:
  zipkin:
   image: openzipkin/zipkin
   restart: always
   container_name: zipkin
   ports:
     - 9411:9411

這是簡化版的 docker-compose.yml 配置文件,這樣的配置就能啓動 Zipkin 。更多的配置詳見:https://github.com/openzipkin-attic/docker-zipkin/blob/master/docker-compose.yml 。

3, 使用 docker-compose up -d 命令(-d 表示後臺啓動)啓動:

部署成功後,訪問 Zipkin ,端口爲 9411 ,訪問地址:http://192.168.242.112:9411/zipkin/

這樣,一個 Zipkin 服務就部署完成了。

將 Sleuth 收集到的日誌信息發送到 Zipkin

首先,還是需要在微服務項目中導入 spring-cloud-sleuth-zipkin 的依賴:

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

然後,增加一些配置,讓 Sleuth 收集的信息發送到 Zipkin 服務上:

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.242.112:81
  sleuth:
    enabled: true
    sampler:
      # 設置 Sleuth 收集信息的百分比,一般情況下,10%就夠用了,這裏設置100%觀察
      rate: 100
  zipkin:
    sender:
      type: web
    base-url: http://192.168.242.112:9411/

好了,再來啓動每個服務,然後訪問下單接口再看下 Zipkin 的面板。訪問 http://192.168.242.112:9411/zipkin/

可以看到有一個請求出來了,點擊 SHOW 查看詳情:

可以清楚地看到調用鏈路上每一步的耗時。

小結

Spring Cloud Sleuth 結合 Zipkin 可以對每個微服務進行鏈路追蹤,從而幫助我們分析服務間調用關係以及調用耗費的時間。

本文只簡單介紹了通過 web 方式(配置項:spring.zipkin.sender.type=web):

也就是通過 HTTP 的方式發送數據到 Zipkin ,如果請求量比較大,這種方式其實性能是比較低的,一般情況下我們都是通過消息中間件來發送,比如 RabbitMQ

如果日誌數據量比較大,一般推薦擁有更高吞吐量的 Kafka 來進行日誌推送。

這種方式就是讓服務將 Sleuth 收集的日誌推給 MQ ,讓 Zipkin 去監控 MQ 的信息,通過 MQ 的隊列獲取到服務的信息。這樣就提高了性能。

而日誌的存儲則可以採用 Elasticsearch 對數據進行持久化,這樣可以保證 Zipkin 重啓後,鏈路信息不會丟失。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/SIpk1aAnJHoFbK1FxaPmuQ