天天看點

SpringCloud (七) --------- Zuul 網關前言一、Zuul 介紹二、路由功能三、通配符四、過濾器五、zuul 過濾器的禁用六、Zuul 的異常處理七、Zuul 的熔斷降級

目錄

  • 前言
  • 一、Zuul 介紹
  • 二、路由功能
  • 三、通配符
  • 四、過濾器
  • 五、zuul 過濾器的禁用
  • 六、Zuul 的異常處理
  • 七、Zuul 的熔斷降級

前言

通過前面内容的學習,我們已經可以基本搭建出一套簡略版的微服務架構了,我 們有注冊中心 Eureka,可以将服務注冊到該注冊中心中,我們有 Ribbon 或Feign 可以實作對服務負載均衡地調用,我們有 Hystrix 可以實作服務的熔斷。

我們來看一下下面的微服務架構圖:

SpringCloud (七) --------- Zuul 網關前言一、Zuul 介紹二、路由功能三、通配符四、過濾器五、zuul 過濾器的禁用六、Zuul 的異常處理七、Zuul 的熔斷降級

在上面的架構圖中,我們的服務包括:内部服務 Service A 和内部服務 Service B,這兩個服務都是叢集部署,每個服務部署了 3 個執行個體,他們都會通過 Eureka Server 注冊中心注冊與訂閱服務,而 Open Service 是一個對外的服務,也是叢集部署,外部調用方通過負載均衡裝置調用 Open Service 服務,比如負載均衡使用 Nginx、LVS、HAProxy等。但這樣的實作是否合理,或者是否有更好的實作方式呢?接下來我們主要圍繞該問題展開讨論。

1、如果我們的微服務中有很多個獨立服務都要對外提供服務,那麼我們要如何 去管理這些接口? 特别是當項目非常龐大的情況下要如何管理 ?

2、在微服務中,一個獨立的系統被拆分成了很多個獨立的服務,為了確定安全,權限管理也是一個不可回避的問題,如果在每一個服務上都添加上相同的權限驗 證代碼來確定系統不被非法通路,那麼工作量也就太大了,而且維護也非常不友善。

為了解決上述問題,微服務架構中提出了 API 網關的概念,它就像一個安檢站一樣,所有外部的請求都需要經過它的排程與過濾,然後 API 網關來實作請求路由、負載均衡、權限驗證等功能。

那麼 Spring Cloud 這個一站式的微服務開發架構基于 Netflix Zuul 實作了 Spring Cloud Zuul,采用 Spring Cloud Zuul 即可實作一套 API 網關服務。

一、Zuul 介紹

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

其中路由功能負責将外部請求轉發到具體的微服務執行個體上,是實作外部通路統一入口的基礎,過濾功能則負責對請求的處理過程進行幹預,是實作請求校驗、服務聚合等功能的基礎。

Zuul 和 Eureka 進行整合,将 Zuul 自身注冊為 Eureka 服務治理下的應用,同時從 Eureka 中獲得其他微服務的資訊,也即以後的通路微服務都是通過 Zuul 跳轉後獲得。

二、路由功能

A、項目加入依賴

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

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

由于 Zuul 最終會注冊進 Eureka,是以我們此處也依賴了 Eureka。

B、配置檔案

server.port=80

#是 eureka 注冊中心首頁的Application這一欄
spring.application.name=springcloud-service-zuul

#每間隔2s,向服務端發送一次心跳,證明自己依然"存活"
eureka.instance.lease-renewal-interval-in-seconds=2
#告訴服務端,如果我10s之内沒有給你發心跳,就代表我故障了,将我踢出掉
eureka.instance.lease-expiration-duration-in-seconds=10
#告訴服務端,服務執行個體以IP作為連結,而不是取機器名
eureka.instance.prefer-ip-address=true
#告訴服務端,服務執行個體的id,id必須要唯一,是eureka注冊中心首頁的Status這一欄
eureka.instance.instance-id=34-springcloud-service-zuul

