1. 程式人生 > >springcloud系列21——Zuul簡介及程式碼示例

springcloud系列21——Zuul簡介及程式碼示例

Zuul簡介

路由是微服務架構的組成部分。 例如,/可以對映到您的Web應用程式,/ api/users對映到使用者服務,/api/shop對映到商店服務。 Zuul是Netflix基於JVM的路由器和伺服器端負載均衡器。

Netflix使用Zuul進行以下操作: 認證 洞察 壓力測試 金絲雀測試 動態路由 服務遷移 負載脫落 安全 靜態響應處理 主動/主動流量管理

Zuul的規則引擎允許任何JVM語言編寫規則和過濾器,內建支援Java和Groovy。

配置屬性zuul.max.host.connections已被兩個新屬性zuul.host.maxTotalConnections和zuul.host.maxPerRouteConnections取代,它們分別預設為200和20。

所有路由的預設Hystrix隔離模式(ExecutionIsolationStrategy)是SEMAPHORE。 如果首選此隔離模式,則可以將zuul.ribbonIsolationStrategy更改為THREAD。

程式碼示例

這裡新建一個模組microservice-gateway-zuul。

1.引入zuul和eureka client的依賴:

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

spring cloud文件有說,Zuul starter沒有包含discovery client,所以我們在上面增加了eureka client的依賴。

2.在Spring boot的主類上增加註解@EnableZuulProxy

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

3.application.yml配置

spring:
  application:
    name: microservice-gateway-zuul

server:
  port: 8808

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    prefer-ip-address: true

測試

1.啟動Eureka server; 2.啟動user微服務; 3.啟動zuul模組。

在user模組,有/sample/1獲取使用者資訊的介面。

可以看到,請求成功。 我們看Zuul模組的控制檯日誌,可以看到下面的日誌:

Mapped URL path [/microservice-springcloud-user/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]

自定義請求路徑

通過Eureka server中的serviceID可以請求成功,但名字太長,如何自定義?

比如我們想通過/user/sample/1訪問,如何做到?

在application.yml增加下面的配置:

