微服務(wù)總結(jié)(下)

六、服務(wù)網(wǎng)關(guān) SpringCloud Gateway

在微服務(wù)架構(gòu)中,一個(gè)系統(tǒng)會(huì)被拆分為很多個(gè)微服務(wù)。那么作為客戶端要如何去調(diào)用這么多的微服務(wù)呢?

如果沒(méi)有網(wǎng)關(guān)的存在,我們只能在客戶端記錄每個(gè)微服務(wù)的地址,然后分別去調(diào)用。

image

這樣的話,會(huì)有很多問(wèn)題:

  • 客戶端多次請(qǐng)求不同的微服務(wù),增加客戶端代碼或配置編寫的復(fù)雜性
  • 認(rèn)證復(fù)雜,每個(gè)服務(wù)都需要獨(dú)立認(rèn)證。
  • 各個(gè)微服務(wù)都存在跨域請(qǐng)求,在一定場(chǎng)景下處理相對(duì)復(fù)雜。

這些問(wèn)題,我們就可以采用網(wǎng)關(guān)來(lái)解決。

所謂的API網(wǎng)關(guān),就是指系統(tǒng)的統(tǒng)一入口,它封裝了應(yīng)用程序的內(nèi)部結(jié)構(gòu),為客戶端提供統(tǒng)一服務(wù),一些與業(yè)務(wù)本身功能無(wú)關(guān)的公共邏輯可以在這里實(shí)現(xiàn),諸如認(rèn)證、鑒權(quán)、監(jiān)控、路由轉(zhuǎn)發(fā)等等。

添加上API網(wǎng)關(guān)之后,系統(tǒng)的架構(gòu)圖變成了如下所示:

image

6.1 目前可以使用的網(wǎng)關(guān)

Ngnix+lua

使用nginx的反向代理和負(fù)載均衡可實(shí)現(xiàn)對(duì)api服務(wù)器的負(fù)載均衡及高可用

lua是一種腳本語(yǔ)言,可以來(lái)編寫一些簡(jiǎn)單的邏輯, nginx支持lua腳本

Kong

基于Nginx+Lua開發(fā),性能高,穩(wěn)定,有多個(gè)可用的插件(限流、鑒權(quán)等等)可以開箱即用。 問(wèn)題:只支持Http協(xié)議;二次開發(fā),自由擴(kuò)展困難;提供管理API,缺乏更易用的管控、配置方式。

Zuul

Netflix開源的網(wǎng)關(guān),功能豐富,使用JAVA開發(fā),易于二次開發(fā)。問(wèn)題:缺乏管控,無(wú)法動(dòng)態(tài)配置;依賴組件較多;處理Http請(qǐng)求依賴的是Web容器,性能不如Nginx。

Spring Cloud Gateway

Spring Cloud Gateway是Spring公司基于Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技術(shù)開發(fā)的網(wǎng)關(guān),它旨在為微服務(wù)架構(gòu)提供一種簡(jiǎn)單有效的統(tǒng)一的 API 路由管理方式。它的目標(biāo)是替代 Netflix Zuul,其不僅提供統(tǒng)一的路由方式,并且基于 Filter 鏈的方式提供了網(wǎng)關(guān)基本的功能,例如:安全,監(jiān)控和限流。

優(yōu)點(diǎn):

  • 性能強(qiáng)勁:是Zuul1.0的1.6倍
  • 功能強(qiáng)大:內(nèi)置了很多實(shí)用的功能,例如轉(zhuǎn)發(fā)、監(jiān)控、限流等
  • 設(shè)計(jì)優(yōu)雅,容易擴(kuò)展

缺點(diǎn):

  • 其實(shí)現(xiàn)依賴Netty與WebFlux,不是傳統(tǒng)的Servlet編程模型,學(xué)習(xí)成本高
  • 不能將其部署在Tomcat、Jetty等Servlet容器里,只能打成jar包執(zhí)行
  • 需要Spring Boot 2.0及以上的版本,才支持

6.3 快速開始

基礎(chǔ)版

1、創(chuàng)建api-gateway模塊

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

2、添加配置文件

