cloud微服務架構組件揭祕—爲什麼選cloud

一、架構演進

1、傳統架構(單系統模式)
單系統模式架構
單系統模式:系統各個模塊全部耦合在一起,單個war包包含了所有的功能。
優缺點:

  • 優點:
    易部署:因爲是單個應用不需要其他操作直接成war包部署
    容易測試運行:也是因爲是單個應用的原因,不需要啓動其他服務
  • 缺點
    複雜性高:隨着業務的不斷迭代,項目的模塊也會不斷的增加,模塊與模塊直接的關係也會變得越來越複雜進而整個項目變得非常複雜,在後期新增功能時很有可能會影響到其他的模塊。
    可靠性低:因爲是單系統應用一旦某個模塊某個環節出現問題,導致整個項目無法正常運行
    可拓展性差:由於是單體應用,模塊與模塊之間的耦合度高,很容易導致牽一髮而動全身

最大負載量:幾千到幾萬的訪問量

2、集羣模式
集羣
集羣:集羣是一種物理形態,簡單來講集羣就是複製,可以是節點集羣,也可以單體應用集羣

  • 優點:
        高可伸縮性:服務器集羣具有很強的可伸縮性。隨着需求和負荷的增長,可以向集羣系統添加更多的服務器。在這樣的配置中,可以有多臺服務器執行相同的應用和數據庫操作。

       高可用性:高可用性是指,在不需要操作者干預的情況下,防止系統發生故障或從故障中自動恢復的能力。通過把故障服務器上的應用程序轉移到備份服務器上運行,集羣系統能夠把正常運行時間提高到大於99.9%,大大減少服務器和應用程序的停機時間。

       高可管理性:系統管理員可以從遠程管理一個、甚至一組集羣,就好像在單機系統中一樣。

  • 缺點
       部署維護費用較高

最大負載量:幾萬到幾十萬的訪問量

3、分佈式架構
分佈式架構
分佈式:分佈式服務顧名思義服務是分散部署在不同的機器上的,一個服務可能負責幾個功能,是一種面向SOA架構的,服務之間也是通過rpc來交互或者是webservice來交互的。邏輯架構設計完後就該做物理架構設計,系統應用部署在超過一臺服務器或虛擬機上,且各分開部署的部分彼此通過各種通訊協議交互信息,就可算作分佈式部署,生產環境下的微服務肯定是分佈式部署的,分佈式部署的應用不一定是微服務架構的,比如集羣部署,它是把相同應用複製到不同服務器上,但是邏輯功能上還是單體應用。
優缺點:

  • 優點:
    系統高可用:因爲分散式部署不同業務模塊,並且擁有多個節點,具有更強的容錯性,一旦某個節點出錯,可以使用另外的替代節點
    高拓展性:由於系統被拆分成各個業務模塊,模塊與模塊之間通過TCP方式交換數據,也就是說增加某個模塊不管過程中該模塊的業務是怎麼實現的,最後只需要提供外部調用接口就行。
    高協作:由於被拆分成模塊,業務可以劃分成模塊緯度分發給開發人員
    高效率:由於項目被拆分成不同模塊分發給開發人員開發,開發速度並行。
  • 缺點:
    架構複雜:服務與服務之間的調用方式,服務的負載…
    問題多樣性:分佈式事務,高併發等等…
    學習成本高:由於高複雜度和問題多樣性,學習起來肯定會碰到各種各樣的問題。

最大負載量:百萬+

