天天看點

Java面試問題(八)—— 微服務五大元件之Hystrix和網關

作者:技術閑聊DD

今天給大家說一下Hystrix和網關方面的面試題。

Hystrix熔斷

什麼是服務雪崩?

服務雪崩效應是一種因服務提供者的不可用導緻服務調用者的不可用,并将不可用逐漸放大的過程。

雪崩形成的原因是什麼?

大緻可以分成三個階段:

  • 服務提供者不可用

    原因:

    (1)硬體故障: 硬體故障可能為硬體損壞造成的伺服器主機當機, 網絡硬體故障造成的服務提供者的不可通路。

    (2)程式Bug。

    (3)緩存擊穿:緩存擊穿一般發生在緩存應用重新開機, 所有緩存被清空時,以及短時間内大量緩存失效時。大量的緩存不命中, 使請求直擊後端,造成服務提供者超負荷運作,引起服務不可用.

    (4)使用者大量請求:在秒殺和大促開始前,如果準備不充分,使用者發起大量請求也會造成服務提供者的不可用.

  • 重試加大流量

    原因:

    (1)使用者重試:在服務提供者不可用後, 使用者由于忍受不了界面上長時間的等待,而不斷重新整理頁面甚至送出表單.

    (2)代碼邏輯重試:服務調用端的會存在大量服務異常後的重試邏輯。

  • 服務調用者不可用

    原因:

    同步等待造成的資源耗盡: 當服務調用者使用 同步調用 時, 會産生大量的等待線程占用系統資源。一旦線程資源被耗盡,服務調用者提供的服務也将處于不可用狀态, 于是服務雪崩效應産生了.

服務雪崩的應對政策都有哪些?

1. 流量控制

(1)網關限流:因為Nginx的高性能, 目前一線網際網路公司大量采用Nginx+Lua的網關進行流量控制, 由此而來的OpenResty也越來越熱門。

(2)使用者互動限流: 采用加載動畫,提高使用者的忍耐等待時間。送出按鈕添加強制等待時間機制.

關閉重試

2. 改進緩存模式

緩存預加載,同步改為異步重新整理。

3. 伺服器自動擴容

AWS的auto scaling

4. 服務調用者降級服務

(1)資源隔離:資源隔離主要是對調用服務的線程池進行隔離。

(2)對依賴服務進行分類:根據具體業務将依賴服務分為強依賴和若依賴。強依賴服務不可用會導緻目前業務中止,而弱依賴服務的不可用不會導緻目前業務的中止。

(3)不可用服務的調用快速失敗:不可用服務的調用快速失敗一般通過 逾時機制, 熔斷器 和熔斷後的 降級方法 來實作。

Hystrix是什麼?

在分布式系統中,每個服務都可能會調用很多其他服務,被調用的那些服務就是依賴服務,有的時候某些依賴服務出現故障也是很常見的。

Hystrix 可以讓我們在分布式系統中對服務間的調用進行控制,加入一些調用延遲或者依賴故障的容錯機制。Hystrix 通過将依賴服務進行資源隔離,進而阻止某個依賴服務出現故障時在整個系統所有的依賴服務調用中進行蔓延;同時Hystrix 還提供故障時的 fallback 降級機制。

總而言之,Hystrix 通過這些方法幫助我們提升分布式系統的可用性和穩定性。

Hystrix的提供的功能有什麼?

資源隔離、限流、熔斷、降級、運維監控。

Hystrix的設計原則是什麼?

  • 對依賴服務調用時出現的調用延遲和調用失敗進行控制和容錯保護。
  • 在複雜的分布式系統中,阻止某一個依賴服務的故障在整個系統中蔓延。比如某一個服務故障了,導緻其它服務也跟着故障。
  • 提供 fail-fast(快速失敗)和快速恢複的支援。
  • 提供 fallback 優雅降級的支援。
  • 支援近實時的監控、報警以及運維操作。

Hystrix的内部處理邏輯是什麼(原理)?

  1. 建構HystrixCommand或者HystrixObservableCommand對象
  2. 調用 command 執行方法
  3. 檢查是否開啟緩存
  4. 檢查是否開啟了斷路器
  5. 檢查線程池/隊列/信号量是否已滿
  6. 執行 command
  7. 斷路健康檢查
  8. 調用 fallback 降級機制
  9. 傳回成功的Response