server:
  port: 7000
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: product_route
          uri: http://127.0.0.1:8081
          order: 1
          predicates: # 斷言,當(dāng)路由滿足全部斷言時(shí)進(jìn)行轉(zhuǎn)發(fā)
            - Path=/product-serv/**
          filters:  # 過(guò)濾器
            - StripPrefix=1 # 刪除一級(jí)路徑

3、啟動(dòng)網(wǎng)關(guān)和商品微服務(wù),請(qǐng)求 http://127.0.0.1:7000/product-serv/product/1

image

接入注冊(cè)中心nacos

1、引入nacos依賴

<!--nacos客戶端-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

2、修改配置文件

server:
  port: 7000
spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 # 將gateway注冊(cè)到nacos
    gateway:
      discovery:
        locator:
          enabled: true # 讓gateway從nacos中獲取服務(wù)信息
      routes:
        - id: product_route
          uri: lb://service-product
#          uri: http://127.0.0.1:8081
          order: 1
          predicates: # 斷言,當(dāng)路由滿足全部斷言時(shí)進(jìn)行轉(zhuǎn)發(fā)
            - Path=/product-serv/**
          filters:  # 過(guò)濾器
            - StripPrefix=1 # 刪除一級(jí)路徑

3、請(qǐng)求 http://127.0.0.1:7000/product-serv/product/1

6.4 Gateway的基本概念&請(qǐng)求處理流程

基本概念

路由(Route) 是 gateway 中最基本的組件之一,表示一個(gè)具體的路由信息載體。主要定義了下面的幾個(gè)信息:

  • id,路由標(biāo)識(shí)符,區(qū)別于其他 Route。
  • uri,路由指向的目的地 uri,即客戶端請(qǐng)求最終被轉(zhuǎn)發(fā)到的微服務(wù)。
  • order,用于多個(gè) Route 之間的排序,數(shù)值越小排序越靠前,匹配優(yōu)先級(jí)越高。
  • predicate,斷言的作用是進(jìn)行條件判斷,只有斷言都返回真,才會(huì)真正的執(zhí)行路由。 、- filter,過(guò)濾器用于修改請(qǐng)求和響應(yīng)信息。

請(qǐng)求處理流程

image
  1. Gateway Client向Gateway Server發(fā)送請(qǐng)求
  2. 請(qǐng)求首先會(huì)被HttpWebHandlerAdapter進(jìn)行提取組裝成網(wǎng)關(guān)上下文
  3. 然后網(wǎng)關(guān)的上下文會(huì)傳遞到DispatcherHandler,它負(fù)責(zé)將請(qǐng)求分發(fā)給RoutePredicateHandlerMapping
  4. RoutePredicateHandlerMapping負(fù)責(zé)路由查找,并根據(jù)路由斷言判斷路由是否可用
  5. 如果過(guò)斷言成功,由FilteringWebHandler創(chuàng)建過(guò)濾器鏈并調(diào)用
  6. 請(qǐng)求會(huì)一次經(jīng)過(guò)PreFilter--微服務(wù)--PostFilter的方法,最終返回響應(yīng)

6.5 斷言

6.5.1 內(nèi)置斷言工廠

image
基于Datetime類型的斷言工廠

此類型的斷言根據(jù)時(shí)間做判斷,主要有三個(gè):

AfterRoutePredicateFactory: 接收一個(gè)日期參數(shù),判斷請(qǐng)求日期是否晚于指定日期

BeforeRoutePredicateFactory: 接收一個(gè)日期參數(shù),判斷請(qǐng)求日期是否早于指定日期

BetweenRoutePredicateFactory: 接收兩個(gè)日期參數(shù),判斷請(qǐng)求日期是否在指定時(shí)間段內(nèi)

-After=2019-12-31T23:59:59.789+08:00[Asia/Shanghai]
基于遠(yuǎn)程地址的斷言工廠

RemoteAddrRoutePredicateFactory:接收一個(gè)IP地址段,判斷請(qǐng)求主機(jī)地址是否在地址段中

-RemoteAddr=192.168.1.1/24
基于Cookie的斷言工廠

CookieRoutePredicateFactory:接收兩個(gè)參數(shù),cookie 名字和一個(gè)正則表達(dá)式。 判斷請(qǐng)求cookie是否具有給定名稱且值與正則表達(dá)式匹配。

-Cookie=chocolate, ch.
基于Header的斷言工廠

HeaderRoutePredicateFactory:接收兩個(gè)參數(shù),標(biāo)題名稱和正則表達(dá)式。判斷請(qǐng)求Header是否具有給定名稱且值與正則表達(dá)式匹配。

-Header=X-Request-Id, \d+
基于Host的斷言工廠

HostRoutePredicateFactory:接收一個(gè)參數(shù),主機(jī)名模式。判斷請(qǐng)求的Host是否滿足匹配規(guī)則。

-Host=**.testhost.org
基于Method請(qǐng)求方法的斷言工廠

MethodRoutePredicateFactory:接收一個(gè)參數(shù),判斷請(qǐng)求類型是否跟指定的類型匹配。

-Method=GET
基于Path請(qǐng)求路徑的斷言工廠

PathRoutePredicateFactory:接收一個(gè)參數(shù),判斷請(qǐng)求的URI部分是否滿足路徑規(guī)則。

-Path=/foo/{segment}
基于Query請(qǐng)求參數(shù)的斷言工廠

QueryRoutePredicateFactory :接收兩個(gè)參數(shù),請(qǐng)求param和正則表達(dá)式,判斷請(qǐng)求參數(shù)是否具有給定名稱且值與正則表達(dá)式匹配。

-Query=baz, ba.
基于路由權(quán)重的斷言工廠

WeightRoutePredicateFactory:接收一個(gè)[組名,權(quán)重], 然后對(duì)于同一個(gè)組內(nèi)的路由按照權(quán)重轉(zhuǎn)發(fā)

routes:
    -id: weight_route1 
     uri: host1 
     predicates: 
        -Path=/product/**
        -Weight=group3, 1
    -id: weight_route2
     uri: host2
     predicates: 
        -Path=/product/**
        -Weight= group3, 9

6.5.2 自定義斷言工廠

我們來(lái)設(shè)定一個(gè)場(chǎng)景: 假設(shè)我們的應(yīng)用僅僅讓age在(min,max)之間的人來(lái)訪問(wèn)。

1、在配置文件中,添加一個(gè)Age的斷言配置

server:
  port: 7000
spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 # 將gateway注冊(cè)到nacos
    gateway:
      discovery:
        locator:
          enabled: true # 讓gateway從nacos中獲取服務(wù)信息
      routes:
        - id: product_route
          uri: lb://service-product
#          uri: http://127.0.0.1:8081
          order: 1
          predicates: # 斷言,當(dāng)路由滿足全部斷言時(shí)進(jìn)行轉(zhuǎn)發(fā)
            - Path=/product-serv/**
            - Age=18,60 # ********看這里
          filters:  # 過(guò)濾器
            - StripPrefix=1 # 刪除一級(jí)路徑

2、新建一個(gè)斷言工廠,實(shí)現(xiàn)斷言方法

//這是一個(gè)自定義的路由斷言工廠類,要求有兩個(gè)
//1 名字必須是 配置+RoutePredicateFactory
//2 必須繼承AbstractRoutePredicateFactory<配置類>
@Component
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {

    //構(gòu)造函數(shù)
    public AgeRoutePredicateFactory() {
        super(Config.class);
    }

    //讀取配置文件的中參數(shù)值 給他賦值到配置類中的屬性上
    public List<String> shortcutFieldOrder() {
        //這個(gè)位置的順序必須跟配置文件中的值的順序?qū)?yīng)
        return Arrays.asList("minAge", "maxAge");
    }

    //斷言邏輯
    public Predicate<ServerWebExchange> apply(Config config) {
        return new Predicate<ServerWebExchange>() {
            @Override
            public boolean test(ServerWebExchange serverWebExchange) {
                //1 接收前臺(tái)傳入的age參數(shù)
                String ageStr = serverWebExchange.getRequest().getQueryParams().getFirst("age");

                //2 先判斷是否為空
                if (StringUtils.isNotEmpty(ageStr)) {
                    //3 如果不為空,再進(jìn)行路由邏輯判斷
                    int age = Integer.parseInt(ageStr);
                    if (age < config.getMaxAge() && age > config.getMinAge()) {
                        return true;
                    } else {
                        return false;
                    }
                }
                return false;
            }
        };
    }

    //配置類,用于接收配置文件中的對(duì)應(yīng)參數(shù)
    @Data
    @NoArgsConstructor
    public static class Config {
        private int minAge;//18
        private int maxAge;//60
    }
}

3、重啟網(wǎng)關(guān)服務(wù),訪問(wèn):

http://localhost:7000/product-serv/product/1?age=30

http://localhost:7000/product-serv/product/1?age=10

6.6 過(guò)濾器

作用:在請(qǐng)求的傳遞過(guò)程中,對(duì)請(qǐng)求和響應(yīng)做一些手腳。

在Gateway中,F(xiàn)ilter的生命周期只有兩個(gè):

  1. PRE:這種過(guò)濾器在請(qǐng)求被路由之前調(diào)用。我們可利用這種過(guò)濾器實(shí)現(xiàn)身份驗(yàn)證、在集群中選擇請(qǐng)求的微服務(wù)、記錄調(diào)試信息等。
  2. POST:這種過(guò)濾器在路由到微服務(wù)以后執(zhí)行。這種過(guò)濾器可用來(lái)為響應(yīng)添加標(biāo)準(zhǔn)的HTTP Header、收集統(tǒng)計(jì)信息和指標(biāo)、將響應(yīng)從微服務(wù)發(fā)送給客戶端等。

Gateway 的Filter從作用范圍可分為兩種:

  1. GatewayFilter:應(yīng)用到單個(gè)路由或者一個(gè)分組的路由上。
  2. GlobalFilter:應(yīng)用到所有的路由上。

6.6.1 局部過(guò)濾器

局部過(guò)濾器是針對(duì)單個(gè)路由的過(guò)濾器。

內(nèi)置局部過(guò)濾器
過(guò)濾器工廠 作用 參數(shù)
AddRequestHeader 為原始請(qǐng)求添加Header Header的名稱及值
AddRequestParameter 為原始請(qǐng)求添加請(qǐng)求參數(shù) 參數(shù)名稱及值
AddResponseHeader 為原始響應(yīng)添加Header Header的名稱及值
DedupeResponseHeader 剔除響應(yīng)頭中重復(fù)的值 需要去重的Header名稱及去重策略
Hystrix 為路由引入Hystrix的斷路器保護(hù) HystrixCommand的名稱
FallbackHeaders 為fallbackUri的請(qǐng)求頭中添加具體的異常信息 Header的名稱
PrefixPath 為原始請(qǐng)求路徑添加前綴 前綴路徑
PreserveHostHeader 為請(qǐng)求添加一個(gè)preserveHostHeader=true的屬性,路由過(guò)濾器會(huì)檢查該屬性以決定是否要發(fā)送原始的Host 無(wú)
RequestRateLimiter 用于對(duì)請(qǐng)求限流,限流算法為令牌桶 keyResolver、rateLimiter、statusCode、denyEmptyKey、emptyKeyStatus
RedirectTo 將原始請(qǐng)求重定向到指定的URL http狀態(tài)碼及重定向的url
RemoveHopByHopHeadersFilter 為原始請(qǐng)求刪除IETF組織規(guī)定的一系列Header 默認(rèn)就會(huì)啟用,可以通過(guò)配置指定僅刪除哪些Header
RemoveRequestHeader 為原始請(qǐng)求刪除某個(gè)Header Header名稱
RemoveResponseHeader 為原始響應(yīng)刪除某個(gè)Header Header名稱
RewritePath 重寫原始的請(qǐng)求路徑 原始路徑正則表達(dá)式以及重寫后路徑的正則表達(dá)式
RewriteResponseHeader 重寫原始響應(yīng)中的某個(gè)Header Header名稱,值的正則表達(dá)式,重寫后的值
SaveSession 在轉(zhuǎn)發(fā)請(qǐng)求之前,強(qiáng)制執(zhí)行WebSession::save操作 無(wú)
secureHeaders 為原始響應(yīng)添加一系列起安全作用的響應(yīng)頭 無(wú),支持修改這些安全響應(yīng)頭的值
SetPath 修改原始的請(qǐng)求路徑 修改后的路徑
SetResponseHeader 修改原始響應(yīng)中某個(gè)Header的值 Header名稱,修改后的值
SetStatus 修改原始響應(yīng)的狀態(tài)碼 HTTP 狀態(tài)碼,可以是數(shù)字,也可以是字符串
StripPrefix 用于截?cái)嘣颊?qǐng)求的路徑 使用數(shù)字表示要截?cái)嗟穆窂降臄?shù)量
Retry 針對(duì)不同的響應(yīng)進(jìn)行重試 retries、statuses、methods、series
RequestSize 設(shè)置允許接收最大請(qǐng)求包的大小。如果請(qǐng)求包大小超過(guò)設(shè)置的值,則返回 413 Payload Too Large 請(qǐng)求包大小,單位為字節(jié),默認(rèn)值為5M
ModifyRequestBody 在轉(zhuǎn)發(fā)請(qǐng)求之前修改原始請(qǐng)求體內(nèi)容 修改后的請(qǐng)求體內(nèi)容
ModifyResponseBody 修改原始響應(yīng)體的內(nèi)容 修改后的響應(yīng)體內(nèi)容
Default 為所有路由添加過(guò)濾器 過(guò)濾器工廠名稱及值

Tips:每個(gè)過(guò)濾器工廠都對(duì)應(yīng)一個(gè)實(shí)現(xiàn)類,并且這些類的名稱必須以GatewayFilterFactory結(jié)尾,這是Spring Cloud Gateway的一個(gè)約定,例如AddRequestHeader對(duì)應(yīng)的實(shí)現(xiàn)類為AddRequestHeaderGatewayFilterFactory

自定義局部過(guò)濾器

1、在配置文件中,添加一個(gè)Log的過(guò)濾器配置

server:
  port: 7000
spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 # 將gateway注冊(cè)到nacos
    gateway:
      discovery:
        locator:
          enabled: true # 讓gateway從nacos中獲取服務(wù)信息
      routes:
        - id: product_route
          uri: lb://service-product
#          uri: http://127.0.0.1:8081
          order: 1
          predicates: # 斷言,當(dāng)路由滿足全部斷言時(shí)進(jìn)行轉(zhuǎn)發(fā)
            - Path=/product-serv/**
#            - Age=18,60
          filters:  # 過(guò)濾器
            - StripPrefix=1 # 刪除一級(jí)路徑
            - Log=true,false # 控制日志是否開啟

2、自定義一個(gè)過(guò)濾器工廠

//自定義局部過(guò)濾器
@Component
public class LogGatewayFilterFactory
        extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {

    //構(gòu)造函數(shù)
    public LogGatewayFilterFactory() {
        super(Config.class);
    }

    //讀取配置文件中的參數(shù) 賦值到 配置類中
    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("consoleLog", "cacheLog");
    }

    //過(guò)濾器邏輯
    @Override
    public GatewayFilter apply(Config config) {
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                if (config.isCacheLog()) {
                    System.out.println("cacheLog已經(jīng)開啟了....");
                }
                if (config.isConsoleLog()) {
                    System.out.println("consoleLog已經(jīng)開啟了....");
                }

                return chain.filter(exchange);
            }
        };
    }

    //配置類 接收配置參數(shù)
    @Data
    @NoArgsConstructor
    public static class Config {
        private boolean consoleLog;
        private boolean cacheLog;
    }
}

3、訪問(wèn) http://localhost:7000/product-serv/product/1 查看控制臺(tái)

6.6.2 全局過(guò)濾器

全局過(guò)濾器作用于所有路由,無(wú)需配置。通過(guò)全局過(guò)濾器可以實(shí)現(xiàn)對(duì)權(quán)限的統(tǒng)一校驗(yàn),安全性驗(yàn)證等功能。

內(nèi)置全局過(guò)濾器
image

其中LBCFilter在我們寫uri: lb://service-product時(shí),已經(jīng)使用到了。

自定義全局過(guò)濾器

內(nèi)置的過(guò)濾器已經(jīng)可以完成大部分的功能,但是對(duì)于企業(yè)開發(fā)的一些業(yè)務(wù)功能處理,還是需要我們自己編寫過(guò)濾器來(lái)實(shí)現(xiàn)的,那么我們一起通過(guò)代碼的形式自定義一個(gè)過(guò)濾器,去完成統(tǒng)一的權(quán)限校驗(yàn)。

eg。實(shí)現(xiàn)服務(wù)鑒權(quán)

當(dāng)客戶端第一次請(qǐng)求服務(wù)時(shí),服務(wù)端對(duì)用戶進(jìn)行信息認(rèn)證(登錄)

認(rèn)證通過(guò),將用戶信息進(jìn)行加密形成token,返回給客戶端,作為登錄憑證

以后每次請(qǐng)求,客戶端都攜帶認(rèn)證的token

服務(wù)端對(duì)token進(jìn)行解密,判斷是否有效。

image

1、實(shí)現(xiàn)GlobalFilter, Ordered 接口,書寫自己的鑒權(quán)邏輯

//自定義全局過(guò)濾器需要實(shí)現(xiàn)GlobalFilter和Ordered接口
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    //完成判斷邏輯
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getQueryParams().getFirst("token");
        if (!StringUtils.equals(token, "admin")) {
            System.out.println("鑒權(quán)失敗");
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        //調(diào)用chain.filter繼續(xù)向下游執(zhí)行
        return chain.filter(exchange);
    }

    //順序,數(shù)值越小,優(yōu)先級(jí)越高
    @Override
    public int getOrder() {
        return 0;
    }
}

2、訪問(wèn)

http://localhost:7000/product-serv/product/1

http://localhost:7000/product-serv/product/1?token=admin

6.7 網(wǎng)關(guān)限流(感覺(jué)用途不大)

網(wǎng)關(guān)是所有請(qǐng)求的公共入口,所以可以在網(wǎng)關(guān)進(jìn)行限流,而且限流的方式也很多,我們本次采用前面學(xué)過(guò)的Sentinel組件來(lái)實(shí)現(xiàn)網(wǎng)關(guān)的限流。Sentinel支持對(duì)SpringCloud Gateway、Zuul等主流網(wǎng)關(guān)進(jìn)行限流。

image

從1.6.0版本開始,Sentinel提供了SpringCloud Gateway的適配模塊,可以提供兩種資源維度的限流:

  • route維度:即在Spring配置文件中配置的路由條目,資源名為對(duì)應(yīng)的routeId
  • 自定義API維度:用戶可以利用Sentinel提供的API來(lái)自定義一些API分組

路由維度

1、引入sentinel依賴

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>

2、編寫配置類

基于Sentinel的Gateway限流是通過(guò)其提供的Filter來(lái)完成的,使用時(shí)只需注入對(duì)應(yīng)的 SentinelGatewayFilter實(shí)例以及 SentinelGatewayBlockExceptionHandler 實(shí)例即可。

代碼見(jiàn) cn.x5456.gateway.config.GatewayConfiguration

3、在一秒鐘內(nèi)多次訪問(wèn)http://localhost:7000/product-serv/product/1就可以看到限流啟作用了。

API維度

代碼見(jiàn) cn.x5456.gateway.config.GatewayConfiguration

七、鏈路追蹤 Sleuth

在大型系統(tǒng)的微服務(wù)化構(gòu)建中,一個(gè)系統(tǒng)被拆分成了許多模塊。這些模塊負(fù)責(zé)不同的功能,組合成系統(tǒng),最終可以提供豐富的功能。在這種架構(gòu)中,一次請(qǐng)求往往需要涉及到多個(gè)服務(wù)?;ヂ?lián)網(wǎng)應(yīng)用構(gòu)建在不同的軟件模塊集上,這些軟件模塊,有可能是由不同的團(tuán)隊(duì)開發(fā)、可能使用不同的編程語(yǔ)言來(lái)實(shí)現(xiàn)、有可能布在了幾千臺(tái)服務(wù)器,橫跨多個(gè)不同的數(shù)據(jù)中心,也就意味著這種架構(gòu)形式也會(huì)存在一些問(wèn)題:

  • 如何快速發(fā)現(xiàn)問(wèn)題?
  • 如何判斷故障影響范圍?
  • 如何梳理服務(wù)依賴以及依賴的合理性?
  • 如何分析鏈路性能問(wèn)題以及實(shí)時(shí)容量規(guī)劃?

分布式鏈路追蹤(Distributed Tracing),就是將一次分布式請(qǐng)求還原成調(diào)用鏈路,進(jìn)行日志記 錄,性能監(jiān)控并將一次分布式請(qǐng)求的調(diào)用情況集中展示。比如各個(gè)服務(wù)節(jié)點(diǎn)上的耗時(shí)、請(qǐng)求具體到達(dá)哪 臺(tái)機(jī)器上、每個(gè)服務(wù)節(jié)點(diǎn)的請(qǐng)求狀態(tài)等等。

7.1 常用鏈路追蹤技術(shù)

cat 由大眾點(diǎn)評(píng)開源,基于Java開發(fā)的實(shí)時(shí)應(yīng)用監(jiān)控平臺(tái),包括實(shí)時(shí)應(yīng)用監(jiān)控,業(yè)務(wù)監(jiān)控。集成方案是通過(guò)代碼埋點(diǎn)的方式來(lái)實(shí)現(xiàn)監(jiān)控,比如: 攔截器,過(guò)濾器等。對(duì)代碼的侵入性很大,集成成本較高。風(fēng)險(xiǎn)較大。

zipkin 由Twitter公司開源,開放源代碼分布式的跟蹤系統(tǒng),用于收集服務(wù)的定時(shí)數(shù)據(jù),以解決微服務(wù)架構(gòu)中的延遲問(wèn)題,包括:數(shù)據(jù)的收集、存儲(chǔ)、查找和展現(xiàn)。該產(chǎn)品結(jié)合spring-cloud-sleuth 使用較為簡(jiǎn)單,集成很方便,但是功能較簡(jiǎn)單。

pinpoint Pinpoint是韓國(guó)人開源的基于字節(jié)碼注入的調(diào)用鏈分析,以及應(yīng)用監(jiān)控分析工具。特點(diǎn)是支持多種插件,UI功能強(qiáng)大,接入端無(wú)代碼侵入。

SkyWalking是本土開源的基于字節(jié)碼注入的調(diào)用鏈分析,以及應(yīng)用監(jiān)控分析工具。特點(diǎn)是支持多種插件,UI功能較強(qiáng),接入端無(wú)代碼侵入。目前已加入Apache孵化器。

SpringCloud Sleuth 提供的分布式系統(tǒng)中鏈路追蹤解決方案。

注:我們可以使用SpringCloud Sleuth收集鏈路中的日志信息,交給zipkin來(lái)展示可視化界面

7.2 SpringCloud Sleuth 基本概念 & 快速開始

基本概念

SpringCloud Sleuth主要功能就是在分布式系統(tǒng)中提供追蹤解決方案。它大量借用了Google Dapper的設(shè)計(jì),先來(lái)了解一下Sleuth中的術(shù)語(yǔ)和相關(guān)概念。

Trace

由一組Trace Id相同的Span串聯(lián)形成一個(gè)樹狀結(jié)構(gòu)。為了實(shí)現(xiàn)請(qǐng)求跟蹤,當(dāng)請(qǐng)求到達(dá)分布式系統(tǒng)的入口端點(diǎn)時(shí),只需要服務(wù)跟蹤框架為該請(qǐng)求創(chuàng)建一個(gè)唯一的標(biāo)識(shí)(即TraceId),同時(shí)在分布式系統(tǒng)內(nèi)部流轉(zhuǎn)的時(shí)候,框架始終保持傳遞該唯一值,直到整個(gè)請(qǐng)求的返回。那么我們就可以使用該唯一標(biāo)識(shí)將所有的請(qǐng)求串聯(lián)起來(lái),形成一條完整的請(qǐng)求鏈路。

Span

代表了一組基本的工作單元。為了統(tǒng)計(jì)各處理單元的延遲,當(dāng)請(qǐng)求到達(dá)各個(gè)服務(wù)組件的時(shí) 候,也通過(guò)一個(gè)唯一標(biāo)識(shí)(SpanId)來(lái)標(biāo)記它的開始、具體過(guò)程和結(jié)束。通過(guò)SpanId的開始和結(jié)束時(shí)間戳,就能統(tǒng)計(jì)該span的調(diào)用時(shí)間,除此之外,我們還可以獲取如事件的名稱。請(qǐng)求信息等元數(shù)據(jù)。

Annotation

用它記錄一段時(shí)間內(nèi)的事件,內(nèi)部使用的重要注釋:

  • cs(Client Send)客戶端發(fā)出請(qǐng)求,開始一個(gè)請(qǐng)求的生命
  • sr(Server Received)服務(wù)端接受到請(qǐng)求開始進(jìn)行處理, sr-cs = 網(wǎng)絡(luò)延遲(服務(wù)調(diào)用的時(shí)間)
  • ss(Server Send)服務(wù)端處理完畢準(zhǔn)備發(fā)送到客戶端,ss - sr = 服務(wù)器上的請(qǐng)求處理時(shí)間
  • cr(Client Reveived)客戶端接受到服務(wù)端的響應(yīng),請(qǐng)求結(jié)束。 cr - sr = 請(qǐng)求的總時(shí)間

QuickStart

1、在api-gateway、shop-order、shop-product微服務(wù)中引入sleuth依賴

<!--鏈路追蹤-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

2、啟動(dòng)3個(gè)微服務(wù),訪問(wèn) http://127.0.0.1:7000/order-serv/order/prod/2 查看日志

微服務(wù)名稱, traceId, spanid,是否將鏈路的追蹤結(jié)果輸出到第三方平臺(tái)
[api-gateway,3977125f73391553,3977125f73391553,false]
[service-order,3977125f73391553,57547b5bf71f8242,false] [service-product,3977125f73391553,449f5b3f3ef8d5c5,false]

通過(guò)TraceId,通過(guò)查看日志,我們可以將調(diào)用鏈路串起來(lái)。

但查看日志文件并不是一個(gè)很好的方法,當(dāng)微服務(wù)越來(lái)越多日志文件也會(huì)越來(lái)越多,通過(guò)Zipkin可以將日志聚合,并進(jìn)行可視化展示和全文檢索。

7.3 集成 ZipKin

Zipkin 是 Twitter 的一個(gè)開源項(xiàng)目,它基于Google Dapper實(shí)現(xiàn),它致力于收集服務(wù)的定時(shí)數(shù)據(jù),以解決微服務(wù)架構(gòu)中的延遲問(wèn)題,包括數(shù)據(jù)的收集、存儲(chǔ)、查找和展現(xiàn)。

我們可以使用它來(lái)收集各個(gè)服務(wù)器上請(qǐng)求鏈路的跟蹤數(shù)據(jù),并通過(guò)它提供的REST API接口來(lái)輔助我們查詢跟蹤數(shù)據(jù)以實(shí)現(xiàn)對(duì)分布式系統(tǒng)的監(jiān)控程序,從而及時(shí)地發(fā)現(xiàn)系統(tǒng)中出現(xiàn)的延遲升高問(wèn)題并找出系統(tǒng)性能瓶頸的根源。

除了面向開發(fā)的 API 接口之外,它也提供了方便的UI組件來(lái)幫助我們直觀的搜索跟蹤信息和分析請(qǐng) 求鏈路明細(xì),比如:可以查詢某段時(shí)間內(nèi)各用戶請(qǐng)求的處理時(shí)間等。

Zipkin 提供了可插拔數(shù)據(jù)存儲(chǔ)方式:In-Memory、MySql、Cassandra 以及 Elasticsearch。

image

上圖展示了 Zipkin 的基礎(chǔ)架構(gòu),它主要由 4 個(gè)核心組件構(gòu)成:

  • Collector:收集器組件,它主要用于處理從外部系統(tǒng)發(fā)送過(guò)來(lái)的跟蹤信息,將這些信息轉(zhuǎn)換為Zipkin內(nèi)部處理的 Span 格式,以支持后續(xù)的存儲(chǔ)、分析、展示等功能。
  • Storage:存儲(chǔ)組件,它主要對(duì)處理收集器接收到的跟蹤信息,默認(rèn)會(huì)將這些信息存儲(chǔ)在內(nèi)存中,我們也可以修改此存儲(chǔ)策略,通過(guò)使用其他存儲(chǔ)組件將跟蹤信息存儲(chǔ)到數(shù)據(jù)庫(kù)中。
  • RESTful API:API 組件,它主要用來(lái)提供外部訪問(wèn)接口。比如給客戶端展示跟蹤信息,或是外接系統(tǒng)訪問(wèn)以實(shí)現(xiàn)監(jiān)控等。
  • Web UI:UI 組件, 基于API組件實(shí)現(xiàn)的上層應(yīng)用。通過(guò)UI組件用戶可以方便而有直觀地查詢和分 析跟蹤信息。

Zipkin分為兩端,一個(gè)是 Zipkin服務(wù)端,一個(gè)是 Zipkin客戶端,客戶端也就是微服務(wù)的應(yīng)用??蛻舳藭?huì)配置服務(wù)端的 URL 地址,一旦發(fā)生服務(wù)間的調(diào)用的時(shí)候,會(huì)被配置在微服務(wù)里面的 Sleuth 的監(jiān)聽(tīng)器監(jiān)聽(tīng),并生成相應(yīng)的 Trace 和 Span 信息發(fā)送給服務(wù)端。

7.3.1 安裝 ZipKin 服務(wù)端

jar包在git中有

java -jar zipkin-server-2.12.9-exec.jar

訪問(wèn) http://127.0.0.1:9411

7.3.2 客戶端集成Zipkin

1、在3個(gè)微服務(wù)中添加依賴

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

2、在3個(gè)微服務(wù)中添加配置

spring:
  zipkin:
    base-url: http://127.0.0.1:9411/  #zipkin server的請(qǐng)求地址
    discoveryClientEnabled: false #讓nacos把它當(dāng)成一個(gè)URL,而不要當(dāng)做服務(wù)名
  sleuth:
    sampler:
      probability: 1.0  #采樣的百分比

3、重啟3個(gè)微服務(wù),訪問(wèn) http://127.0.0.1:7000/order-serv/order/prod/2

4、進(jìn)入zipkin,查看

image

7.4 持久化

Zipkin Server默認(rèn)會(huì)將追蹤數(shù)據(jù)信息保存到內(nèi)存,但這種方式不適合生產(chǎn)環(huán)境。Zipkin支持將追蹤數(shù)據(jù)持久化到mysql數(shù)據(jù)庫(kù)或elasticsearch中。

7.4.1 持久化到MySQL中

1、創(chuàng)建MySQL表

CREATE TABLE IF NOT EXISTS zipkin_spans (
  `trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
  `trace_id` BIGINT NOT NULL,
  `id` BIGINT NOT NULL,
  `name` VARCHAR(255) NOT NULL,
  `parent_id` BIGINT,
  `debug` BIT(1),
  `start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for endTs query and to implement TTL',
  `duration` BIGINT COMMENT 'Span.duration(): micros used for minDuration and maxDuration query',
  PRIMARY KEY (`trace_id_high`, `trace_id`, `id`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;

ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTracesByIds';
ALTER TABLE zipkin_spans ADD INDEX(`name`) COMMENT 'for getTraces and getSpanNames';
ALTER TABLE zipkin_spans ADD INDEX(`start_ts`) COMMENT 'for getTraces ordering and range';

CREATE TABLE IF NOT EXISTS zipkin_annotations (
  `trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
  `trace_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.trace_id',
  `span_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.id',
  `a_key` VARCHAR(255) NOT NULL COMMENT 'BinaryAnnotation.key or Annotation.value if type == -1',
  `a_value` BLOB COMMENT 'BinaryAnnotation.value(), which must be smaller than 64KB',
  `a_type` INT NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if Annotation',
  `a_timestamp` BIGINT COMMENT 'Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp',
  `endpoint_ipv4` INT COMMENT 'Null when Binary/Annotation.endpoint is null',
  `endpoint_ipv6` BINARY(16) COMMENT 'Null when Binary/Annotation.endpoint is null, or no IPv6 address',
  `endpoint_port` SMALLINT COMMENT 'Null when Binary/Annotation.endpoint is null',
  `endpoint_service_name` VARCHAR(255) COMMENT 'Null when Binary/Annotation.endpoint is null'
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;

ALTER TABLE zipkin_annotations ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `span_id`, `a_key`, `a_timestamp`) COMMENT 'Ignore insert on duplicate';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`, `span_id`) COMMENT 'for joining with zipkin_spans';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTraces/ByIds';
ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT 'for getTraces and getServiceNames';
ALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for getTraces and autocomplete values';
ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces and autocomplete values';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id`, `span_id`, `a_key`) COMMENT 'for dependencies job';

CREATE TABLE IF NOT EXISTS zipkin_dependencies (
  `day` DATE NOT NULL,
  `parent` VARCHAR(255) NOT NULL,
  `child` VARCHAR(255) NOT NULL,
  `call_count` BIGINT,
  `error_count` BIGINT,
  PRIMARY KEY (`day`, `parent`, `child`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;

2、刪除之前的zipkin容器,重新執(zhí)行下面代碼

java -jar zipkin-server-2.12.9-exec.jar --STORAGE_TYPE=mysql --MYSQL_HOST=127.0.0.1 --MYSQL_TCP_PORT=3306 --MYSQL_DB=zipkin --MYSQL_USER=root --MYSQL_PASS=5456

7.4.2 持久化到elasticsearch中

java -jar zipkin-server-2.12.9-exec.jar --STORAGE_TYPE=elasticsearch --ES-HOST=localhost:9200

八、服務(wù)配置中心 Nacos Config

微服務(wù)架構(gòu)下關(guān)于配置文件存在的一些問(wèn)題:

  1. 配置文件相對(duì)分散。在一個(gè)微服務(wù)架構(gòu)下,配置文件會(huì)隨著微服務(wù)的增多變的越來(lái)越多,而且分散在各個(gè)微服務(wù)中,不好統(tǒng)一配置和管理
  2. 配置文件無(wú)法區(qū)分環(huán)。微服務(wù)項(xiàng)目可能會(huì)有多個(gè)環(huán)境,例如:測(cè)試環(huán)境、預(yù)發(fā)布環(huán)境、生產(chǎn)環(huán)境。每一個(gè)環(huán)境所使用的配置理論上都是不同的,一旦需要修改,就需要我們?nèi)ジ鱾€(gè)微服務(wù)下手動(dòng)維護(hù),這比較困難。
  3. 配置文件無(wú)法實(shí)時(shí)更新。我們修改了配置文件之后,必須重新啟動(dòng)微服務(wù)才能使配置生效,這對(duì)一個(gè)正在運(yùn)行的項(xiàng)目來(lái)說(shuō)是非常不友好的。

基于上面這些問(wèn)題,我們就需要配置中心的加入來(lái)解決這些問(wèn)題。

配置中心的思路是:

  1. 首先把項(xiàng)目中各種配置全部都放到一個(gè)集中的地方進(jìn)行統(tǒng)一管理,并提供一套標(biāo)準(zhǔn)的接口。
  2. 當(dāng)各個(gè)服務(wù)需要獲取配置的時(shí)候,就來(lái)配置中心的接口拉取自己的配置。
  3. 當(dāng)配置中心中的各種參數(shù)有更新的時(shí)候,也能通知到各個(gè)服務(wù)實(shí)時(shí)的過(guò)來(lái)同步最新的信息,使之動(dòng)態(tài)更新。

當(dāng)加入了服務(wù)配置中心之后,我們的系統(tǒng)架構(gòu)圖會(huì)變成下面這樣:

image

8.1 常見(jiàn)的服務(wù)配置中心

Apollo是由攜程開源的分布式配置中心。特點(diǎn)有很多,比如:配置更新之后可以實(shí)時(shí)生效,支持灰度發(fā)布功能,并且能對(duì)所有的配置進(jìn)行版本管理、操作審計(jì)等功能,提供開放平臺(tái)API。并且資料也寫的很詳細(xì)。

Disconf是由百度開源的分布式配置中心。它是基于Zookeeper來(lái)實(shí)現(xiàn)配置變更后實(shí)時(shí)通知和生效的。

SpringCloud Config是Spring Cloud中帶的配置中心組件。它和Spring是無(wú)縫集成,使用起來(lái)非常方便,并且它的配置存儲(chǔ)支持Git。不過(guò)它沒(méi)有可視化的操作界面,配置的生效也不是實(shí)時(shí)的,需要重啟或去刷新。

Nacos是SpingCloud alibaba技術(shù)棧中的一個(gè)組件,前面我們已經(jīng)使用它做過(guò)服務(wù)注冊(cè)中心。其實(shí)它也集成了服務(wù)配置的功能,我們可以直接使用它作為服務(wù)配置中心。

8.2 Nacos Config 快速開始

1、在商品微服務(wù)中引入nacos config的依賴

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

2、新建bootstrap.yml

spring:
  application:
    name: service-product 
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848 #nacos中心地址
        file-extension: yaml # 配置文件格式 
  profiles:
    active: dev # 環(huán)境標(biāo)識(shí)

注:配置文件優(yōu)先級(jí)(由高到低):bootstrap.properties -> bootstrap.yml -> application.properties -> application.yml

3、將application.yml中的配置刪除,在nacos中添加配置

image
image
server:
  port: 8081
spring:
  zipkin:
    base-url: http://127.0.0.1:9411  #zipkin server的請(qǐng)求地址
    discoveryClientEnabled: false #讓nacos把它當(dāng)成一個(gè)URL,而不要當(dāng)做服務(wù)名
  sleuth:
    sampler:
      probability: 1.0  #采樣的百分比
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/shop?characterEncoding=UTF-8
    username: root
    password: 5456
  jpa:
    properties:
      hibernate:
        hbm2ddl:
          auto: update
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

4、重啟商品微服務(wù)

8.3 配置動(dòng)態(tài)刷新

在nacos中的配置中,新增下面配置:

config:
    appName: product

方式一

@RestController
public class NacosConfigController {

    @Autowired
    private ConfigurableApplicationContext applicationContext;

    @RequestMapping("/test-config1")
    public String testConfig1() {
        return applicationContext.getEnvironment().getProperty("config.appName");
    }
}

方式二

@RestController
@RefreshScope//動(dòng)態(tài)刷新的注解
public class NacosConfigController {

    @Autowired
    private ConfigurableApplicationContext applicationContext;

    @Value("${config.appName}")
    private String appName;

    @RequestMapping("/test-config1")
    public String testConfig1() {
        return applicationContext.getEnvironment().getProperty("config.appName");
    }


    @RequestMapping("/test-config2")
    public String testConfig2() {
        return appName;
    }
}

注:類似數(shù)據(jù)庫(kù)連接的配置修改后是不會(huì)動(dòng)態(tài)更新的

8.4 配置共享

同一個(gè)微服務(wù)的不同環(huán)境之間共享配置

當(dāng)配置越來(lái)越多的時(shí)候,我們就發(fā)現(xiàn)有很多配置是重復(fù)的,這時(shí)候就考慮可不可以將公共配置文件提取出來(lái),然后實(shí)現(xiàn)共享呢?當(dāng)然是可以的。接下來(lái)我們就來(lái)探討如何實(shí)現(xiàn)這一功能。

如果想在同一個(gè)微服務(wù)的不同環(huán)境之間實(shí)現(xiàn)配置共享,其實(shí)很簡(jiǎn)單。只需要提取一個(gè)以 spring.application.name 命名的配置文件,然后將其所有環(huán)境的公共配置放在里面即可。

1、新建一個(gè)名為service-product.yaml配置存放商品微服務(wù)的公共配置

image

2、刪除dev配置文件的這部分

3、重啟系統(tǒng)

不同微服務(wù)中間共享配置

不同為服務(wù)之間實(shí)現(xiàn)配置共享的原理類似于文件引入,就是定義一個(gè)公共配置,然后在當(dāng)前配置中引入。

1、在nacos中定義一個(gè)DataID為all-service.yaml(這個(gè)名字可以隨便寫)的配置,用于所有微服務(wù)共享

image
spring:
  zipkin:
    base-url: http://127.0.0.1:9411  #zipkin server的請(qǐng)求地址
    discoveryClientEnabled: false #讓nacos把它當(dāng)成一個(gè)URL,而不要當(dāng)做服務(wù)名
  sleuth:
    sampler:
      probability: 1.0  #采樣的百分比

2、刪除商品微服務(wù)-dev的這部分配置

3、修改bootstrap.yml

spring:
  application:
    name: service-product
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848 #nacos中心地址
        file-extension: yaml # 配置文件格式
        shared-dataids: all-service.yaml # 配置要引入的配置 
        refreshable-dataids: all-service.yaml # 配置要實(shí)現(xiàn)動(dòng)態(tài)配置刷新的配置
  profiles:
    active: dev # 環(huán)境標(biāo)識(shí)

4、重啟微服務(wù)

8.5 nacos-config 的幾個(gè)概念

命名空間(Namespace) 命名空間可用于進(jìn)行不同環(huán)境的配置隔離。一般一個(gè)環(huán)境劃分到一個(gè)命名空間

配置分組(Group) 配置分組用于將不同的服務(wù)可以歸類到同一分組。一般將一個(gè)項(xiàng)目的配置分到一組

配置集(Data ID) 在系統(tǒng)中,一個(gè)配置文件通常就是一個(gè)配置集。一般一個(gè)微服務(wù)的配置就是一個(gè)配置集

image

九、分布式事務(wù) Seata

概念見(jiàn) http://www.itdecent.cn/p/19492cfc71fb

9.1 Seata簡(jiǎn)介

2019 年 1 月,阿里巴巴中間件團(tuán)隊(duì)發(fā)起了開源項(xiàng)目 Fescar(Fast & EaSy Commit And Rollback),其愿景是讓分布式事務(wù)的使用像本地事務(wù)的使用一樣,簡(jiǎn)單和高效,并逐步解決開發(fā)者們遇到的分布式事務(wù)方面的所有難題。后來(lái)更名為 Seata,意為:Simple Extensible Autonomous Transaction Architecture,是一套分布式事務(wù)解決方案。

Seata的設(shè)計(jì)目標(biāo)是對(duì)業(yè)務(wù)無(wú)侵入,因此從業(yè)務(wù)無(wú)侵入的2PC方案著手,在傳統(tǒng)2PC的基礎(chǔ)上演進(jìn)。它把一個(gè)分布式事務(wù)理解成一個(gè)包含了若干分支事務(wù)的全局事務(wù)。全局事務(wù)的職責(zé)是協(xié)調(diào)其下管轄的分支事務(wù)達(dá)成一致,要么一起成功提交,要么一起失敗回滾。此外,通常分支事務(wù)本身就是一個(gè)關(guān)系數(shù)據(jù)庫(kù)的本地事務(wù)。

image

Seata主要由三個(gè)重要組件組成:

  • TC:Transaction Coordinator 事務(wù)協(xié)調(diào)器,管理全局的分支事務(wù)的狀態(tài),用于全局性事務(wù)的提交和回滾。
  • TM:Transaction Manager 事務(wù)管理器,用于開啟、提交或者回滾全局事務(wù)。
  • RM:Resource Manager 資源管理器,用于分支事務(wù)上的資源管理,向TC注冊(cè)分支事務(wù),上報(bào)分支事務(wù)的狀態(tài),接受TC的命令來(lái)提交或者回滾分支事務(wù)。
image

A服務(wù)的TM向TC申請(qǐng)開啟一個(gè)全局事務(wù),TC就會(huì)創(chuàng)建一個(gè)全局事務(wù)并返回一個(gè)唯一的XID

A服務(wù)的RM向TC注冊(cè)分支事務(wù),并及其納入XID對(duì)應(yīng)全局事務(wù)的管轄

A服務(wù)執(zhí)行分支事務(wù),向數(shù)據(jù)庫(kù)做操作

A服務(wù)開始遠(yuǎn)程調(diào)用B服務(wù),此時(shí)XID會(huì)在微服務(wù)的調(diào)用鏈上傳播

B服務(wù)的RM向TC注冊(cè)分支事務(wù),并將其納入XID對(duì)應(yīng)的全局事務(wù)的管轄

B服務(wù)執(zhí)行分支事務(wù),向數(shù)據(jù)庫(kù)做操作

全局事務(wù)調(diào)用鏈處理完畢,TM根據(jù)有無(wú)異常向TC發(fā)起全局事務(wù)的提交或者回滾

TC協(xié)調(diào)其管轄之下的所有分支事務(wù),決定是否回滾

Seata實(shí)現(xiàn)2PC與傳統(tǒng)2PC的差別

  1. 架構(gòu)層次方面,傳統(tǒng)2PC方案的 RM 實(shí)際上是在數(shù)據(jù)庫(kù)層,RM本質(zhì)上就是數(shù)據(jù)庫(kù)自身,通過(guò)XA協(xié)議實(shí)現(xiàn),而 Seata 的RM是以jar包的形式作為中間件層部署在應(yīng)用程序這一側(cè)的。

  2. 兩階段提交方面,傳統(tǒng)2PC無(wú)論第二階段的決議是commit還是rollback,事務(wù)性資源的鎖都要保持到Phase2完成才釋放。而Seata的做法是在Phase1 就將本地事務(wù)提交,這樣就可以省去Phase2 持鎖的時(shí)間,整體提高效率。

?這樣會(huì)不會(huì)出現(xiàn)線程安全問(wèn)題啊

9.2 快速開始

模擬電商中的下單和扣庫(kù)存的過(guò)程,我們通過(guò)訂單微服務(wù)執(zhí)行下單操作,然后由訂單微服務(wù)調(diào)用商品微服務(wù)扣除庫(kù)存。

1、新建OrderController3


@RestController
@Slf4j
public class OrderController3 {

    @Autowired
    private OrderDao orderDao;

    @Autowired
    private ProductClient productClient;


    //下單--fegin
    @RequestMapping("/order3/prod/{pid}")
    public Order order(@PathVariable("pid") Integer pid) {
        log.info("接收到{}號(hào)商品的下單請(qǐng)求,接下來(lái)調(diào)用商品微服務(wù)查詢此商品信息", pid);

        //調(diào)用商品微服務(wù),查詢商品信息
        Product product = productClient.findByPid(pid);

        if (product.getPid() == -100) {
            Order order = new Order();
            order.setOid(-100L);
            order.setPname("下單失敗");
            return order;
        }

        log.info("查詢到{}號(hào)商品的信息,內(nèi)容是:{}", pid, JSON.toJSONString(product));

        //下單(創(chuàng)建訂單)
        Order order = new Order();
        order.setUid(1);
        order.setUsername("測(cè)試用戶");
        order.setPid(pid);
        order.setPname(product.getPname());
        order.setPprice(product.getPprice());
        order.setNumber(1);

        orderDao.save(order);

        log.info("創(chuàng)建訂單成功,訂單信息為{}", JSON.toJSONString(order));

        //扣庫(kù)存
        productClient.reduceInventory(pid, order.getNumber());

        return order;
    }
}

2、在ProductClient中添加扣減庫(kù)存

//value用于指定調(diào)用nacos下哪個(gè)微服務(wù)
@FeignClient(value = "service-product"/*, fallbackFactory = ProductServiceFallbackFactory.class*/)
public interface ProductClient {
    //@FeignClient的value +  @RequestMapping的value值  其實(shí)就是完成的請(qǐng)求地址  "http://service-product/product/" + pid
    //指定請(qǐng)求的URI部分
    @RequestMapping("/product/{pid}")
    Product findByPid(@PathVariable("pid") Integer pid);