4、微服務
微服務架構
微服務:簡單來說微服務就是很小的服務,小到一個服務只對應一個單一的功能,只做一件事。這個服務可以單獨部署運行,服務之間可以通過RPC來相互交互,每個微服務都是由獨立的小團隊開發,測試,部署,上線,負責它的整個生命週期。。微服務與分佈式的細微差別是,微服務的應用不一定是分散在多個服務器上,他也可以是同一個服務器,微服務就是分佈式的進一步細化。
優缺點

  • 優點:
    系統高可用:因爲把單個功能劃分成一個服務單獨部署,並且同樣可以進行多節點部署,容錯性毋庸置疑,並且擁有自身的熔斷降級等獨特功能,更加的保障了服務的高可用。
    高拓展性:由於系統被拆分成各個功能模塊,顆粒度較分佈式更加的細微,服務與服務之間通過TCP方式交換數據,也就是說增加某個功能不管過程中該業務功能是怎麼實現的,最後只需要提供外部調用接口就行。
    高協作:由於被拆分成模塊,業務可以劃分成模塊緯度分發給開發人員
    高效率:由於項目被拆分成不同模塊分發給開發人員開發,開發速度並行。
    生態齊全:微服務目前有spring於阿里體系,都提供了大量的微服務問題解決方案,例如服務訪問、服務治理、服務熔斷、服務安全、網關等等…
  • 缺點:
    架構複雜:服務與服務之間的調用方式,服務的負載…
    問題多樣性:分佈式事務,高併發等等…
    學習成本高:由於高複雜度和問題多樣性,學習起來肯定會碰到各種各樣的問題。
    架構部署成本高:支撐起一個微服務需要幾十甚至成百上千臺服務器
    維護成本大:因爲服務器數量一旦上去了,維護服務的人工成本就必須上去

最大負載量:百萬+

二、集羣、分佈式、微服務之間的關係

讓我們再來回顧下他們各自的定義:

  1. 集羣:集羣是一種物理部署形態,只是把同一服務,同一系統或者同一節點同時部署到不同機器上,單體應用可以集羣,分佈式節點可以集羣,微服務也可以集羣。

  2. 分佈式:分佈式是一種工作方式,把系統的不同模塊分散式部署到不同的機器上,一個服務可能負責幾個功能,是一種面向SOA架構的,服務之間也是通過rpc來交互或者是webservice來交互的。邏輯架構設計完後就該做物理架構設計,系統應用部署在超過一臺服務器或虛擬機上,且各分開部署的部分彼此通過各種通訊協議交互信息,就可算作分佈式部署,生產環境下的微服務肯定是分佈式部署的,分佈式部署的應用不一定是微服務架構的,比如集羣部署,它是把相同應用複製到不同服務器上,但是邏輯功能上還是單體應用。

  3. 微服務:微服務就是很小的服務,小到一個服務只對應一個單一的功能,只做一件事。

集羣和分佈式的區別:集羣是個物理形態,分佈式是個工作方式 分佈式是將不同的業務分佈在不同的機器上,而集羣則相反,它是把幾個服務器集中在一起實現同一業務。分佈式的每一個節點都可以是集羣,但是集羣不一定是分佈式。

  • 舉個栗子:豆瓣網,每天訪問人數超過上萬,他想要集羣,用戶發送請求,通過Nginx來分發請求看哪臺服務器負載較低就把請求給那臺機器,而分佈式從狹義來講也算是一種集羣但是組織較鬆散,它不像集羣,萬一有一天分佈式的哪一個節點服務掛了服務就運行不起來了,而集羣,一臺服務器掛了另一臺服務器可以頂替上去。

簡單講:分佈式是以縮短單個任務的執行時間來提升效率的,而集羣則是通過提高單位時間內執行的任務數來提升效率。 一個提高執行頻率一個提高執行速度。

  • 例如:如果一個任務由 10 個子任務組成,每個子任務單獨執行需 1 小時,則在一臺服務器上執行該任務需 10 小時。 採用分佈式方案,提供
    10 臺服務器,每臺服務器只負責處理一個子任務,不考慮子任務間的依賴關係,執行完這個任務只需一個小時。(這種工作模式的一個典型代表就是
    Hadoop 的 Map/Reduce 分佈式計算模型) 而採用集羣方案,同樣提供 10 臺服務器,每臺服務器都能獨立處理這個任務。假設有
    10 個任務同時到達,10 個服務器將同時工作,1 小時後,10 個任務同時完成,這樣,整身來看,還是 1 小時內完成一個任務!