Hystrix 實作資源隔離的技術是什麼?

Hystrix 裡面核心的一項功能,其實就是所謂的資源隔離,要解決的最核心的問題,就是将多個依賴服務的調用分别隔離到各自的資源池内。避免說對某一個依賴服務的調用,因為依賴服務的接口調用的延遲或者失敗,導緻服務所有的線程資源全部耗費在這個服務的接口調用上。一旦說某個服務的線程資源全部耗盡的話,就可能導緻服務崩潰,甚至說這種故障會不斷蔓延。

Hystrix 實作資源隔離,主要有兩種技術:線程池,信号量。預設情況下,Hystrix 使用線程池模式。

1. 信号量隔離政策

信号量隔離主要通過TryableSemaphore接口實作:

interface TryableSemaphore {

    // 嘗試擷取信号量
    public abstract boolean tryAcquire();
    // 釋放信号量    
    public abstract void release();
    // 
    public abstract int getNumberOfPermitsUsed();

}           

它的主要實作類主要有TryableSemaphoreNoOp,顧名思義,不進行信号量隔離,當采取線程隔離政策的時候将會注入該實作到HystrixCommand中,如果采用信号量的隔離政策時,将會注入TryableSemaphoreActual,但此時無法逾時和異步化,因為信号量隔離資源的政策無法指定指令的在特定的線程執行,進而無法控制線程的執行結果。

TryableSemaphoreActual實作相當簡單,通過AtomicInteger記錄目前請求的信号量的線程數(原子操作保證資料的一緻性),與初始化設定的允許最大信号量數進行比較numberOfPermits(可以動态調整),進而判斷是否允許擷取信号量,輕量級的實作,保證TryableSemaphoreActual無阻塞的操作方式。

需要注意的是每一個TryableSemaphore通過CommandKey與HystrixCommand一一綁定,在AbstractCommand的getExecutionSemaphore()有展現。

如果是采用信号量隔離的政策,将嘗試從緩存中擷取該CommandKey對應的TryableSemaphoreActual(緩存中不存在建立一個新的,并與CommandKey綁定放置到緩存中),否則傳回TryableSemaphoreNoOp不進行信号量隔離。

2. 線程隔離政策

在AbstractCommand的executeCommandWithSpecifiedIsolation()的方法中,線程隔離政策與信号隔離政策的操作主要差別是将Observable的執行線程通過threadPool.getScheduler()進行了指定,我們先檢視一下HystrixThreadPool的相關接口。

HystrixThreadPool是用來将HystrixCommand#run()(被HystrixCommand包裝的代碼)指定到隔離的線程中執行的。

public interface HystrixThreadPool {

    // 擷取線程池
   public ExecutorService getExecutor();
    // 擷取線程排程器
   public Scheduler getScheduler();
    //
   public Scheduler getScheduler(Func0<Boolean> shouldInterruptThread);

   // 标記一個指令已經開始執行 
   public void markThreadExecution();

   // 标記一個指令已經結束執行 
   public void markThreadCompletion();

   // 标記一個指令無法從線程池擷取到線程
   public void markThreadRejection();

   // 線程池隊列是否有空閑 
   public boolean isQueueSpaceAvailable();
    
 }
           

HystrixThreadPool是由HystrixThreadPool.Factory生成和管理的,是通過ThreadPoolKey(@HystrixCommand中threadPoolKey指定)與HystrixCommand進行綁定,它的預設實作為HystrixThreadPoolDefault,其内的線程池ThreadPoolExecutor是通過HystrixConcurrencyStrategy政策生成。

如果允許配置的maximumSize生效的話(allowMaximumSizeToDivergeFromCoreSize為true),在coreSize小于maximumSize時,會建立一個線程最大值為maximumSize的線程池,但會在相對不活動期間傳回多餘的線程到系統。否則就隻應用coreSize來定義線程池中線程的數量。dynamic字首說明這些配置都可以在運作時動态修改,如通過配置中心的方式。

touchConfig()的方法中可以動态調整線程池線程大小、線程存活時間等線程池的關鍵配置,在配置中心存在的情況下可以動态設定。

HystrixContextScheduler是Hystrix對rx中Scheduler排程器的重寫,主要為了實作在Observable未被訂閱時,不擷取線程執行指令,以及支援在指令執行過程中能夠打斷運作。