    // 扣減庫(kù)存
    @RequestMapping("/product/reduceInventory")
    void reduceInventory(@RequestParam("pid") Integer pid, @RequestParam("num") Integer num);
}

3、在商品微服務(wù)中添加扣減庫(kù)存方法

@Transactional
@RequestMapping("/product/reduceInventory")
public void reduceInventory(@RequestParam("pid") Integer pid, @RequestParam("num") Integer num) {
    Product product = productDao.findById(pid).get();
    product.setStock(product.getStock() - num);
    productDao.save(product);
}

4、啟動(dòng)項(xiàng)目測(cè)試一下 http://127.0.0.1:8091/order3/prod/1

5、下載Seata服務(wù)端,修改conf/registry.conf

這是注冊(cè)中心和配置中心的配置

registry {
    type = "nacos"
    nacos {
        serverAddr = "localhost"
        namespace = "public"
        cluster = "default"
    }
}

config {
    type = "nacos"
    nacos {
        serverAddr = "localhost"
        namespace = "public"
        cluster = "default"
    }
}

6、刪除nacos中的所有配置,將商品微服務(wù)的配置改回之前的

7、打開nacos-config.txt,添加

service.vgroup_mapping.service-product=default 
service.vgroup_mapping.service-order=default

這里的語(yǔ)法為:service.vgroup_mapping.{your-service-gruop}=default ,中間的{your-service-gruop} 為自己定義的服務(wù)組名稱, 這里需要我們?cè)诔绦虻呐渲梦募信渲谩?/p>