分佈式和微服務的區別:前面我們說了,分佈式是種工作方式,是將服務分散部署在不同的機器上,一個服務可能負責幾個功能,是一種面向SOA架構的,服務之間也是通過rpc來交互或者是webservice來交互的。邏輯架構設計完後就該做物理架構設計,系統應用部署在超過一臺服務器或虛擬機上,且各分開部署的部分彼此通過各種通訊協議交互信息,就可算作分佈式部署,生產環境下的微服務肯定是分佈式部署的,分佈式部署的應用不一定是微服務架構的,比如集羣部署,它是把相同應用複製到不同服務器上,但是邏輯功能上還是單體應用。而微服務呢?微服務是一種架構設計方式 在做架構設計的時候,先做邏輯架構,再做物理架構,當你拿到需求後,估算過最大用戶量和併發量後,計算單個應用服務器能否滿足需求,如果用戶量只有幾百人的小應用,單體應用就能搞定,即所有應用部署在一個應用服務器裏,如果是很大用戶量,且某些功能會被頻繁訪問,或者某些功能計算量很大,建議將應用拆解爲多個子系統,各自負責各自功能,這就是微服務架構。

三、微服務解決方案——cloud架構

  1. 服務治理————euraka、Zookeeper、Consul
  2. 服務調用————feign
  3. 負載均衡————ribbon
  4. 服務熔斷/降級————hystrix
  5. 網關權限————zuul
  6. 配置中心————springcloud config
  7. 鏈路追蹤————Sleuth和ZipKin

在講之前先了解下分佈式核心理論——CAP定理

https://blog.csdn.net/weixin_42083036/article/details/105968452

1、服務治理

1.1、Eureka架構中的三個核心角色:
  • 服務註冊中心
          Eureka的服務端應用,提供服務註冊和發現功能,就是剛剛我們建立的euraka-server。

  • 服務提供者
            提供服務的應用,可以是SpringBoot應用,也可以是其它任意技術實現,只要對外提供的是Rest風格服務即可。

  • 服務消費者
            消費應用從註冊中心獲取服務列表,從而得知每個服務方的信息,知道去哪裏調用服務方。

euraka原理
Register: 服務註冊
服務的提供者,將自身註冊到註冊中心,服務提供者也是一個 Eureka Client。當 Eureka Client 向 Eureka Server 註冊時,它提供自身的元數據,比如 IP 地址、端口,運行狀況指示符 URL,主頁等。

Renew: 服務續約
Eureka Client 會每隔 30 秒發送一次心跳來續約。 通過續約來告知 Eureka Server 該 Eureka Client 運行正常,沒有出現問題。 默認情況下,如果 Eureka Server 在 90 秒內沒有收到 Eureka Client 的續約,Server 端會將實例從其註冊表中刪除,此時間可配置,一般情況不建議更改。
服務續約中重要的屬性

服務續約任務的調用間隔時間,默認爲30秒
eureka.instance.lease-renewal-interval-in-seconds=30

服務失效的時間,默認爲90秒。
eureka.instance.lease-expiration-duration-in-seconds=90

Eviction 服務剔除
當 Eureka Client 和 Eureka Server 不再有心跳時,Eureka Server 會將該服務實例從服務註冊列表中刪除,即服務剔除。

Cancel: 服務下線
Eureka Client 在程序關閉時向 Eureka Server 發送取消請求。 發送請求後,該客戶端實例信息將從 Eureka Server 的實例註冊表中刪除。該下線請求不會自動完成,它需要通過調用“關機”命令:

DiscoveryManager.getInstance().shutdownComponent();

Remote Call: 遠程調用
當 Eureka Client 從註冊中心獲取到服務提供者信息後,就可以通過 Http 請求調用對應的服務;服務提供者有多個時,Eureka Client 客戶端會通過 Ribbon 自動進行負載均衡。

2、註冊中心與客戶端整合boot使用

首先找到boot對應的cloud版本
詳細網址https://start.spring.io/actuator/info