zuul:
  routes:
    microservice-springcloud-user: /user/**

Zuul忽略某些服務

比如有user和movie2個服務,但我只想Zuul代理user服務。

zuul:
  ignoredServices: '*'
  routes:
    microservice-springcloud-user: /user/**

ignoredServices:*忽略所有的服務,然後在routes中指定了user,所以最終就是Zuul代理user服務。

或者:

zuul:
  # 多個服務id之間用逗號分隔
  ignoredServices: microservice-springcloud-movie
  routes:
    microservice-springcloud-user: /user/**

Zuul指定path和serviceId

zuul:
  routes:
    # 下面的user1只是一個標識,保證唯一即可
    user1:
      # 對映的路徑
      path: /user/**
      # 服務id
      serviceId: microservice-springcloud-user

上面的配置意思是:讓Zuul代理microservice-springcloud-user,路徑為/user/**,user1可以隨便寫,只要保證唯一即可。

Zuul指定path+url

除了上面說的指定path+serviceId外,還可以使用path+url的配置。

zuul:
  routes:
    user1:
      path: /user/**
      # url為user服務的url
      url: http://10.41.3.149:7902

Zuul指定可用服務的負載均衡

在spring cloud的文件中有寫,如果使用上面的path+url配置,不會作為HystrixCommand執行,也不會使用Ribbon對多個URL進行負載均衡。 要實現此目的,您可以使用靜態伺服器列表指定serviceId。

zuul:
  routes:
    user1:
      path: /user/**
      serviceId: microservice-springcloud-user

# 在Ribbon中禁用eureka
ribbon:
  eureka:
   enabled: false

microservice-springcloud-user:
  ribbon:
    listOfServers: localhost:7901,localhost:7902

如上,需要在ribbon中禁用Eureka。然後指定了2個user服務,埠分別為7901,7902。

Zuul使用正則表示式指定路由規則

您可以使用regexmapper在serviceId和路由之間提供約定。 它使用名為groups的正則表示式從serviceId中提取變數並將它們注入路由模式。

將user服務id修改為

spring:
  application:
    name: microservice-springcloud-user-v1

zuul模組application.yml

spring:
  application:
    name: microservice-gateway-zuul

server:
  port: 8808

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    prefer-ip-address: true

Zuul主類:

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

    @Bean
    public PatternServiceRouteMapper serviceRouteMapper() {
        // 第一個引數:servicePattern,第2個引數routePattern
        return new PatternServiceRouteMapper(
                "(?<name>^.+)-(?<version>v.+$)",
                "${version}/${name}");
    }
}

上面的PatternServiceRouteMapper意思是myusers-v1將對映為/v1/myusers/**。

接受任何正則表示式,但所有命名組必須同時出現在servicePattern和routePattern中。

如果servicePattern與serviceId不匹配,則使用預設行為。比如user的serviceId為microservice-springcloud-user,那麼實際上最終是通過http://localhost:zuul埠/microservice-springcloud-user/**來訪問。

在上面的示例中,serviceId“myusers”將對映到路由“/myusers/**”(在未檢測到版本時)預設情況下禁用此功能,僅適用於已發現的服務。

然後瀏覽器可以通過[http://localhost:zuul埠]/v1/microservice-springcloud-user/sample/1來訪問user服務。

為所有對映增加字首

要為所有對映新增字首,請將zuul.prefix設定為值,例如/ api。 在預設情況下轉發請求之前,會從請求中刪除代理字首(使用zuul.stripPrefix = false關閉此行為)。

在application.yml增加

zuul:
  prefix: /api

然後通過http://localhost:Zuul埠/api/microservice-springcloud-user/sample/1來訪問。

上面是一種全域性的設定方式。可以通過zuul.stripPrefix=true/false來設定在請求具體的服務時是否剝離字首。比如訪問/api/sample/1,如果zuul.stripPrefix設定為true(預設為true),則實際請求使用者服務的是/sample/1,相反請求路徑是/api/sample/1.

您還可以關閉從各個路由中剝離特定於服務的字首,例如: 假定user服務中指定了context-path為/user,我們訪問/sample/1是通過/user/sample/1來訪問的。現在我想通過http://localhost:zuul埠/user/sample/1來訪問,可以這樣做:

zuul:
  routes:
    microservice-springcloud-user:
      path: /user/**
      stripPrefix: false

或者:

zuul:
  routes:
    microservice-springcloud-user:
      prefix: /user
      path: /**
      stripPrefix: false

stripPrefix是剝離字首的意思,設定為false就是不剝離字首,Zuul預設是剝離字首的。比如我們設定path=/user/**,比如訪問/user/sample/1,實際請求使用者服務的是/sample/1。

stripPrefix比較實用的場景是服務帶有context-path。

zuul.stripPrefix僅適用於zuul.prefix中設定的字首。 它對給定路由的路徑中定義的字首沒有任何影響。

Zuul忽略指定的路徑

上面說過了,通過ignoredServices可以指定忽略某些服務,這是比較粗粒度的控制。如果想細粒度的控制忽略某些路徑,可以通過下面的方式:

zuul:
  ignoredPatterns: /**/admin/**
  routes:
    users: /myusers/**

這意味著所有諸如“/myusers/101”之類的請求將被轉發到“users”服務上的“/101”。 但包括“/admin/”在內的請求則不會處理。

Zuul指定路由的順序

zuul:
  routes:
    microservice-springcloud-user:
      path: /user/**
      stripPrefix: false
    legacy:
      path: /**

上面配置的意思是/user**的請求轉發到microservice-springcloud-user去處理,其他的請求按預設的方式處理(即通過http://zuulHost:zuulPort/服務名/請求路徑)。 比如我們啟動了microservice-springcloud-user和microservice-springcloud-movie-feign-without-hystrix2個服務。

通過Zuul訪問microservice-springcloud-user,可以這樣訪問:

通過Zuul訪問microservice-springcloud-movie-feign-without-hystrix需要如下方式訪問:

如果你需要保證路由的順序,則需要使用YAML檔案,因為使用屬性檔案就會丟失順序。所以,如果你用properties檔案配置,可能會導致/user/**訪問不到microservice-springcloud-user。

Zuul Http Client

Zuul預設使用的是Apache的http client。之前使用的是RestClient。如果你還是想使用RestClient,可以設定ribbon.restclient.enabled=true;如果你想使用OkHttp3,可以設定ribbon.okhttp.enabled=true。

如果要自定義Apache HTTP客戶端或OK HTTP客戶端,請提供ClosableHttpClient或OkHttpClient型別的bean。

Cookie和敏感的Headers

在同一系統中的服務之間共享Headers是可以的,但您可能不希望敏感Headers向下遊洩漏到外部伺服器。您可以在路由配置中指定忽略的Headers列表。 Cookie起著特殊的作用,因為它們在瀏覽器中具有明確定義的語義,並且它們始終被視為敏感。如果您的代理的消費者是瀏覽器,那麼下游服務的cookie也會給使用者帶來問題,因為它們都會混亂(所有下游服務看起來都來自同一個地方)。

如果您對服務的設計非常小心,例如,如果只有一個下游服務設定了cookie,那麼您可以讓它們從後端一直流到呼叫者。此外,如果您的代理設定了cookie並且您的所有後端服務都是同一系統的一部分,那麼簡單地共享它們就很自然(例如使用Spring Session將它們連結到某個共享狀態)。除此之外,由下游服務設定的任何cookie都可能對呼叫者不是很有用,因此建議您(至少)將“Set-Cookie”和“Cookie”放入敏感的標頭中不屬於您的域名。即使對於屬於您域的路由,在允許cookie在它們與代理之間流動之前,請仔細考慮它的含義。

可以將敏感報頭配置為每個路由的逗號分隔列表,例如,

zuul:
  routes:
    users:
      path: /myusers/**
      sensitiveHeaders: Cookie,Set-Cookie,Authorization
      url: https://downstream

這是sensitiveHeaders的預設值,因此除非您希望它不同,否則無需進行設定。注: 這是Spring Cloud Netflix 1.1中的新功能(在1.0中,使用者無法控制Headers,所有Cookie都在所有方向上流動)。

sensitiveHeaders是黑名單,預設不為空,因此要使Zuul傳送所有Headers(“忽略”的Headers除外),您必須將其明確設定為空列表。 如果要將cookie或授權Headers傳遞給後端,則必須執行此操作。 例:

zuul:
  routes:
    users:
      path: /myusers/**
      sensitiveHeaders:
      url: https://downstream

也可以通過設定zuul.sensitiveHeaders來全域性設定敏感的Headers。 如果在路由上設定了sensitiveHeaders,則會覆蓋全域性sensitiveHeaders設定。

忽略Headers

除了每個路由規則上面的敏感頭部資訊設定,我們還可以在閘道器與外部服務互動的時候,用一個全域性的設定zuul.ignoredHeaders,去除那些我們不想要的http頭部資訊(包括請求和響應的)。在預設情況下,zuul是不會去除這些資訊的。如果Spring Security不在類路徑上的話,它們就會被初始化為一組眾所周知的“安全”頭部資訊(例如,涉及快取),這是由Spring Security指定的。在這種情況下,假設請求閘道器的服務也會新增頭部資訊,我們又要得到這些代理頭部資訊,就可以設定zuul.ignoreSecurityHeaders為false,同時保留Spring Security的安全頭部資訊和代理的頭部資訊。當然,我們也可以不設定這個值,僅僅獲取來自代理的頭部資訊。

路由端點

Actuator提供了一個可以檢視路由規則的端點/routes,我們在Zuul中引入Actuator依賴:

<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-actuator</artifactId>  
</dependency> 

再把安全驗證關閉,讓我們可以訪問到這個端點:

management:
  security:
    enabled: false

這裡,遺留請求的路由規則會影響到我們訪問這個端點,先註釋掉這個路由規則:

#legacy:
  #path: /**

重啟Zuul專案,我們便能看到zuul閘道器的路由規則了

如果想知道路由的詳細細節,可以增加引數?format=details

Strangulation Patterns (絞殺者模式)

遷移現有應用程式或API時的常見模式是“扼殺”舊的端點,慢慢地用不同的實現替換它們。 Zuul代理是一個有用的工具,因為可以使用它來處理來自舊端點的客戶端的所有流量,但重定向一些請求到新的端點。

zuul:
  routes:
    first:
      path: /first/**
      url: http://first.example.com
    second:
      path: /second/**
      url: forward:/second
    third:
      path: /third/**
      url: forward:/3rd # 本地的轉發
    legacy: # 老系統的請求
      path: /**
      url: http://legacy.example.com

在這個例子中,我們正在扼殺“遺留”應用程式,該應用程式對映到與其他模式之一不匹配的所有請求。 /first/**中的路徑已被提取到具有外部URL的新服務中。 並轉發/second/**中的路徑,以便可以在本地處理它們,例如, 使用正常的Spring @RequestMapping。 /third/**中的路徑也被轉發,但具有不同的字首(即/third/foo被轉發到/3rd/foo)。

忽略的模式不會被完全忽略,它們只是不由代理處理(因此它們也可以在本地有效轉發)。

通過Zuul上傳檔案

新建一個模組microservice-file-upload,該模組用於檔案上傳。 application.yml

server:
  port: 9999

spring:
  application:
    name: microservice-file-upload

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    prefer-ip-address: true

上傳檔案的Controller

@Controller
@RequestMapping("/file")
public class FileUploadController {

    @RequestMapping(value = "/upload",method = RequestMethod.POST)
    @ResponseBody
    public String uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
        String uploadDir = "E:/test/";
        String originName = file.getOriginalFilename();
        String uploadPath = uploadDir+originName;

        File destDir = new File(uploadDir);
        if (!destDir.exists()) {
            destDir.mkdirs();
        }

        File dest = new File(uploadPath);
        if (dest.exists()) {
            dest.delete();
        }
        file.transferTo(dest);
        System.out.println("檔案上傳路徑:" + uploadPath);

        return uploadPath;
    }
}

這裡將服務註冊到Eureka,是為了後面使用Zuul代理檔案上傳功能。

這裡用curl測試。

curl -F "[email protected]:/luckystar88/books/java_bloomfilter.rar" http://localhost:9999/file/upload

可以看到,請求成功。

剛剛上傳的檔案大小14Kb,我們上傳一個大點的檔案(檔案大小18.9M)。

出錯,看錯誤資訊,提示檔案大小19M,超過了配置的最大大小10M。

解決辦法:

spring:
  application:
    name: microservice-file-upload
  http:
    multipart:
      # 單個檔案大小
      max-file-size: 1024Mb
      # 總上傳資料的大小
      max-request-size: 2048Mb

配置上面2項設定檔案大小即可。

重新上傳:

現在我們使用Zuul測試。

修改Zuul的application.yml

zuul:
  routes:
    microservice-file-upload:
      path: /upload-api/**

將/upload-api/**的請求交給microservice-file-upload處理。

啟動Eureka Server,Zuul和file-upload模組。

curl -F "[email protected]:/360極速瀏覽器下載/111.mp4" http://localhost:8808/zuul/upload-api/file/upload

可以看到上傳成功。

我們準備一個大點的檔案(175M)測試下上傳超時。

curl -F "[email protected]:\Users\Administrator\Downloads\Spring+Cloud微服務實戰.pdf" http://localhost:8808/zuul/upload-api/file/upload

可以看到Zuul報超時了。

解決辦法: 在Zuul增加配置:

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

重新請求,可以看到上傳成功了(檔名亂碼就暫時不管了)。

禁用Zuul Filters

預設會使用很多filters,可採用如下方式禁止

zuul.SendResponseFilter.post.disable=true

Zuul的回退

當Zuul中給定路徑的電路跳閘時,您可以通過建立ZuulFallbackProvider型別的bean來提供回退響應。 在此bean中,您需要指定回退所針對的路由ID,並提供ClientHttpResponse作為回退返回。

我們建立一個模組microservice-gateway-zuul-fallback。 application.yml

spring:
  application:
    name: microservice-gateway-zuul-fallback

server:
  port: 8808

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    prefer-ip-address: true

zuul:
  routes:
    microservice-springcloud-user:
      path: /user/**
      stripPrefix: false

由於在microservice-springcloud-user服務中指定了context-path,所以這裡設定stripPrefix=false。

spring boot主類:

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

回退類:

@Component
public class UserFallbackProvider implements ZuulFallbackProvider {
    @Override
    public String getRoute() {
        return "microservice-springcloud-user";
    }

    @Override
    public ClientHttpResponse fallbackResponse() {
        return new ClientHttpResponse() {
            @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() {

            }

            @Override
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream((getRoute() + "==》fallback").getBytes());
            }

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

回退的類中指定了路由為microservice-springcloud-user,同時指定了響應碼,響應內容,響應型別等資訊。

測試:

啟動Eureka server,microservice-springcloud-user和microservice-gateway-zuul-fallback。

user服務正常時訪問:

關閉user服務,再次訪問:

通過/routes訪問路由資訊:

注意:FallbackProvider類中的routes必須與配置檔案中的一致。

如果想為所有的路由設定一個預設的fallback,可以建立一個ZuulFallbackProvider型別的Bean,並且getRoute返回*或null。

比如:

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

    @Override
    public ClientHttpResponse fallbackResponse() {
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.OK;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return 200;
            }

            @Override
            public String getStatusText() throws IOException {
                return "OK";
            }

            @Override
            public void close() {

            }

            @Override
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream("fallback".getBytes());
            }

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

如果您想根據失敗原因選擇響應,請使用FallbackProvider,它將取代未來版本中的ZuulFallbackProvder。

@Component
public class MyFallbackProvider implements FallbackProvider {

    @Override
    public String getRoute() {
        return "*";
    }

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

    @Override
    public ClientHttpResponse fallbackResponse() {
        return response(HttpStatus.INTERNAL_SERVER_ERROR);
    }

    private ClientHttpResponse response(final 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("fallback".getBytes());
            }

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