7、初始化seata在nacos的配置

./nacos-config.sh 127.0.0.1

執(zhí)行成功后可以打開Nacos的控制臺(tái),在配置列表中,可以看到初始化了很多Group為SEATA_GROUP的配置。

image

8、啟動(dòng)seata服務(wù)

cd ../bin
./seata-server.sh -p 9000 -m file

啟動(dòng)后在 Nacos 的服務(wù)列表下面可以看到一個(gè)名為 serverAddr 的服務(wù)。

image

9、在我們的數(shù)據(jù)庫(kù)(每個(gè)業(yè)務(wù)庫(kù)都要有)中加入一張undo_log表,這是Seata記錄事務(wù)日志要用到的表

CREATE TABLE `undo_log` (
  `id`            BIGINT(20)   NOT NULL AUTO_INCREMENT,
  `branch_id`     BIGINT(20)   NOT NULL,
  `xid`           VARCHAR(100) NOT NULL,
  `context`       VARCHAR(128) NOT NULL,
  `rollback_info` LONGBLOB     NOT NULL,
  `log_status`    INT(11)      NOT NULL,
  `log_created`   DATETIME     NOT NULL,
  `log_modified`  DATETIME     NOT NULL,
  `ext`           VARCHAR(100)          DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
)
  ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8;