#eureka注冊中心的連接配接位址
eureka.client.service-url.defaultZone=http://192.168.160.133:8761/eureka,http://192.168.160.133:8762/eureka,http://192.168.160.133:8763/eureka
           

C、啟動類

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

這樣簡單的 zuul 就搭建好了, 啟動項目我們即可通過zuul 然後加上對應的微服務名字通路微服務,比如:

http://localhost:80/springcloud-service-portal/cloud/goodsFeign

http://localhost:80/ 這個是zuul本身位址

springcloud-service-portal 這個是要調用的項目名稱

/cloud/goodsFeign 這個是被調用的contrller上的接口路徑

在實際開發當中我們肯定不會通過微服務名去調用,比如我要調用消費者可能隻要一個/ cloud/goodsFeign 就好了,而不是 /springcloud-service-portal/cloud/goodsFeign

配置路由規則

zuul.routes.portal.service-id=springcloud-service-portal
zuul.routes.portal.path=/portal/**

           

解釋:

/** 代表是所有(多個) 層級 /cloud/goodsFeignHystrix

/* 是代表一層

如果是 /* 的話 /api/goods 就不會被路由

此時我們能通過自定義的規則進行通路,但是我們現在依然能用之前的微服務名調用,這是不合理的,第一是有多重位址了, 第二一般微服務名這種最好不要暴露在外,是以我們一般會禁用微服務名方式調用。

加入配置:

zuul.ignored-services=springcloud-service-portal
           

這裡能發現我們不能通過微服務名來調用了, 不過這個配置如果一個一個通過微服務名來配置難免有點複雜,是以一般這樣配置來禁用所有:

可能有時候我們的接口調用需要一定的規範,比如調用微服務的API URL字首需要加上 /api 對于這種情況,zuul 也考慮到了并給出了解決方案:

zuul.prefix=/api
           

比如:http://localhost/api/portal/cloud/goodsFeignHystrix

三、通配符

通配符 說明
比對任意單個字元
* 比對任意數量的字元
** 比對任意數量的字元

四、過濾器

主要功能 :限流、權限驗證、記錄日志

過濾器 (filter) 是 zuul 的核心元件,zuul 大部分功能都是通過過濾器來實作的。 zuul 中定義了 4 種标準過濾器類型,這些過濾器類型對應于請求的典型生命周期。

PRE :

這種過濾器在請求被路由之前調用。可利用這種過濾器實作身份驗證、在叢集中選擇請求的微服務、記錄調試資訊等。

ROUTING :

這種過濾器将請求路由到微服務。這種過濾器用于建構發送給微服務的請求,并使用 Apache HttpClient 或 Netfilx Ribbon請求微服務

POST :

這種過濾器在路由到微服務以後執行。這種過濾器可用來為響應添加标準的 HTTP

Header、收集統計資訊和名額、将響應從微服務發送給用戶端等。

ERROR :

在其他階段發生錯誤時執行該過濾器。
SpringCloud (七) --------- Zuul 網關前言一、Zuul 介紹二、路由功能三、通配符四、過濾器五、zuul 過濾器的禁用六、Zuul 的異常處理七、Zuul 的熔斷降級

如果要編寫一個過濾器,則需繼承 ZuulFilter 類實作其中的方法:

@Component
public class LogFilter extends ZuulFilter {
    
    @Override
    public String filterType() {
        return FilterConstants.ROUTE_TYPE;
    }
   
    @Override
    public int filterOrder() {
        return FilterConstants.PRE_DECORATION_FILTER_ORDER;
    }
    
    @Override 
    public boolbean shouldFilter() {
        return true;
    }
    
    @Override
    public Object run() throws ZuulException {
        RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest requst = currentContext.getRequest();
        String remoteAddr = request.getServerName();
        System.out.println("通路位址:" + request.getRequestURI());
        return null;
    }
}
           

由代碼可知,自定義的 zuul Filter 需實作以下幾個方法:

filterType

傳回過濾器的類型。有 pre、 route、 post、 error 等幾種取值,分别對應上文的幾種過濾器。

詳細可以參考 com.netflix.zuul.ZuulFilter.filterType() 中的注釋。

filter0rder

傳回一個 int值來指定過濾器的執行順序,不同的過濾器允許傳回相同的數字。

shouldFilter

傳回一個 boolean 值來判斷該過濾器是否要執行, true 表示執行, false 表示不執行。

run

過濾器的具體邏輯。

五、zuul 過濾器的禁用

Spring Cloud 預設為 Zuul 編寫并啟用了一些過濾器,例如 DebugFilter、FormBodyWrapperFilter 等,這些過濾器都存放在 spring-cloud-netflix-zuul 這個jar 包裡,一些場景下,想要禁用掉部分過濾器,該怎麼辦呢?

隻需在 application.properties 裡設定 zuul…disable=true 例如,要禁用上面我們寫的過濾器,這樣配置就行了:

六、Zuul 的異常處理

Spring Cloud Zuul 對異常的處理是非常友善的,但是由于 Spring Cloud 處于迅速發展中,各個版本之間有所差異,本案例是以 Finchley.RELEASE 版本為例, 來說明 Spring Cloud Zuul 中的異常處理問題。

看一張官方給出的 Zuul 請求的生命周期圖:

SpringCloud (七) --------- Zuul 網關前言一、Zuul 介紹二、路由功能三、通配符四、過濾器五、zuul 過濾器的禁用六、Zuul 的異常處理七、Zuul 的熔斷降級

1、正常情況下所有的請求都是按照 pre、route、post 的順序來執行,然後由 post 傳回 response 。

2、在 pre 階段,如果有自定義的過濾器則執行自定義的過濾器。

3、pre、routing、post 的任意一個階段如果抛異常了,則執行 error 過濾器

我們可以統一處理異常。

實作步驟:

禁用 zuul 預設的異常處理 SendErrorFilter 過濾器,然後自定義我們自己的Errorfilter 過濾器

@Component
public class ErrorFilter extends ZuulFilter {
    
    @Override
    public String filterType() {
        return "error"; 
    }
    
    @Override
    public int filterOrder() {
        return 1;
    } 

    @Override 
    public boolean shouldFilter() {
        return true;
    }
    
    @Override
    public Object run() throws ZuulException {
        try {
            RequestContext context = RequestContext.getCurrentContext();
            
            ZuulException exception = (ZuulException)context.getThrowable();
            
            System.out.println("進入系統異常攔截" + exception.getMessage());
            
            HttpServletResponse response = context.getResponse();
            
            response.setContentType("application/json; charset=utf8");
            
            response.setStatus(exception.nStatusCode);
            
            PrintWriter writer = null;
            
            try {
                writer = response.getWriter();
                writer.print("{code:" + exception.nStatusCode + ",message:\"" + exception.getMessage() +"\"}");
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if(writer!=null) {
                    writer.close();
                }
            }
        } catch (Exception e) {
            ReflectionUtils.rethrowRuntimeException(e);
        }
        
        return null;
    }
}
           

七、Zuul 的熔斷降級

zuul 是一個代理服務,但如果被代理的服務突然斷了,這個時候 zuul 上面會有出錯資訊,例如,停止了被調用的微服務。

一般服務方自己會進行服務的熔斷降級,但對于 zuul 本身,也應該進行 zuul 的降級處理。

我們需要有一個 zuul 的降級,實作如下:

@Component
public class ProviderFallback implements FallbackProvider {
    
    @Override
    public String getRoute() {
        return "*";
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        return new ClientHttpResponse() {
            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.set("Content-Type", "text/html; charset=UTF-8");
                return headers;
            }

            @Override
            public InputStream getBody() throws IOException {
                // 響應體
                return new ByteArrayInputStream("服務正在維護,請稍後再試.".getBytes());
            }

            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.BAD_REQUEST;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return HttpStatus.BAD_REQUEST.value();
            }

            @Override
            public String getStatusText() throws IOException {
                return HttpStatus.BAD_REQUEST.getReasonPhrase();
            }

            @Override
            public void close() {

            }
        };
    }
}