cloud boot
Finchley.M2 “Spring Boot >=2.0.0.M3 and <2.0.0.M5”
Finchley.M3 “Spring Boot >=2.0.0.M5 and <=2.0.0.M5”
Finchley.M4 “Spring Boot >=2.0.0.M6 and <=2.0.0.M6”
Finchley.M5 “Spring Boot >=2.0.0.M7 and <=2.0.0.M7”
Finchley.M6 “Spring Boot >=2.0.0.RC1 and <=2.0.0.RC1”
Finchley.M7 “Spring Boot >=2.0.0.RC2 and <=2.0.0.RC2”
Finchley.M9 “Spring Boot >=2.0.0.RELEASE and <=2.0.0.RELEASE”
Finchley.RC1 “Spring Boot >=2.0.1.RELEASE and <2.0.2.RELEASE”
Finchley.RC2 “Spring Boot >=2.0.2.RELEASE and <2.0.3.RELEASE”
Finchley.SR4 “Spring Boot >=2.0.3.RELEASE and <2.0.999.BUILD-SNAPSHOT”
Finchley.BUILD-SNAPSHOT “Spring Boot >=2.0.999.BUILD-SNAPSHOT and <2.1.0.M3”
Greenwich.M1 “Spring Boot >=2.1.0.M3 and <2.1.0.RELEASE”
Greenwich.SR2 “Spring Boot >=2.1.0.RELEASE and <2.1.9.BUILD-SNAPSHOT”
Greenwich.BUILD-SNAPSHOT “Spring Boot >=2.1.9.BUILD-SNAPSHOT and <2.2.0.M4”
Hoxton.SR1 Spring Boot >=2.2.0.M4 and <2.2.3.BUILD-SNAPSHOT
Hoxton.BUILD-SNAPSHOT Spring Boot >=2.2.3.BUILD-SNAPSHOT
2.1、註冊中心

導入依賴

 <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

配置文件

server:
  port: 8761

eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

啓動類添加@EnableEurekaServer註解
啓動 訪問 localhost:8761
在這裏插入圖片描述
假如進入的界面是如下
在這裏插入圖片描述
出現上述情況可以在配置文件中加入 下面配置(生產環境建議打開)

eureka.server.enable-self-preservation=true      //開啓自我保護

什麼叫自我保護:Eureka在運行期間會統計心跳失敗的比例,在15分鐘內是否低於85%,如果出現了低於的情況,Eureka Server會將當前的實例註冊信息保護起來,同時提示一個警告,一旦進入保護模式,Eureka Server將會嘗試保護其服務註冊表中的信息,不再刪除服務註冊表中的數據。也就是不會註銷任何微服務。

註冊中心集羣

server:
  port: 8761

eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://localhost:8762/eureka/
server:
  port: 8762

eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

註冊中心可以相互註冊不過需要打開註冊發現 如下

 eureka.client.registerWithEureka=true

在這裏插入圖片描述

2.2、客戶端

依賴

 <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

配置文件 (這裏默認取application.name作爲服務發現名)

#網關名稱
spring:
  application:
    name: api-gateway

#註冊中心客戶端
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka

啓動服務 訪問localhost:8761
在這裏插入圖片描述

2、服務調用

RPC與HTTP
在這裏插入圖片描述
spring自帶服務調用工具RestTemplate
在這裏插入圖片描述

3、負載均衡

3.1、負載均衡是什麼?

簡單來講就是原先任務量無腦分配給一個人做,現在有了一個專門管理任務的“管家”,他能觀察每臺機器的壓力,根據機器壓力的不同來分配任務。

3.2、負載均衡工具Ribbon

**核心是@Loadbalannce註解以及 LoadBalancerClient **
上面我們瞭解了spring自帶的遠程調用工具RestTemplate,restyemplate是沒有管家的功能的,於是有人就想把他改造成管家類幫助實現負載均衡。
例子:創建一個order-service服務 配置如下 再創建product-service服務 8764和8765 統一註冊到8761
在這裏插入圖片描述
此時訂單服務需要調用商品服務根據id查詢商品使用resttemplate類
在這裏插入圖片描述
實現負載
1、啓動類下注入並添加@LoadBalanced註解

   @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

2、修改service方法

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private LoadBalancerClient loadBalancerClient;
    
 public String testLoadB(int id){
        //獲取註冊列表中名稱爲product-service的註冊實例
        ServiceInstance instance = loadBalancerClient.choose("product-service");
        //使用佔位符控制host和port
        String url = String.format("http://%s:%s/api/product/find?id="+id,instance.getHost(),instance.getPort(),Object.class);
       return restTemplate.getForObject(url,String.class);
    }

結果

冰箱1 I am from 8764
冰箱1 I  am from 8765

負載均衡策略選擇(一般默認策略)
1、如果每個機器的配置差不多,不建議修改策略默認(輪詢)就可以
2、如果有某臺機器的配置比較號就可以改用 權重策略