10、在商品和order微服務(wù)中添加以下依賴

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

11、在商品和order微服務(wù)中添加DataSourceProxyConfig

Seata 是通過(guò)代理數(shù)據(jù)源實(shí)現(xiàn)事務(wù)分支的,所以需要配置 io.seata.rm.datasource.DataSourceProxy的Bean,且是@Primary默認(rèn)的數(shù)據(jù)源,否則事務(wù)不會(huì)回滾,無(wú)法實(shí)現(xiàn)分布式事務(wù)

@Configuration
public class DataSourceProxyConfig {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DruidDataSource druidDataSource() {
        return new DruidDataSource();
    }

    @Primary
    @Bean
    public DataSourceProxy dataSource(DruidDataSource druidDataSource) {
        return new DataSourceProxy(druidDataSource);
    }
}

12、在resources下添加Seata的配置文件 registry.conf

和上面的那個(gè)一樣

registry {
    type = "nacos"
    nacos {
        serverAddr = "localhost"
        namespace = "public"
        cluster = "default"
    }
}

config {
    type = "nacos"
    nacos {
        serverAddr = "localhost"
        namespace = "public"
        cluster = "default"
    }
}

13、修改bootstarp.yml,添加如下配置

spring:
  cloud:
    nacos:
      config:
        server-addr: localhost:8848 # nacos的服務(wù)端地址
        namespace: public
        group: SEATA_GROUP
    alibaba:
      seata:
        tx-service-group: service-order # 與第7步是對(duì)應(yīng)的

