微服務總結(下)

六、服務網關 SpringCloud Gateway

在微服務架構中,一個系統會被拆分為很多個微服務。那么作為客戶端要如何去調用這么多的微服務呢?

如果沒有網關的存在,我們只能在客戶端記錄每個微服務的地址,然后分別去調用。

image

這樣的話,會有很多問題:

  • 客戶端多次請求不同的微服務,增加客戶端代碼或配置編寫的復雜性
  • 認證復雜,每個服務都需要獨立認證。
  • 各個微服務都存在跨域請求,在一定場景下處理相對復雜。

這些問題,我們就可以采用網關來解決。

所謂的API網關,就是指系統的統一入口,它封裝了應用程序的內部結構,為客戶端提供統一服務,一些與業(yè)務本身功能無關的公共邏輯可以在這里實現,諸如認證、鑒權、監(jiān)控、路由轉發(fā)等等。

添加上API網關之后,系統的架構圖變成了如下所示:

image

6.1 目前可以使用的網關

Ngnix+lua

使用nginx的反向代理和負載均衡可實現對api服務器的負載均衡及高可用

lua是一種腳本語言,可以來編寫一些簡單的邏輯, nginx支持lua腳本

Kong

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

Zuul

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

Spring Cloud Gateway

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

優(yōu)點:

  • 性能強勁:是Zuul1.0的1.6倍
  • 功能強大:內置了很多實用的功能,例如轉發(fā)、監(jiān)控、限流等
  • 設計優(yōu)雅,容易擴展

缺點:

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

6.3 快速開始