3.3、ribbon的集成—feign

定義:Feign是一個聲明性的web服務客戶端,它使編寫web服務客戶機變得更容易。使用Feign創建接口並對其進行註釋。它有可插入的註釋支持,包括Feign註釋和JAX-RS註釋。Feign還支持可插入式的編碼器和解碼器。Spring Cloud增加了對Spring MVC註釋的支持,並支持在Spring Web中使用默認的HttpMessageConverters。Spring Cloud集成了Ribbon和Eureka,在使用Feign時提供負載平衡的http客戶端。(來自有道翻譯)
總結2點:

  1. feign是接口加註解
  2. feign整合了ribbon

如何使用

  • 步驟:
    1、導入依賴
  <!--引入feign依賴  -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

       2、啓動類添加@EnableFeignClients
在這裏插入圖片描述
       3、書寫feign客戶端
在這裏插入圖片描述
商品服務方法
在這裏插入圖片描述
訂單服務降級方法
在這裏插入圖片描述
演示
成功時調用返回:
在這裏插入圖片描述
此時product_service服務宕機/調用時間過長後返回:
在這裏插入圖片描述
在這裏插入圖片描述
訂單服務降級後可選擇調用方法(可做服務預警)
在這裏插入圖片描述

    /**
     *
     * @param userId
     * @param product_id
     * @return
     */
    //注方法簽名一定要和api接口參數一致
    private RetResult saveOrderFail(int userId, int product_id) {
        //監控報警
        String saveOrderKey = "save-order";
        String sendValue = redisTemplate.opsForValue().get(saveOrderKey);
        new Thread(()->{
            if(StringUtils.isBlank(sendValue)){
                System.out.println("通知開發人員,服務接口已經掛了");
                //TODO 調用一個短信服務接口   20秒過期
                redisTemplate.opsForValue().set(saveOrderKey,"save-order-fail",20, TimeUnit.SECONDS);
            }else{
                System.out.println( redisTemplate.opsForValue().get(saveOrderKey));
                System.out.println("已發送短信,20秒內不會重複發送");
            }
        }).start();
        Map<String, Object> msg = new HashMap<String, Object>();
        msg.put("code", -1);
        msg.put("msg", "下單人數過多,您被擠下線了,請稍後再試!");
        //如果product服務掛了 將模擬的商品數據傳送給訂單服務
        msg.put("data",productServiceFallback.findByid(product_id));
        return RetResponse.makeRsp(msg.get("code").toString(),msg.get("msg").toString(),msg.get("data"));
    }

注意問題https://blog.csdn.net/weixin_42083036/article/details/91982930

4、Hystrix(豪豬)斷路器

在這裏插入圖片描述
   1、什麼是Hystrix?
         1)hystrix對應的中文名字是“豪豬”
         2)hystrix 英[hɪst’rɪks] 美[hɪst’rɪks]
   2、爲什麼要用?
            在一個分佈式系統裏,一個服務依賴多個服務,可能存在某個服務調用失敗,比如超時、異常等,如何能夠保證在一個依賴出問題的情況下,不會導致整體服務失敗,通過Hystrix就可以解決。
   3、提供了熔斷、隔離、Fallback、cache、監控等功能
   4、熔斷後怎麼處理?
          1. 出現錯誤之後可以 fallback 錯誤的處理信息
          2. 兜底數據(服務降級)例子:淘寶秒殺

   作用: 保護機制 防止上游服務調用下游服務失敗而導致整個微服務異常
   雪崩問題:當用戶發送請求,由於某個服務響應速度慢/失敗導致該請求無法快速應答,從而導致後續的所有請求無法正常響應堆積在內存中最終OOM 服務掛了

    圖例:當前系統中有A,B,C三個服務,服務A是上游,服務B是中游,服務C是下游。它們的調用鏈如下:
在這裏插入圖片描述
      一旦下游服務C因某些原因變得不可用,積壓了大量請求,服務B的請求線程也隨之阻塞。線程資源逐漸耗盡,使得服務B也變得不可用。緊接着,服務A也變爲不可用,整個調用鏈路被拖垮。
鏈路雪崩
解決手段:

  • 線程隔離
  • 服務熔斷