14、開啟全局事務(wù)

@GlobalTransactional
@RequestMapping("/order3/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {

15、添加異常

@Transactional
@RequestMapping("/product/reduceInventory")
public void reduceInventory(@RequestParam("pid") Integer pid, @RequestParam("num") Integer num) {
    Product product = productDao.findById(pid).get();
    product.setStock(product.getStock() - num);
    productDao.save(product);

    int i = 1 / 0;
}

16、測(cè)試

注意:alibaba的版本一定要切換到2.1.0.RELEASE,見(jiàn)5.5.1。

9.3 seata運(yùn)行流程分析

image

1、每個(gè)RM使用DataSourceProxy連接數(shù)據(jù)庫(kù),其目的是使用ConnectionProxy,使用數(shù)據(jù)源和數(shù)據(jù)連接代理的目的就是在第一階段將undo_log和業(yè)務(wù)數(shù)據(jù)放在一個(gè)本地事務(wù)提交,這樣就保存了只要有業(yè)務(wù)操作就一定有undo_log。

2、在第一階段undo_log中存放了數(shù)據(jù)修改前和修改后的值,為事務(wù)回滾作好準(zhǔn)備,所以第一階段完成就已經(jīng)將分支事務(wù)提交,也就釋放了鎖資源。

3、TM開啟全局事務(wù)開始,將XID全局事務(wù)id放在事務(wù)上下文中,通過(guò)feign調(diào)用也將XID傳入下游分支事務(wù),每個(gè)分支事務(wù)將自己的Branch ID分支事務(wù)ID與XID關(guān)聯(lián)。

4、第二階段全局事務(wù)提交,TC會(huì)通知各各分支參與者提交分支事務(wù),在第一階段就已經(jīng)提交了分支事務(wù),這里各各參與者只需要?jiǎng)h除undo_log即可,并且可以異步執(zhí)行,第二階段很快可以完成。