你們在項目中如何使用Hystrix?

  1. 在feign中已經內建了Hystrix元件相關的依賴,是以我們不需要額外的添加。
  2. feign中預設是關閉了Hystrix功能的,是以需要開啟熔斷功能,隻需要在application.yml檔案中添加如下配置:
feign:
  hystrix:
    enabled: true
## true:表示開啟hystrix熔斷功能,false表示關閉
           
  1. 然後需要為每個@FeignClient添加fallback屬性配置快速失敗處理類。該處理類是feign hystrix的邏輯處理類,必須實作被@FeignClient注解修飾的接口。比如我這裡定義為HiHystrix.java類,然後在該類上加上@Component注解,以spring bean的形式注入到IoC容器中。

zuul和spring gateway網關

什麼是網關?

網關是整個微服務API請求的入口,負責攔截所有請求,分發到服務上去。可以實作日志攔截、權限控制、解決跨域問題、限流、熔斷、負載均衡,隐藏服務端的ip,黑名單與白名單攔截、授權等,常用的網關有zuul和spring cloud gateway 。

網關的作用是什麼?

  • 網關對所有服務會話進行攔截。
  • 網關安全控制、統一異常處理、xxs、sql注入。
  • 權限控制、黑名單和白名單、性能監控、日志列印等。

網關(zuul或Gateway)和Nginx的差別?

  • 相同點:

    網關和Nginx都可以實作負載均衡、反向代理(隐藏真實ip位址),過濾請求,實作網關的效果。

  • 不同點:

    網關負載均衡實作:采用ribbon+eureka實作本地負載均衡

    Nginx負載均衡實作:采用服務端實作負載均衡

    Nginx相比網關功能會更加強大,因為Nginx整合一些腳本語言(Nginx+lua)

    Nginx适合于伺服器端負載均衡,網關适合微服務中實作網關。

過濾器和網關的對比?

  • 過濾器:對單個伺服器的請求進行攔截控制。
  • 網關:對所有的伺服器的請求進行攔截控制。

什麼是zuul?

Zuul包含了對請求的路由和過濾兩個最主要的功能:

  • 其中路由功能負責将外部請求轉發到具體的微服務執行個體上,是實作外部通路統一入口的基礎而過濾器功能則負責對請求的處理過程進行幹預,是實作請求校驗、服務聚合等功能的基礎。
  • Zuul和Eureka進行整合,将Zuul自身注冊為Eureka服務治理下的應用,同時從Eureka中獲得其他微服務的消息,也即以後的通路微服務都是通過Zuul跳轉後獲得。

zuul 的作用是什麼?

Zuul可以通過加載動态過濾機制,進而實作以下各項功能:

  • 驗證與安全保障: 識别面向各類資源的驗證要求并拒絕那些與要求不符的請求。
  • 審查與監控: 在邊緣位置追蹤有意義資料及統計結果,進而為我們帶來準确的生産狀态結論。
  • 動态路由: 以動态方式根據需要将請求路由至不同後端叢集處。
  • 壓力測試: 逐漸增加指向叢集的負載流量,進而計算性能水準。
  • 負載配置設定: 為每一種負載類型配置設定對應容量,并棄用超出限定值的請求。
  • 靜态響應處理: 在邊緣位置直接建立部分響應,進而避免其流入内部叢集。
  • 多區域彈性: 跨越AWS區域進行請求路由,旨在實作ELB使用多樣化并保證邊緣位置與使用者盡可能接近。

zuul用來做限流,但是為什麼要限流?

  • 防止不需要頻繁請求服務的請求惡意頻繁請求服務,造成伺服器資源浪費。
  • 防止不法分子惡意攻擊系統,擊穿系統盜取資料,防止資料安全隐患。防止系統高峰時期,對系統頻繁通路,給伺服器帶來巨大壓力。限流政策。

zuul如何實作限流?

Spring Cloud Zuul RateLimiter結合Zuul對RateLimiter進行了封裝,通過實作ZuulFilter提供了服務限流功能。

限流政策如下:

限流粒度/類型 說明
Authenticated User 針對請求的使用者進行限流
Request Origin 針對請求的Origin進行限流
URL 針對URL/接口進行限流
Service 針對服務進行限流,如果沒有配置限流類型,則此類型生效

多種粒度臨時變量儲存方式如下:

存儲方式 說明
IN_MEMORY 基于本地記憶體,底層是ConcurrentHashMap,預設的。
REDIS 基于redis存儲,使用時必須搭建redis
CONSUL consul 的kv存儲
JPA spring data jpa,基于資料庫
BUKET4J 使用一個Java編寫的基于令牌桶算法的限流庫

這裡重點說一下,如果 zuul 需要多節點部署,那就不能用 IN_MEMORY 存儲方式,比較常用的就是用REDIS。

  • 引入spring-cloud-zuul-ratelimit
<dependency>
    <groupId>com.marcosbarbero.cloud</groupId>
    <artifactId>spring-cloud-zuul-ratelimit</artifactId>
    <version>2.0.4.RELEASE</version>
</dependency>           

配置:

ratelimit: 
    key-prefix: springcloud-book #按粒度拆分的臨時變量key字首
    enabled: true #啟用開關
    repository: IN_MEMORY #key存儲類型,預設是IN_MEMORY本地記憶體,此外還有多種形式
    behind-proxy: true #表示代理之後
    default-policy: #全局限流政策,可單獨細化到服務粒度
      limit: 2 #在一個機關時間視窗的請求數量
      quota: 1 #在一個機關時間視窗的請求時間限制
      refresh-interval: 3 #機關時間視窗
      type: 
        - user #可指定使用者粒度
        - origin #可指定用戶端位址粒度
        - url #可指定url粒度
 
    policies:
      client-a:
      limit: 5
      quota: 5
      efresh-interval: 10
           

zuul的工作原理?

Zuul網關的核心是一系列的過濾器,這些過濾器可以對請求或者響應結果做一系列過濾,Zuul 提供了一個架構可以支援動态加載,編譯,運作這些過濾器,這些過濾器是使用責任鍊方式順序對請求或者響應結果進行處理的,這些過濾器不會直接進行通信,但是通過責任鍊傳遞的RequestContext參數可以共享資料。

Zuul的過濾器是由Groovy寫成,這些過濾器檔案被放在Zuul Server上的特定目錄下面,Zuul會定期輪詢這些目錄,修改過的過濾器會動态的加載到Zuul Server中以便過濾請求使用。

Java面試問題(八)—— 微服務五大元件之Hystrix和網關

zuul的Filter類型,以及作用是什麼?

Zuul大部分功能都是通過過濾器來實作的。Zuul中定義了四種标準過濾器類型,這些過濾器類型對應于請求的典型生命周期。

  1. PRE:這種過濾器在請求被路由之前調用。我們可利用這種過濾器實作身份驗證、在叢集中選擇請求的微服務、記錄調試資訊等。
  2. ROUTING:這種過濾器将請求路由到微服務。這種過濾器用于建構發送給微服務的請求,并使用Apache HttpClient或Netfilx Ribbon請求微服務。
  3. POST:這種過濾器在路由到微服務以後執行。這種過濾器可用來為響應添加标準的HTTP Header、收集統計資訊和名額、将響應從微服務發送給用戶端等。
  4. ERROR:在其他階段發生錯誤時執行該過濾器。

Zuul内部轉發請求有兩種,為服務下邊的RibbonRoutingFilter,普通http轉發的SimpleHostRoutingFilter。

Zuul 如何自定義filter?

在zuul項目中建立MyFilter繼承ZuulFilter

package com.example.gatewayservicezuulsimple.zuulFilter;
 
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import javax.servlet.http.HttpServletRequest;
 
public class MyFilter extends ZuulFilter {
    private final Logger logger = LoggerFactory.getLogger(MyFilter.class);
    @Override
    public String filterType() {
        return "pre"; //定義filter的類型,有pre、route、post、error四種
    }
 
    @Override
    public int filterOrder() {
        return 0; //定義filter的順序,數字越小表示順序越高,越先執行
    }
 
    @Override
    public boolean shouldFilter() {
        return true; //表示是否需要執行該filter,true表示執行,false表示不執行
    }
 
    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
 
        logger.info("--->>> TokenFilter {},{}", request.getMethod(), request.getRequestURL().toString());
 
        String token = request.getParameter("token");// 擷取請求的參數
 