1、 線程隔離:

在這裏插入圖片描述
示意圖解讀:

Hystrix通過命令模式,將每個類型的業務請求封裝成對應的命令請求,比如查詢訂單->訂單Command,查詢商品->商品Command,查詢用戶->用戶Command。每個類型的Command對應一個線程池。創建好的線程池是被放入到ConcurrentHashMap中,比如查詢訂單:

final static ConcurrentHashMap<String, HystrixThreadPool> threadPools = new ConcurrentHashMap<String, HystrixThreadPool>();
threadPools.put(“hystrix-order”, new HystrixThreadPoolDefault(threadPoolKey, propertiesBuilder));

當第二次查詢訂單請求過來的時候,則可以直接從Map中獲取該線程池。具體流程如下圖:
在這裏插入圖片描述
        Hystrix爲每個依賴服務調用分配一個小的線程池,如果線程池已滿調用將被立即拒絕,默認不採用排隊.加速失敗判定時間。
        用戶的請求將不再直接訪問服務,而是通過線程池中的空閒線程來訪問服務,如果線程池已滿,或者請求超時,則會進行降級處理,什麼是服務降級?

  1. 服務降級:優先保證核心服務,而非核心服務不可用或弱可用。
  2. 一旦線程池滿、請求超時 就會觸發服務降級

總結:執行依賴代碼的線程與請求線程(比如Tomcat線程)分離,請求線程可以自由控制離開的時間,這也是我們通常說的異步編程,Hystrix是結合RxJava來實現的異步編程。通過設置線程池大小來控制併發訪問量,當線程飽和的時候可以拒絕服務,防止依賴問題擴散。
在這裏插入圖片描述

1.1、服務降級代碼實現:

1.1.1、引入依賴

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

1.1.2、加入註解 @EnableCircuitBreaker
如果閒註解太多可以使用 @SpringCloudApplication組合註解替換

1.1.3、編寫降級接口 一般寫在feign調用接口下方(服務提供方)

@Controller
@RequestMapping("consumer/user")
public class UserController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping
    @ResponseBody
    @HystrixCommand(fallbackMethod = "queryUserByIdFallBack")
    public String queryUserById(@RequestParam("id") Long id) {
        String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class);
        return user;
    }

    public String queryUserByIdFallBack(Long id){
        return "請求繁忙,請稍後再試!";
    }
}

        要注意,因爲熔斷的降級邏輯方法必須跟正常邏輯方法保證:相同的參數列表和返回值聲明。失敗邏輯中返回User對象沒有太大意義,一般會返回友好提示。所以我們把queryById的方法改造爲返回String,反正也是Json數據。這樣失敗邏輯中返回一個錯誤說明,會比較方便。

        當然你會提問是不是所有的方法都必須添加註解 @HystrixCommand 非也非也 也可直接設置全局降級方法

@Controller
@RequestMapping("consumer/user")
@DefaultProperties(defaultFallback = "fallBackMethod") // 指定一個類的全局熔斷方法
public class UserController {

   @Autowired
   private RestTemplate restTemplate;

   @GetMapping
   @ResponseBody
   @HystrixCommand // 標記該方法需要熔斷
   public String queryUserById(@RequestParam("id") Long id) {
       String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class);
       return user;
   }

   /**
    * 熔斷方法
    * 返回值要和被熔斷的方法的返回值一致
    * 熔斷方法不需要參數
    * @return
    */
   public String fallBackMethod(){
       return "請求繁忙,請稍後再試!";
   }
}

@DefaultProperties(defaultFallback = “defaultFallBack”):在類上指明統一的失敗降級方法
@HystrixCommand:在方法上直接使用該註解,使用默認的降級方法。
defaultFallback:默認降級方法,不用任何參數,以匹配更多方法,但是返回值一定一致

