⑤SpringCloud 實戰:引入Zuul組件,開啟網關路由

這是SpringCloud實戰系列中第4篇文章,了解前面第兩篇文章更有助於更好理解本文內容:
①SpringCloud 實戰:引入Eureka組件,完善服務治理
②SpringCloud 實戰:引入Feign組件,發起服務間調用
③SpringCloud 實戰:使用 Ribbon 客戶端負載均衡
④SpringCloud 實戰:引入Hystrix組件,分佈式系統容錯

簡介

Zuul 也是 Netflix OSS 中的一員,是一個基於 JVM 路由和服務端的負載均衡器,支持動態路由、監控、彈性和安全等特性。Spring Cloud 會創建一個嵌入式 Zuul 代理來簡化一個常見用例的開發,比如用戶程序可能會對一個或多個後端服務進行調用,引入 Zuul 網關能有效避免為所有後端獨立管理CORS和身份驗證問題的需求

Zuul的使用了一系列的過濾器,這些過濾器可以完成以下功能:

  • 身份驗證和安全性
    識別每個資源的身份驗證要求,並拒絕不滿足這些要求的請求。
  • 審查與監控
    跟蹤有意義的數據和統計數據,以便給我們一個準確的生產視圖。
  • 動態路由
    根據需要將請求動態路由到不同的後端集群。
  • 壓力測試
    逐漸增加集群的流量,以評估性能。
  • 負載消減
    為每種類型的請求分配容量,並丟棄超出限制的請求。
  • 靜態響應處理
    直接在邊緣構建一些響應,而不是將它們轉發到內部集群Zuul 的使用

實戰 Zuul