        if (StringUtils.isNotBlank(token)) {
            ctx.setSendZuulResponse(true); //對請求進行路由
            ctx.setResponseStatusCode(200);
            ctx.set("isSuccess", true);
            return null;
        } else {
            ctx.setSendZuulResponse(false); //不對其進行路由
            ctx.setResponseStatusCode(400);
            ctx.setResponseBody("token is empty");
            ctx.set("isSuccess", false);
            return null;
        }
    }
} 
           

Zuul與Spring Cloud Gateway對比?

Spring Cloud Gateway基于Spring 5、Project Reactor、Spring Boot 2,使用非阻塞式的API,内置限流過濾器,支援長連接配接(比如 websockets),在高并發和後端服務響應慢的場景下比Zuul1的表現要好。

Zuul基于Servlet2.x建構,使用阻塞的API,沒有内置限流過濾器,不支援長連接配接。

Zuul的叢集搭建?

使用 Nginx+Zuul 實作網關叢集。

Java面試問題(八)—— 微服務五大元件之Hystrix和網關
#配置Zuul端口
server:
  port: 81
spring:
  application:
    name: zull-gateway-service    #服務名
#Eureka配置
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8761/eureka/    #注冊中心位址
      
# 配置網關反向代理,例如通路/api-member/** 直接重定向到member-service服務,實作路由轉發,隐藏服務的真實ip(服務都實在内網中)
#zull根據服務名,去Eureka擷取服務真實位址,并通過本地轉發,而且預設開啟Ribbon實作負載均衡
#預設讀取Eureka注冊清單 預設30秒間隔  
zuul:
 routes:
   api-a: #會員服務網關配置
     path: /api-member/**   #通路隻要是/api-member/ 開頭的直接轉發到member-service服務
     #服務名
     serviceId: member-service
   api-b: #訂單服務網關配置
     path: /api-order/**
     serviceId: order-service

           

zuul1.0和2.0的差別?

Zuul 1.x 基于同步 IO,Zuul 2.x 最大的改進就是基于 Netty Server 實作了異步 IO 來接入請求,同時基于 Netty Client 實作了到後端業務服務 API 的請求。這樣就可以實作更高的性能、更低的延遲。此外也調整了 filter 類型,将原來的三個核心 filter 顯式命名為:Inbound Filter、Endpoint Filter和 Outbound Filter。

Gateway的組成都有什麼?

  • 路由 : 網關的基本子產品,有ID,目标URI,一組斷言和一組過濾器組成。
  • 斷言:就是該路由的通路規則,可以用來比對來自http請求的任何内容,例如headers或者參數。
  • 過濾器:這個就是我們平時說的過濾器,用來過濾一些請求的,gateway有自己預設的過濾器,具體請參考官網,我們也可以自定義過濾器,但是要實作兩個接口,ordered和globalfilter。

簡單來說就是Route、Predicate、Filter三大核心元件。

Gateway的流程(工作原理)?

  1. 用戶端發送請求,會到達網關的DispatcherHandler處理,比對到RoutePredicateHandlerMapping。
  2. 根據RoutePredicateHandlerMapping比對到具體的路由政策。
  3. FilteringWebHandler擷取的路由的GatewayFilter數組,建立 GatewayFilterChain 處理過濾請求
  4. 執行我們的代理業務邏輯通路。

Gateway如何使用?

  1. 引入依賴
<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
           
  1. yml配置
server:
  port: 9524 #端口号
spring:
  application:
    name: cloud-gateway # 微服務注冊名稱
  cloud:
    gateway: #Gayeway配置
      routes:
        - id: payment_routh #路由的ID,沒有固定規則但要求唯一,建議配合服務名
          uri: http://localhost:8007   #比對後提供服務的路由位址
          predicates:
            - Path=/payment/get/**   #斷言,路徑相比對的進行路由

        - id: payment_routh2
          uri: http://localhost:8007
          predicates:
            - Path=/payment/lb/**   #斷言,路徑相比對的進行路由
eureka:
  instance:
    hostname: cloud-gateway-service
  client:
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://eureka7004.com:7004/eureka
           
  1. 項目主啟動類
package com.demo.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527 {
    public static void main(String[] args) {
            SpringApplication.run( GateWayMain9527.class,args);
        }
}
           

然後通過9524端口即可通路8007端口下的微服務。

還有一種方式是用代碼寫配置類

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
	RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
	return routes.route("path_route1", r -> r.path("/guonei")
			.uri("https://news.baidu.com/guonei"))
			.build();
}
           

繼續閱讀