注:局部降級方法需要入參和返回都必修和熔斷方法一致,全局的返回參數必須和被熔斷方法一致參數列表必須爲空

        在之前的案例中,請求在超過1秒後都會返回錯誤信息,這是因爲Hystix的默認超時時長爲1,我們可以通過配置修改這個值:
        我們可以通過hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds來設置Hystrix超時時間。如下

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 6000 # 設置hystrix的超時時間爲6000ms
2、 服務熔斷(參考保險絲原理)

       正所謂刮骨療毒,壯士斷腕。在這種時候,就需要我們的熔斷機制來挽救整個系統。熔斷就是上游服務隊下游服務的一種處理方式。

  • 2.1、熔斷原理
         當服務正常,熔斷處於閉合狀態,所有的請求都可以通過。如果當請求失敗次數比例超過50%開啓熔斷,後面的所有請求(無論是正常請求還是不正常請求)均被熔斷,無法訪問服務。進入休眠期,隨後進入半開狀態,釋放一部分請求,假如此時的請求健康,斷路器關閉,請求可以正常訪問服務但是如果後續請求都是不健康則再次打開斷路器,再次休眠計時如此進行循環。

熔斷狀態:

  • Closed:關閉狀態,所有請求都正常訪問。
  • Open:打開狀態,所有請求都會被降級。Hystix會對請求情況計數,當一定時間內失敗請求百分比達到閾值,則觸發熔斷,斷路器會完全打開。默認失敗比例的閾值是50%,請求次數最少不低於20次。
  • HalfOpen:半開狀態,open狀態不是永久的,打開後會進入休眠時間(默認是5S)。隨後斷路器會自動進入半開狀態。此時會釋放部分請求通過,若這些請求都是健康的,則會完全關閉斷路器,否則繼續保持打開,再次進行休眠計時
@GetMapping("{id}")
@HystrixCommand
public String queryUserById(@PathVariable("id") Long id){
    if(id == 1){
        throw new RuntimeException("太忙了");
    }
    String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class);
    return user;
}

我們準備兩個請求窗口:
一個請求:http://localhost/consumer/user/1,註定失敗 //如果請求1設置休眠超時
一個請求:http://localhost/consumer/user/2,肯定成功 //不做任何超時

當我們瘋狂訪問id爲1的請求時(超過20次),就會觸發熔斷。斷路器會斷開,一切請求都會被降級處理。
此時你訪問id爲2的請求,會發現返回的也是失敗,而且失敗時間很短,只有幾毫秒左右:
不過,默認的熔斷觸發要求較高,休眠時間窗較短,爲了測試方便,我們可以通過配置修改熔斷策略:

circuitBreaker.requestVolumeThreshold=10
circuitBreaker.sleepWindowInMilliseconds=10000
circuitBreaker.errorThresholdPercentage=50

解讀:

  • requestVolumeThreshold:觸發熔斷的最小請求次數,默認20
  • errorThresholdPercentage:觸發熔斷的失敗請求最小佔比,默認50%
  • sleepWindowInMilliseconds:休眠時長,默認是5000毫秒

5、zuul網關

1、架構圖:

在這裏插入圖片描述

2、定義(大型攔截器)

        服務網關是微服務架構中一個不可或缺的部分。通過服務網關統一向外系統提供REST API的過程中,除了具備服務路由、均衡負載功能之外,它還具備了權限控制等功能。Spring Cloud Netflix中的Zuul就擔任了這樣的一個角色,爲微服務架構提供了前門保護的作用,同時將權限控制這些較重的非業務邏輯內容遷移到服務路由層面,使得服務集羣主體能夠具備更高的可複用性和可測試性。

  • 作用:

       1、統一入口:未全部爲服務提供一個唯一的入口,網關起到外部和內部隔離的作用,保障了後臺服務的安全性。
       2、鑑權校驗:識別每個請求的權限,拒絕不符合要求的請求。
       3、動態路由:動態的將請求路由到不同的後端集羣中。
       4、減少客戶端與服務端的耦合:服務可以獨立發展,通過網關層來做映射。

3、快速運用

導入依賴

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

因爲zuul的使用伴隨着負載均衡 所以需要註冊到eureka上使用纔有意義

4、zuul網關的四種路由配置

  • 使用路徑名直接訪問 容易出錯 需要把負載均衡關閉 因爲此時沒有負載均衡效果