基礎版

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: # 斷言,當路由滿足全部斷言時進行轉發(fā)
            - Path=/product-serv/**
          filters:  # 過濾器
            - StripPrefix=1 # 刪除一級路徑

3、啟動網關和商品微服務,請求 http://127.0.0.1:7000/product-serv/product/1

image

接入注冊中心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注冊到nacos
    gateway:
      discovery:
        locator:
          enabled: true # 讓gateway從nacos中獲取服務信息
      routes:
        - id: product_route
          uri: lb://service-product
#          uri: http://127.0.0.1:8081
          order: 1
          predicates: # 斷言,當路由滿足全部斷言時進行轉發(fā)
            - Path=/product-serv/**
          filters:  # 過濾器
            - StripPrefix=1 # 刪除一級路徑

3、請求 http://127.0.0.1:7000/product-serv/product/1

6.4 Gateway的基本概念&請求處理流程

基本概念

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

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

請求處理流程

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

6.5 斷言

6.5.1 內置斷言工廠

image
基于Datetime類型的斷言工廠

此類型的斷言根據時間做判斷,主要有三個:

AfterRoutePredicateFactory: 接收一個日期參數,判斷請求日期是否晚于指定日期

BeforeRoutePredicateFactory: 接收一個日期參數,判斷請求日期是否早于指定日期

BetweenRoutePredicateFactory: 接收兩個日期參數,判斷請求日期是否在指定時間段內

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

RemoteAddrRoutePredicateFactory:接收一個IP地址段,判斷請求主機地址是否在地址段中

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

CookieRoutePredicateFactory:接收兩個參數,cookie 名字和一個正則表達式。 判斷請求cookie是否具有給定名稱且值與正則表達式匹配。

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

HeaderRoutePredicateFactory:接收兩個參數,標題名稱和正則表達式。判斷請求Header是否具有給定名稱且值與正則表達式匹配。

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

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

-Host=**.testhost.org
基于Method請求方法的斷言工廠

MethodRoutePredicateFactory:接收一個參數,判斷請求類型是否跟指定的類型匹配。

-Method=GET
基于Path請求路徑的斷言工廠

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

-Path=/foo/{segment}
基于Query請求參數的斷言工廠

QueryRoutePredicateFactory :接收兩個參數,請求param和正則表達式,判斷請求參數是否具有給定名稱且值與正則表達式匹配。

-Query=baz, ba.
基于路由權重的斷言工廠

WeightRoutePredicateFactory:接收一個[組名,權重], 然后對于同一個組內的路由按照權重轉發(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 自定義斷言工廠

我們來設定一個場景: 假設我們的應用僅僅讓age在(min,max)之間的人來訪問。

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

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

2、新建一個斷言工廠,實現斷言方法

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

    //構造函數
    public AgeRoutePredicateFactory() {
        super(Config.class);
    }

    //讀取配置文件的中參數值 給他賦值到配置類中的屬性上
    public List<String> shortcutFieldOrder() {
        //這個位置的順序必須跟配置文件中的值的順序對應
        return Arrays.asList("minAge", "maxAge");
    }

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

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

    //配置類,用于接收配置文件中的對應參數
    @Data
    @NoArgsConstructor
    public static class Config {
        private int minAge;//18
        private int maxAge;//60
    }
}

3、重啟網關服務,訪問:

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

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

6.6 過濾器

作用:在請求的傳遞過程中,對請求和響應做一些手腳。

在Gateway中,Filter的生命周期只有兩個:

  1. PRE:這種過濾器在請求被路由之前調用。我們可利用這種過濾器實現身份驗證、在集群中選擇請求的微服務、記錄調試信息等。
  2. POST:這種過濾器在路由到微服務以后執(zhí)行。這種過濾器可用來為響應添加標準的HTTP Header、收集統計信息和指標、將響應從微服務發(fā)送給客戶端等。

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

  1. GatewayFilter:應用到單個路由或者一個分組的路由上。
  2. GlobalFilter:應用到所有的路由上。

6.6.1 局部過濾器

局部過濾器是針對單個路由的過濾器。

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

Tips:每個過濾器工廠都對應一個實現類,并且這些類的名稱必須以GatewayFilterFactory結尾,這是Spring Cloud Gateway的一個約定,例如AddRequestHeader對應的實現類為AddRequestHeaderGatewayFilterFactory。

自定義局部過濾器

1、在配置文件中,添加一個Log的過濾器配置

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

2、自定義一個過濾器工廠

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

    //構造函數
    public LogGatewayFilterFactory() {
        super(Config.class);
    }

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

    //過濾器邏輯
    @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已經開啟了....");
                }
                if (config.isConsoleLog()) {
                    System.out.println("consoleLog已經開啟了....");
                }

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

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

3、訪問 http://localhost:7000/product-serv/product/1 查看控制臺

6.6.2 全局過濾器

全局過濾器作用于所有路由,無需配置。通過全局過濾器可以實現對權限的統一校驗,安全性驗證等功能。

內置全局過濾器
image

其中LBCFilter在我們寫uri: lb://service-product時,已經使用到了。

自定義全局過濾器

內置的過濾器已經可以完成大部分的功能,但是對于企業(yè)開發(fā)的一些業(yè)務功能處理,還是需要我們自己編寫過濾器來實現的,那么我們一起通過代碼的形式自定義一個過濾器,去完成統一的權限校驗。

eg。實現服務鑒權

當客戶端第一次請求服務時,服務端對用戶進行信息認證(登錄)

認證通過,將用戶信息進行加密形成token,返回給客戶端,作為登錄憑證

以后每次請求,客戶端都攜帶認證的token

服務端對token進行解密,判斷是否有效。

image

1、實現GlobalFilter, Ordered 接口,書寫自己的鑒權邏輯

//自定義全局過濾器需要實現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("鑒權失敗");
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        //調用chain.filter繼續(xù)向下游執(zhí)行
        return chain.filter(exchange);
    }

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

2、訪問

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

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

6.7 網關限流(感覺用途不大)

網關是所有請求的公共入口,所以可以在網關進行限流,而且限流的方式也很多,我們本次采用前面學過的Sentinel組件來實現網關的限流。Sentinel支持對SpringCloud Gateway、Zuul等主流網關進行限流。

image

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

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

路由維度

1、引入sentinel依賴

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

2、編寫配置類

基于Sentinel的Gateway限流是通過其提供的Filter來完成的,使用時只需注入對應的 SentinelGatewayFilter實例以及 SentinelGatewayBlockExceptionHandler 實例即可。

代碼見 cn.x5456.gateway.config.GatewayConfiguration

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

API維度

代碼見 cn.x5456.gateway.config.GatewayConfiguration

七、鏈路追蹤 Sleuth

在大型系統的微服務化構建中,一個系統被拆分成了許多模塊。這些模塊負責不同的功能,組合成系統,最終可以提供豐富的功能。在這種架構中,一次請求往往需要涉及到多個服務?;ヂ摼W應用構建在不同的軟件模塊集上,這些軟件模塊,有可能是由不同的團隊開發(fā)、可能使用不同的編程語言來實現、有可能布在了幾千臺服務器,橫跨多個不同的數據中心,也就意味著這種架構形式也會存在一些問題:

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

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

7.1 常用鏈路追蹤技術

cat 由大眾點評開源,基于Java開發(fā)的實時應用監(jiān)控平臺,包括實時應用監(jiān)控,業(yè)務監(jiān)控。集成方案是通過代碼埋點的方式來實現監(jiān)控,比如: 攔截器,過濾器等。對代碼的侵入性很大,集成成本較高。風險較大。

zipkin 由Twitter公司開源,開放源代碼分布式的跟蹤系統,用于收集服務的定時數據,以解決微服務架構中的延遲問題,包括:數據的收集、存儲、查找和展現。該產品結合spring-cloud-sleuth 使用較為簡單,集成很方便,但是功能較簡單。

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

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

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

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

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

基本概念

SpringCloud Sleuth主要功能就是在分布式系統中提供追蹤解決方案。它大量借用了Google Dapper的設計,先來了解一下Sleuth中的術語和相關概念。

Trace

由一組Trace Id相同的Span串聯形成一個樹狀結構。為了實現請求跟蹤,當請求到達分布式系統的入口端點時,只需要服務跟蹤框架為該請求創(chuàng)建一個唯一的標識(即TraceId),同時在分布式系統內部流轉的時候,框架始終保持傳遞該唯一值,直到整個請求的返回。那么我們就可以使用該唯一標識將所有的請求串聯起來,形成一條完整的請求鏈路。

Span

代表了一組基本的工作單元。為了統計各處理單元的延遲,當請求到達各個服務組件的時 候,也通過一個唯一標識(SpanId)來標記它的開始、具體過程和結束。通過SpanId的開始和結束時間戳,就能統計該span的調用時間,除此之外,我們還可以獲取如事件的名稱。請求信息等元數據。

Annotation

用它記錄一段時間內的事件,內部使用的重要注釋:

  • cs(Client Send)客戶端發(fā)出請求,開始一個請求的生命
  • sr(Server Received)服務端接受到請求開始進行處理, sr-cs = 網絡延遲(服務調用的時間)
  • ss(Server Send)服務端處理完畢準備發(fā)送到客戶端,ss - sr = 服務器上的請求處理時間
  • cr(Client Reveived)客戶端接受到服務端的響應,請求結束。 cr - sr = 請求的總時間

QuickStart

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

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

2、啟動3個微服務,訪問 http://127.0.0.1:7000/order-serv/order/prod/2 查看日志

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

通過TraceId,通過查看日志,我們可以將調用鏈路串起來。

但查看日志文件并不是一個很好的方法,當微服務越來越多日志文件也會越來越多,通過Zipkin可以將日志聚合,并進行可視化展示和全文檢索。

7.3 集成 ZipKin

Zipkin 是 Twitter 的一個開源項目,它基于Google Dapper實現,它致力于收集服務的定時數據,以解決微服務架構中的延遲問題,包括數據的收集、存儲、查找和展現。

我們可以使用它來收集各個服務器上請求鏈路的跟蹤數據,并通過它提供的REST API接口來輔助我們查詢跟蹤數據以實現對分布式系統的監(jiān)控程序,從而及時地發(fā)現系統中出現的延遲升高問題并找出系統性能瓶頸的根源。

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

Zipkin 提供了可插拔數據存儲方式:In-Memory、MySql、Cassandra 以及 Elasticsearch。

image

上圖展示了 Zipkin 的基礎架構,它主要由 4 個核心組件構成:

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

Zipkin分為兩端,一個是 Zipkin服務端,一個是 Zipkin客戶端,客戶端也就是微服務的應用??蛻舳藭渲梅斩说?URL 地址,一旦發(fā)生服務間的調用的時候,會被配置在微服務里面的 Sleuth 的監(jiān)聽器監(jiān)聽,并生成相應的 Trace 和 Span 信息發(fā)送給服務端。

7.3.1 安裝 ZipKin 服務端

jar包在git中有

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

訪問 http://127.0.0.1:9411

7.3.2 客戶端集成Zipkin

1、在3個微服務中添加依賴

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

2、在3個微服務中添加配置

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

3、重啟3個微服務,訪問 http://127.0.0.1:7000/order-serv/order/prod/2

4、進入zipkin,查看

image

7.4 持久化

Zipkin Server默認會將追蹤數據信息保存到內存,但這種方式不適合生產環(huán)境。Zipkin支持將追蹤數據持久化到mysql數據庫或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

八、服務配置中心 Nacos Config

微服務架構下關于配置文件存在的一些問題:

  1. 配置文件相對分散。在一個微服務架構下,配置文件會隨著微服務的增多變的越來越多,而且分散在各個微服務中,不好統一配置和管理。
  2. 配置文件無法區(qū)分環(huán)。微服務項目可能會有多個環(huán)境,例如:測試環(huán)境、預發(fā)布環(huán)境、生產環(huán)境。每一個環(huán)境所使用的配置理論上都是不同的,一旦需要修改,就需要我們去各個微服務下手動維護,這比較困難。
  3. 配置文件無法實時更新。我們修改了配置文件之后,必須重新啟動微服務才能使配置生效,這對一個正在運行的項目來說是非常不友好的。

基于上面這些問題,我們就需要配置中心的加入來解決這些問題。

配置中心的思路是:

  1. 首先把項目中各種配置全部都放到一個集中的地方進行統一管理,并提供一套標準的接口。
  2. 當各個服務需要獲取配置的時候,就來配置中心的接口拉取自己的配置。
  3. 當配置中心中的各種參數有更新的時候,也能通知到各個服務實時的過來同步最新的信息,使之動態(tài)更新。

當加入了服務配置中心之后,我們的系統架構圖會變成下面這樣:

image

8.1 常見的服務配置中心

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

Disconf是由百度開源的分布式配置中心。它是基于Zookeeper來實現配置變更后實時通知和生效的。

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

Nacos是SpingCloud alibaba技術棧中的一個組件,前面我們已經使用它做過服務注冊中心。其實它也集成了服務配置的功能,我們可以直接使用它作為服務配置中心。

8.2 Nacos Config 快速開始

1、在商品微服務中引入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)境標識

注:配置文件優(yōu)先級(由高到低):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的請求地址
    discoveryClientEnabled: false #讓nacos把它當成一個URL,而不要當做服務名
  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、重啟商品微服務

8.3 配置動態(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//動態(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;
    }
}

注:類似數據庫連接的配置修改后是不會動態(tài)更新的

8.4 配置共享

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

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

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

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

image

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

3、重啟系統

不同微服務中間共享配置

不同為服務之間實現配置共享的原理類似于文件引入,就是定義一個公共配置,然后在當前配置中引入。

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

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

2、刪除商品微服務-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 # 配置要實現動態(tài)配置刷新的配置
  profiles:
    active: dev # 環(huán)境標識

4、重啟微服務

8.5 nacos-config 的幾個概念

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

配置分組(Group) 配置分組用于將不同的服務可以歸類到同一分組。一般將一個項目的配置分到一組

配置集(Data ID) 在系統中,一個配置文件通常就是一個配置集。一般一個微服務的配置就是一個配置集

image

九、分布式事務 Seata

概念見 http://m.itdecent.cn/p/19492cfc71fb

9.1 Seata簡介

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

Seata的設計目標是對業(yè)務無侵入,因此從業(yè)務無侵入的2PC方案著手,在傳統2PC的基礎上演進。它把一個分布式事務理解成一個包含了若干分支事務的全局事務。全局事務的職責是協調其下管轄的分支事務達成一致,要么一起成功提交,要么一起失敗回滾。此外,通常分支事務本身就是一個關系數據庫的本地事務。

image

Seata主要由三個重要組件組成:

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

A服務的TM向TC申請開啟一個全局事務,TC就會創(chuàng)建一個全局事務并返回一個唯一的XID

A服務的RM向TC注冊分支事務,并及其納入XID對應全局事務的管轄

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

A服務開始遠程調用B服務,此時XID會在微服務的調用鏈上傳播

B服務的RM向TC注冊分支事務,并將其納入XID對應的全局事務的管轄

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

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

TC協調其管轄之下的所有分支事務,決定是否回滾

Seata實現2PC與傳統2PC的差別

  1. 架構層次方面,傳統2PC方案的 RM 實際上是在數據庫層,RM本質上就是數據庫自身,通過XA協議實現,而 Seata 的RM是以jar包的形式作為中間件層部署在應用程序這一側的。

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

?這樣會不會出現線程安全問題啊

9.2 快速開始

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

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("接收到{}號商品的下單請求,接下來調用商品微服務查詢此商品信息", pid);

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

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

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

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

        orderDao.save(order);

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

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

        return order;
    }
}

2、在ProductClient中添加扣減庫存

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

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

3、在商品微服務中添加扣減庫存方法

@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、啟動項目測試一下 http://127.0.0.1:8091/order3/prod/1

5、下載Seata服務端,修改conf/registry.conf

這是注冊中心和配置中心的配置

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

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

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

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

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

這里的語法為:service.vgroup_mapping.{your-service-gruop}=default ,中間的{your-service-gruop} 為自己定義的服務組名稱, 這里需要我們在程序的配置文件中配置。

7、初始化seata在nacos的配置

./nacos-config.sh 127.0.0.1

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

image

8、啟動seata服務

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

啟動后在 Nacos 的服務列表下面可以看到一個名為 serverAddr 的服務。

image

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

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微服務中添加以下依賴

<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微服務中添加DataSourceProxyConfig

Seata 是通過代理數據源實現事務分支的,所以需要配置 io.seata.rm.datasource.DataSourceProxy的Bean,且是@Primary默認的數據源,否則事務不會回滾,無法實現分布式事務

@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

和上面的那個一樣

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的服務端地址
        namespace: public
        group: SEATA_GROUP
    alibaba:
      seata:
        tx-service-group: service-order # 與第7步是對應的

14、開啟全局事務

@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、測試

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

9.3 seata運行流程分析

image

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

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

3、TM開啟全局事務開始,將XID全局事務id放在事務上下文中,通過feign調用也將XID傳入下游分支事務,每個分支事務將自己的Branch ID分支事務ID與XID關聯。

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

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

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容