5、第二階段全局事務(wù)回滾,TC會(huì)通知各各分支參與者回滾分支事務(wù),通過(guò) XID 和 Branch ID 找到相應(yīng)的回滾日志,通過(guò)回滾日志生成反向的 SQL 并執(zhí)行,以完成分支事務(wù)回滾到之前的狀態(tài),如果回滾失 敗則會(huì)重試回滾操作。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 為什么要有服務(wù)網(wǎng)關(guān)? 我們都知道在微服務(wù)架構(gòu)中,系統(tǒng)會(huì)被拆分為很多個(gè)微服務(wù)。那么作為客戶端要如何去調(diào)用這么多的微服...
    匆匆歲月閱讀 2,885評(píng)論 0 13
  • 微服務(wù)架構(gòu)模式的核心在于如何識(shí)別服務(wù)的邊界,設(shè)計(jì)出合理的微服務(wù)。但如果要將微服務(wù)架構(gòu)運(yùn)用到生產(chǎn)項(xiàng)目上,并且能夠發(fā)揮...
    java菜閱讀 3,038評(píng)論 0 6
  • 一、微服務(wù)簡(jiǎn)介 1. 微服務(wù)的誕生 微服務(wù)是基于分而治之的思想演化出來(lái)的。過(guò)去傳統(tǒng)的一個(gè)大型而又全面的系統(tǒng),隨著互...
    Java_蘇先生閱讀 2,910評(píng)論 0 3
  • 今天下午來(lái)三點(diǎn)多左右,我發(fā)現(xiàn)徐天浩像瘋了似的一直在打人,然后我和同學(xué)又去告了陳老師,陳老師聽(tīng)了就趕緊來(lái)到了...
    崳ZXY閱讀 526評(píng)論 2 6
  • 我回頭,它仍然是那樣一動(dòng)不動(dòng)的躺著,四肢前后伸展,側(cè)躺在地上,像熟睡了般。要不是它沒(méi)有呼吸的起伏,要不是一輛摩...
    young書閱讀 472評(píng)論 1 0

友情鏈接更多精彩內(nèi)容