zuul:
  routes:
    service-provider:  #路由名稱 隨意取名 習慣取服務名
      path: /service-leyou/**
      url: http//localhost:8087
  • 通過服務ID(首先得註冊eureka 由eureka中心的註冊服務ID)
zuul:
  routes:
    service-provider: #路由名稱 隨意取名 習慣取服務名
      path: /service-leyou/**
      #url: http//localhost:8087
      #serviceID: service-leyou
  • 直接通過路由名稱和eureka服務ID一致 (推薦)
zuul:
  routes:
    service-leyou: /service-leyou/**  #此時 前面的事服務ID 後面是路徑前綴
  prefix: /api    #指定路由前綴 用以區分網關與服務  
  • 不配置路由 默認第三種服務ID尋找路徑

5、zuul過濾器——ZuulFilter

public abstract ZuulFilter implements IZuulFilter{

    abstract public String filterType();

    abstract public int filterOrder();
    
    boolean shouldFilter();// 來自IZuulFilter

    Object run() throws ZuulException;// IZuulFilter
}
  • shouldFilter:返回一個Boolean值,判斷該過濾器是否需要執行。返回true執行,返回false不執行。
  • run:過濾器的具體業務邏輯。
  • filterType:返回字符串,代表過濾器的類型。包含以下4種:
    • pre:請求在被路由之前執行
    • route:在路由請求時調用
    • post:在route和errror過濾器之後調用
    • error:處理請求時發生錯誤調用
  • filterOrder:通過返回的int值來定義過濾器的執行順序,數字越小優先級越高。

ZuulFilter 的生命週期
在這裏插入圖片描述
週期圖流程解讀

  • 當 pre 出現異常 ——>error ——>post——>HTTP
  • 當 routing 出現異常——>error——>post——>HTTP
  • 當 error 出現異常——>post——>HTTP
  • 當 post 出現異常——>error——>HTTP

自定義filter

/**
 * @program: leyou
 * @description: //zuul網關
 * @author: wyy
 * @create: 2020-01-05 17:28
 **/
@Component
public class LoginFilter extends ZuulFilter {
    /**
     * 過濾器類型 pre、route、post、error
     * @return
     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * 執行順序 數字越小優先級越高
     * @return
     */
    @Override
    public int filterOrder() {
        return 10;
    }

    /**
     * 是否執行該過濾器 如果true則執行run方法 false不執行run方法
     * @return
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 編寫過濾器的業務邏輯
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        //獲取zuul上下文
        RequestContext context = RequestContext.getCurrentContext();
        //獲取request請求對象
        HttpServletRequest request = context.getRequest();
        //判斷是否存在令牌
        String token = request.getHeader("token");
        if(StringUtils.isBlank(token)){
            String token1 = request.getParameter("token");
            if(StringUtils.isBlank(token1)){
                //false 攔截不轉發請求
                context.setSendZuulResponse(false);
                //設置響應狀態碼  身份未認證
                context.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
                //設置響應體
                context.setResponseBody("request error!");
            }
        }
        //返回值爲null 表示該過濾器什麼都不做
        return null;
    }
}

6、自定義熔斷時間——zuul自動集成了負載均衡和熔斷功能默認熔斷時間是1S

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 2000 # 設置hystrix的超時時間爲6000ms

使用zuul網關整合guava 框架實現接口令牌限流
實現原理圖:
在這裏插入圖片描述
流程圖剖析:
     使用zuul過濾器filter實現請求攔截,在filter設置一個全局變量令牌容器,創造令牌一般是1000個,每一次請求都會從容器中帶走一個令牌,擁有令牌的請求是可以正常通過的,但是沒有令牌的就必須等待容器重新生成令牌.
實現源碼:

/**
 * 使用guava限流框架
 * @author wyy
 * @date
 */
public class OrderRateLimitFilter extends ZuulFilter {

    //每一秒生成1000個令牌   根據接口的壓力來規定有多少個令牌
    private static final RateLimiter RATE_LIMITER = RateLimiter.create(1000);

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return -4;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        /* System.out.println(request.getRequestURI());*/
        //需要對那個接口限流就把哪個接口寫下去
        //ACL   攔截下面的接口 忽略大小寫
        if ("/apigateway/order/api/v1/order/save".equalsIgnoreCase(request.getRequestURI())) {
            return true;
        }
        return false;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
        if (!RATE_LIMITER.tryAcquire()) {
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(HttpStatus.TOO_MANY_REQUESTS.value());
        }
        return null;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章