搭建 zuul

  1. 新建一個新的項目jlw-zuul

  2. 引入zuul依賴

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
    </dependency>
    
  3. 啟動類上加入註解@EnableZuulProxy

  4. 引入Eureka註冊中心,並註冊上去

  5. 現在不需要額外配置就可以啟動了,啟動之後你會看到默認的服務映射相關日誌:

    Mapped URL path [/eureka-provider-temp/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
    Mapped URL path [/eureka-server/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
    Mapped URL path [/eureka-provider/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
    Mapped URL path [/ribbon-client/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
    Mapped URL path [/eureka-client/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
    
  6. 然後就可以通過zuul網關來訪問後端服務了

管理端點

Zuul 默認依賴了 actuator,並且會暴露/actuator/routes/actuator/filters 兩個端點,訪問這兩個斷點,可以很直觀的查看到路由信息,在查看之前需要添加以下配置:

# 應該包含的端點ID,全部:*
management.endpoints.web.exposure.include: *

訪問//127.0.0.1:8000/actuator可以查看所有端點信息,訪問//127.0.0.1:8000/actuator/routes 可查看到路由信息:

路由配置

為網關添加前綴

# 訪問網關的時候必須要加的路徑前綴
zuul.prefix = /api

添加以上配置後,訪問網關時路徑必須是/api/**,然後才會正確的路由到後端對應的服務

如果在轉發請求到服務的時候要去掉這個前綴,可以設置strip-prefix= false來忽略

# 請求轉發前是否要刪除 zuul.prefix 設置的前綴 ,true:轉發前要帶上前綴(默認值),fasle:不帶上前綴
zuul.routes.ecs.strip-prefix = true

配置路由

# 忽略註冊中心 eureka-server,*:會忽略所有的服務
zuul.ignored-services = eureka-server,eureka-client
# eureka-client 服務映射規則,//127.0.0.1:8000/ec/sayHello
zuul.routes.eureka-client = /ec/**

上面的配置會忽略eureka-server和eureka-client,訪問//127.0.0.1:8000/api/ec/**的請求的都會被路由到eureka-client,如果沒有忽略eureka-client,則訪問/eureka-client/**/ec/** 都會路由到eureka-client服務。

注意/ec/*只會匹配一個層級,/ec/** 會匹配多個層級。

指定服務id和path

# 指定service-id和path
zuul.routes.rcs.service-id = ribbon-client
zuul.routes.rcs.path = /rc/**

然後訪問//127.0.0.1:8000/api/rc/queryPort接口就會被路由到ribbon-client服務

路由配置順序

如果想按照配置的順序進行路由規則控制,則需要使用YAML,如果是使用propeties文件,則會丟失順序。例如:

zuul:
  routes:
    users:
      path: /myusers/**
    legacy:
      path: /**

使用propeties文件,舊的路徑可能出現在用戶路徑的前面,從而導致用戶路徑無法訪問。

關閉重試

可以通過將zuul.retryable設置為false來關閉Zuul的重試功能,默認值也是false。

zuul.retryable=false

還可以通過將zuul.routes.routename.retryable設置為false來禁用逐個路由的重試功能

# 關閉指定路由的重試
zuul.routes.ecs.retryable = false

忽略服務路由

添加以下配置會忽略指定的服務,很明顯註冊中心一般是不需要通過網關來訪問的,所以需要忽略它

# 忽略註冊中心 eureka-server,*:會忽略所有的服務
zuul.ignored-services = eureka-server

也可以通過zuul.ignoredPatterns 來配置你不想暴露出去的API

隔離策略

設置信號量

# 改為信號量隔離
zuul.ribbon-isolation-strategy=semaphore
# Hystrix的最大總信號量
zuul.semaphore.max-semaphores=1000
# 單個路由可以使用的最大連接數
zuul.host.max-per-route-connections=500

最大總信號量默認是100,單個路由最大的連接數默認是20,有時候並發量上不去可能就是使用的默認配置。

設置獨立的線程池

Zuul 中默認採用信號量隔離機制,如果想要換成線程,需要配置 zuul.ribbon-isolation-strategy=THREAD,配置後所有的路由對應的 Command 都在一個線程池中執行,這樣其實達不到隔離的效果,所以我們需要增加一個 zuul.thread-pool.use-separate-thread-pools 的配置,讓每個路由都使用獨立的線程池,zuul.thread-pool.thread-pool-key-prefix 可以為線程池配置對應的前綴,方便調試。

## 線程隔離
#zuul.ribbon-isolation-strategy=THREAD
## 每個路由使用獨立的線程池
#zuul.thread-pool.use-separate-thread-pools=true
## 線程池前綴
#zuul.thread-pool.thread-pool-key-prefix=zuul-pool-

其他配置

更換Http客戶端

Zuul默認使用的是 Apache HTTP Client,需要更換的話只需要設置對應的屬性即可

# Ribbon RestClient
ribbon.restclient.enabled=true
#  or okhttp
ribbon.okhttp.enabled=true

Cookie和請求頭

Zuul 提供了一個敏感頭屬性配置,設置了該屬性後,Zuul 就不會把相關的請求頭轉發到下游的服務,比如:

# 請求頭裏面的字段不會帶到eureka-client服務
zuul.routes.ecs.sensitive-headers = jinglingwang

sensitiveHeaders 的默認值是Cookie、Set-Cookie、Authorization,如果把該值配置成空值,則會把所有的頭都傳遞到下游服務。

還可以通過設置zuul.sensitiveHeaders來設置全局的敏感標頭。 如果在路由上設置了sensitiveHeaders,它將覆蓋全局的sensitiveHeaders設置

忽略請求頭

除了對路由敏感的標頭單獨設置之外,還可以設置一個名為zuul.ignoredHeaders的全局值,比如:

# 該配置的Header也不會轉發到下游服務
zuul.ignored-headers=jinglingwang

在默認情況下是沒有這個配置的,如果項目中引入了Spring Security,那麼Spring Security會自動加上這個配置,默認值為: Pragma,Cache-Control,X-Frame-Options,X-Content-Type-Options,X-XSS-Protection,Expries。

下游服務需要使用Spring Security的Header時,可以增加zuul.ignoreSecurityHeaders=false的配置

文件上傳

通過Zuul網關上傳文件時,只要文件不大,都可以正常的上傳,對於大文件,Zuul有一個替代路徑(/zuul/*)可以繞過Spring DispatcherServlet,比如你的文件服務(file-service)路由配置是zuul.routes.file-service=/file/**,然後你post提交文件到/zuul/file/** 即可。

還有一種辦法就是直接修改可上傳文件大小的配置:

# 文件最大值。值可以使用後綴「 MB」或「 KB」分別表示兆位元組或千位元組
spring.servlet.multipart.max-file-size=10MB
# 最大請求大小
spring.servlet.multipart.max-request-size=30MB

兩種辦法都需要在文件服務裏面添加以上的配置

在上傳大文件時也需要設置合理的超時時間:

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
  ConnectTimeout: 3000
  ReadTimeout: 60000

自定義過濾器

過濾器是 Zuul 中的核心內容,很多高級的擴展都需要自定義過濾器來實現,在 Zuul 中自定義一個過濾器只需要繼承 ZuulFilter,然後重寫 ZuulFilter 的四個方法即可:

@Component
public class LogFilter extends ZuulFilter{
    /**
     * 返回過濾器的類型,可選值有 pre、route、post、error 四種類型
     * @return
     */
    @Override
    public String filterType(){
        return "pre";
    }

    /**
     * 指定過濾器的執行順序,數字越小,優先級越高
     * 默認的filter的順序可以在FilterConstants類中查看。
     * @return
     */
    @Override
    public int filterOrder(){
        // pre filter
        return PRE_DECORATION_FILTER_ORDER - 1 ;
        // ROUTE filter
        //return SIMPLE_HOST_ROUTING_FILTER_ORDER - 1 ;
        // POST filter
        //return SEND_RESPONSE_FILTER_ORDER - 1 ;
    }

    /**
     * 決定了是否執行該過濾器,true 為執行,false 為不執行
     * @return
     */
    @Override
    public boolean shouldFilter(){
        return true;
    }

    /**
     * 如果shouldFilter()為true,則將調用此方法。該方法是ZuulFilter的核心方法
     * @return 返回值會被忽略
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException{
        HttpServletRequest req = (HttpServletRequest) RequestContext.getCurrentContext().getRequest();
        System.out.println("ZUUL REQUEST:: " + req.getScheme() + " " + req.getRemoteAddr() + ":" + req.getRemotePort() + " uri::"+ req.getRequestURI()) ;
        return null;
    }
}

禁用過濾器

Zuul 默認提供了很多過濾器(ZuulFilter),有關可啟用的過濾器列表,可以參考Zuul 過濾器的包(netflix.zuul.filters)。如果要禁用一個過濾器,可以按照zuul.<SimpleClassName>.<filterType>.disable=true 格式來進行設置,比如:

zuul.SendResponseFilter.post.disable=true

跨域支持

如果是外部網頁應用需要調用網關的 API,不在同一個域名下則會存在跨域的問題,想讓Zuul處理這些跨域的請求,可以通過提供自定義WebMvcConfigurer bean來完成:

@Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            /**
             * 配置跨源請求處理
             * @param registry
             */
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/path/**")
                        .allowedOrigins("//jinglingwang.cn")
                        .allowedMethods("GET", "POST");
            }
        };
    }

上面的示例中,允許jinglingwang.cnGETPOST方法將跨域請求發送到 /path/**開頭的端點

Zuul 超時

有兩種情況:

  1. 如果Zuul使用服務發現,則需要配置Ribbon的屬性配置超時

    ribbon.ReadTimeout
    ribbon.SocketTimeout
    
  2. 如果通過指定URL配置了Zuul路由

    # 套接字超時(以毫秒為單位)。默認為10000
    zuul.host.socket-timeout-millis=15000
    # 連接超時(以毫秒為單位)。默認為2000
    zuul.host.connect-timeout-millis=3000
    

服務容錯與回退

Spring Cloud 中,Zuul 默認整合了 Hystrix,當Zuul中給定路由的電路跳閘時,可以通過創建FallbackProvider類型的bean提供回退響應。配置示例代碼如下:

@Component
public class EurekaClientFallbackProvider implements FallbackProvider{
    @Override
    public String getRoute(){
        // 路由的server-id,* or null:為所有的路由都配置回退
        return "eureka-client";
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route,Throwable cause){
        if (cause instanceof HystrixTimeoutException) {
            return response(HttpStatus.GATEWAY_TIMEOUT);
        } else {
            return response(HttpStatus.INTERNAL_SERVER_ERROR);
        }

    }

    private ClientHttpResponse response(HttpStatus status){
        return new ClientHttpResponse(){
            @Override
            public HttpStatus getStatusCode() throws IOException{
                return status;
            }

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

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

            @Override
            public void close(){
            }

            @Override
            public InputStream getBody() throws IOException{
                return new ByteArrayInputStream("eureka-client 服務暫不可用,jinglingwang請你稍後重試!".getBytes());
            }

            @Override
            public HttpHeaders getHeaders(){
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
                return headers;
            }
        };
    }
}

重啟後,運行效果如下:

如果要為所有路由提供默認回退,getRoute方法返回*或null即可。

Ribbon client延遲加載

Zuul內部使用Ribbon來調用遠程URL。 默認情況下,Ribbon 客戶端在第一次調用時由Spring Cloud進行延遲加載。可以通過以下配置來開啟啟動時立即加載:

zuul.ribbon.eager-load.enabled=true

@EnableZuulProxy vs @EnableZuulServer

Spring Cloud Netflix安裝了很多過濾器,具體取決於用於啟用Zuul的註解。 @EnableZuulProxy是@EnableZuulServer的超集。換句話說,@ EnableZuulProxy包含@EnableZuulServer安裝的所有過濾器。 「proxy」中的其他過濾器啟用路由功能。 如果需要一個「空白」的 Zuul,則應使用@EnableZuulServer。