Spring Cloud(09)——服務端負載工具Zuul

服務端負載工具Zuul

Zuul是Netflix公司提供的服務端負載工具,Spring Cloud基於它做了一些整合。試想一下微服務場景下服務端有服務A、服務B、服務C等,每個服務對應不同的地址,作爲服務提供者,你不想直接對外暴露服務A、服務B、服務C的地址,而且每種服務又有N臺機器提供服務。使用Zuul後,可以同時聚合服務A、服務B、服務C,又可實現服務的負載均衡,即同時聚合多個服務A的提供者。Zuul是作用於服務端的,同時它在提供負載均衡時是基於Ribbon實現的。其實也很好理解,Zuul對於真正的服務提供者來說它又是作爲客戶端的,所以它使用了客戶端負載工具Ribbon。Zuul會把每個請求封裝爲Hystrix Command,所以它也可能會觸發斷路器打開。

Spring Cloud應用使用Zuul的第一步是在pom.xml中引入spring-cloud-starter-netflix-zuul

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

然後在主程序Class上加上@EnableZuulProxy以啓用Spring Cloud內置的Zuul反向代理。

@EnableZuulProxy
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
    
}

然後可以在application.properties或application.yml中配置服務對應的路由關係。如下指定了服務hello對應的映射路徑是/api/**,即當接收到請求爲/api/abc/def時會轉發到服務hello對應的/abc/def,因爲Zuul在轉發時默認會把前綴去掉(默認會去掉*以前的內容)。

zuul.routes.hello=/api/**

由於Zuul是基於Ribbon進行負載均衡的,所以我們可以通過Ribbon配置服務地址的方式給Zuul的某個服務配置服務地址。如下配置了服務hello對應的服務地址是localhost:8900localhost:8901

server.port=8888
zuul.routes.hello=/api/**
hello.ribbon.listOfServers=localhost:8900,localhost:8901

當我們請求http://localhost:8888/api/hello時就會轉發爲請求http://localhost:8900/hellohttp://localhost:8901/hello。可以通過zuul.prefix=/api爲所有的請求指定一個通用的前綴,而這個前綴默認也是會去掉的,比如當進行了下面的配置,在訪問http://localhost:8888/api/hello/abc時會轉發到http://localhost:8900/abc,因爲通用的前綴默認會去掉,特定服務的路由前綴也會去掉。

zuul.prefix=/api
zuul.routes.hello=/hello/**
hello.ribbon.listOfServers=localhost:8900

如果希望通用的前綴不去掉,可以加上zuul.stripPrefix=false,此時通過zuul.prefix指定的前綴就不會去掉了。所以當訪問http://localhost:8888/api/hello/abc時會轉發到http://localhost:8900/api/abc。如果需要特定服務路由的前綴也不去掉,可以使用zuul.routes.<serviceId>.stripPrefix=false,對於hello服務來說就是zuul.routes.hello.stripPrefix=false。此時除了加上這個外,hello服務的路徑信息也需要通過zuul.routes.<serviceId>.path來配置,所以此時的配置會是如下這樣。

zuul.prefix=/api
zuul.stripPrefix=false
zuul.routes.hello.path=/hello/**
zuul.routes.hello.stripPrefix=false
hello.ribbon.listOfServers=localhost:8900

@EnableZuulProxy的自動配置由org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration負責,所有的配置信息由org.springframework.cloud.netflix.zuul.filters.ZuulProperties負責接收。我們可以通過ZuulProperties查看可以配置哪些信息,也可以查看Zuul的一些默認配置。比如可以通過下面的方式指定最大的連接數爲100,默認是200;指定Socket的超時時間爲3秒,默認是10秒。

zuul.host.maxTotalConnections=100
zuul.host.socketTimeoutMillis=3000

之前定義的zuul.prefixzuul.stripPrefixzuul.routes.*等都來自於ZuulProperties,更多可配置的信息請參考ZuulProperties的API文檔或源碼。

也可以不使用Ribbon,直接寫死服務轉發地址。如下配置當請求http://localhost:8888/api/hello/abc時會轉發爲請求http://localhost:8900/api/hello/abc

server.port=8888
zuul.prefix=/api
zuul.stripPrefix=false
zuul.routes.hello.path=/hello/**
zuul.routes.hello.url=http://localhost:8900
zuul.routes.hello.stripPrefix=false

自定義HttpClient

Zuul默認會使用Apache的Http Client作爲向後端服務發起請求的客戶端,如果用戶想對HttpClient進行一些自定義,則可以定義自己的HttpClient類型的bean。比如下面的代碼自定義了HttpClient,其每次請求時都往Header裏面寫入名爲abc,值爲123的Header。

@Configuration
public class HttpClientConfig {

    @Bean
    public CloseableHttpClient httpClient() {
        List<Header> defaultHeaders = new ArrayList<>();
        defaultHeaders.add(new BasicHeader("abc", "123"));
        CloseableHttpClient httpClient = HttpClientBuilder.create().setDefaultHeaders(defaultHeaders).build();
        return httpClient;
    }
    
}

用戶也可選擇自定義自己的HttpClientBuilder類型的bean,因爲Spring Cloud在創建HttpClient時會獲取HttpClientBuilder類型的bean創建HttpClient,當我們沒有自定義HttpClientBuilder類型的bean時Spring Cloud會自動創建一個。

敏感性Header和Cookie

Zuul服務接收到的請求Header可以被轉發到負載的底層服務,但是有時候可能你不希望這些Header被轉發到底層服務,此時可以通過sensitiveHeaders指定敏感Header。比如我們的Zuul服務是直接面向瀏覽器客戶的,我們不希望瀏覽器的信息被轉發到底層服務,則可以在application.properties中添加如下配置信息,這樣就不會往底層服務傳遞user-agent和cache-control這兩個Header。

zuul.sensitiveHeaders=user-agent,cache-control

通過zuul.sensitiveHeaders指定的配置將對所有的服務生效,如果我們只想對某個服務隱藏一些Header,則可以通過該服務的路由配置sensitiveHeaders,比如不希望user-agent和cache-control這兩個Header轉發到服務hello,則可以進行如下配置。

zuul.routes.hello.sensitiveHeaders=user-agent,cache-control

當同時配置了特定服務配置的sensitiveHeaders和通用的sensitiveHeaders時,特定服務的sensitiveHeaders將擁有更高的優先級,即特定服務的sensitiveHeaders會覆蓋通用的sensitiveHeaders。比如通用的sensitiveHeaders配置了敏感Header爲ABC,服務hello配置了sensitiveHeaders爲BCD,那麼請求轉發到服務hello時將不會轉發Header BCD,但是會繼續轉發Header ABC。

默認的敏感Header是Cookie、Set-Cookie和Authorization,當自己指定了sensitiveHeaders時,默認的sensitiveHeaders自動失效。

忽略Header

除了敏感性Header可以不轉發到底層服務外,還可以通過ignoredHeaders指定需要忽略的Header。ignoredHeaders指定的Header將不轉發到底層服務,同時將從底層服務的響應中自動移除。如下配置指定了將忽略user-agent和cache-control這兩個Header。

zuul.ignoredHeaders=user-agent,cache-control

ignoredHeaders只能指定通用的,沒有特定服務級別的。

Endpoint

當使用@EnableZuulProxy會自動引入routes和filters這兩個Endpoint,可以單獨發佈這兩個Endpoint,也可以通過如下方式發佈所有的Endpoint。

management.endpoints.web.exposure.include=*

之後可以通過actuator/routes查看所有的路由信息,即服務對應的映射路徑信息,類似如下這樣。

{
/api/hello/**: "hello"
}

還可以在後面加上/details得到路由的詳細信息,即請求/actuator/routes/details,得到的路由詳細信息是類似如下這樣的。

{
    "/api/hello/**": {
        "id": "hello",
        "fullPath": "/api/hello/**",
        "location": "hello",
        "path": "/hello/**",
        "prefix": "/api",
        "retryable": false,
        "sensitiveHeaders": [
            "upgrade-insecure-requests",
            "accept"
        ],
        "customSensitiveHeaders": true,
        "prefixStripped": false
    }
}

可以通過actuator/filters查看所有的com.netflix.zuul.ZuulFilter及對應的Filter類型等信息,類似如下這樣。

{
    "error": [
        {
            "class": "org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter",
            "order": 0,
            "disabled": false,
            "static": true
        }
    ],
    "post": [
        {
            "class": "org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter",
            "order": 1000,
            "disabled": false,
            "static": true
        }
    ],
    "pre": [
        {
            "class": "org.springframework.cloud.netflix.zuul.filters.pre.DebugFilter",
            "order": 1,
            "disabled": false,
            "static": true
        },
        {
            "class": "org.springframework.cloud.netflix.zuul.filters.pre.FormBodyWrapperFilter",
            "order": -1,
            "disabled": false,
            "static": true
        },
        {
            "class": "org.springframework.cloud.netflix.zuul.filters.pre.Servlet30WrapperFilter",
            "order": -2,
            "disabled": false,
            "static": true
        },
        {
            "class": "org.springframework.cloud.netflix.zuul.filters.pre.ServletDetectionFilter",
            "order": -3,
            "disabled": false,
            "static": true
        },
        {
            "class": "org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter",
            "order": 5,
            "disabled": false,
            "static": true
        }
    ],
    "route": [
        {
            "class": "org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter",
            "order": 100,
            "disabled": false,
            "static": true
        },
        {
            "class": "org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter",
            "order": 10,
            "disabled": false,
            "static": true
        },
        {
            "class": "org.springframework.cloud.netflix.zuul.filters.route.SendForwardFilter",
            "order": 500,
            "disabled": false,
            "static": true
        }
    ]
}

本地轉發

Zuul除了把請求轉發到外部服務外,還可以把請求轉發到本地的@RequestMapping請求。比如Zuul所在應用有如下Controller,其可以接收/local/abc請求。

@RestController
@RequestMapping("local")
public class LocalFowardController {

    @GetMapping("abc")
    public String abc() {
        return "ABC" + LocalDateTime.now();
    }
    
}

然後我們配置名爲local1的路由信息,其將把/local1/**請求轉發到本地的/local。即當接收到請求/local1/abc時會轉發爲請求本地的/local/abc,即請求LocalFowardController.abc()

zuul.routes.local1.path=/local1/**
zuul.routes.local1.url=forward:/local

禁用ZuulFilter

Spring Cloud對Zuul的支持是由一系列的ZuulFilter來實現的,它們都定義在org.springframework.cloud.netflix.zuul.filters.xxx下,其中xxx指對應的ZuulFilter。每個ZuulFilter是相互獨立的,它們之間會通過com.netflix.zuul.context.RequestContext交互數據,RequestContext還持有當前請求的HttpServletRequest和HttpServletResponse的引用。使用@EnableZuulProxy時會自動創建這些ZuulFilter的bean。如果想禁用其中的某個ZuulFilter,則可以通過設置zuul.<SimpleClassName>.<filterType>.disable=true來禁用它。比如想要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter,則可以設置zuul.SendResponseFilter.post.disable=true

這裏只是拿SendResponseFilter來舉個例,實際使用時禁用了SendResponseFilter將不會把代理的Response寫入到當前請求的Response中,所以千萬不要禁用它。

自定義ZuulFilter

如果有需要也可以定義自己的ZuulFilter,並把它加入到ZuulFilter鏈中。假設我們想從Zuul開始追蹤整個請求,我們可以定義一個ZuulFilter,往Header中寫入一個唯一的請求標識,然後在多個ZuulFilter以及後端服務之間進行共享。爲此我們定義瞭如下這樣一個ZuulFilter。只需要把它定義爲bean即可自動把它加入ZuulFilter鏈中。

@Component
public class AddRequestIdZuulFilter extends ZuulFilter {

    private static final String REQUEST_ID_HEADER = "X-REQUEST-ID";
    
    @Override
    public boolean shouldFilter() {
        return !RequestContext.getCurrentContext().getZuulRequestHeaders().containsKey(REQUEST_ID_HEADER);
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext context = RequestContext.getCurrentContext();
        context.addZuulRequestHeader(REQUEST_ID_HEADER, UUID.randomUUID().toString());
        return null;
    }

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

    @Override
    public int filterOrder() {
        return 0;
    }

}

Zuul histrix

Zuul會自動把請求封裝爲一個Hystrix Command,且@EnableZuulProxy上使用了@EnableCircuitBreaker。可以對路由的服務使用Histrix fallback,當熔斷器打開時將調用對應的fallback。需要爲特定的路由(或serviceId)指定fallback,可以定義一個FallbackProvider類型的bean,然後通過其getRoute()返回該fallback對應的路由(或serviceId),其fallbackResponse()將在需要發生fallback時調用。

@Component
public class HelloFallbackProvider implements FallbackProvider {

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

    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        return new ClientHttpResponse() {

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

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

            @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() {
                
            }
            
        };
    }

}

比如上述代碼我們定義了FallbackProvider是對應於路由hello的。當該路由擁有下述配置時,如果Zuul請求http://localhost:8900/xxx網絡不通,則會轉而返回上述的fallback的結果。

zuul.routes.hello=/hello/**
hello.ribbon.listOfServers=localhost:8900

FallbackProvider也可以是作用於所有的路由的,此時只需指定FallbackProvider的getRoute()的返回值爲*。其作用類似於默認FallbackProvider,當同時指定了默認的FallbackProvider和作用於特定的路由的FallbackProvider時,特定路由的FallbackProvider擁有更高的優先級。

@Component
public class DefaultFallbackProvider implements FallbackProvider {

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

    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        return new ClientHttpResponse() {

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

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

            @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() {
                
            }
            
        };
    }

}

指定超時時間

Zuul調用後端服務使用Ribbon時可以通過Ribbon的配置屬性來指定建立連接的超時時間和調用遠程服務的超時時間。如下配置指定了Zuul使用Ribbon進行負載,且與後端服務建立連接的超時時間是3秒,調用後端服務的接口的超時時間是2秒。

zuul.routes.hello=/hello/**
hello.ribbon.listOfServers=localhost:8900
ribbon.ReadTimeout=2000
ribbon.ConnectTimeout=3000

如果Zuul不使用Ribbon進行負載,而是直接指定路由對應的後端服務地址,則超時時間需要通過如下方式指定。

zuul.routes.hello.path=/hello/**
zuul.routes.hello.url=http://localhost:8900
zuul.host.socket-timeout-millis=2000
zuul.host.connect-timeout-millis=3000

和Eureka一起使用

Zuul底層使用Ribbon進行負載,所以Zuul和Eureka一起使用相當於Ribbon和Eureka一起使用。先在pom.xml中加上Eureka client的依賴。

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

然後在application.properties中加上Eureka的配置,Ribbon會自動使用Eureka進行服務發現。

zuul.routes.hello=/hello/**
eureka.client.registerWithEureka=false
eureka.client.serviceUrl.defaultZone=http://localhost:8089/eureka/

Zuul默認會把serviceId映射爲/serviceId/**,即如果有一個服務hello,其對應的映射路徑默認是/hello/**。所以當我們的服務可以滿足這種需求時可以不通過zuul.routes.serviceId指定服務的映射路徑。這樣的話如果你通過Eureka註冊了10個服務,那他們都會通過Zuul進行自動映射,如果你的Zuul是直接對外的,那麼可能你不希望其中的某些服務通過Zuul對外暴露。此時可以zuul.ignored-services屬性指定需要忽略的服務id。比如下面的配置指定了將忽略服務hello1和hello2。

zuul.ignored-services=hello1,hello2

也可以像如下這樣忽略所有的服務,然後再通過routes指定需要對外暴露的映射信息,如下就指定了需要對外暴露hello服務,且對應的映射路徑是/hello/**

zuul.ignored-services=*
zuul.routes.hello=/hello/**

自動重試

有時候可能由於網絡波動等原因,Zuul在轉發請求到後端服務會失敗。Zuul可以設置在轉發請求到後端服務失敗時自動發起重試。使用這種自動重試機制需要先在pom.xml中引入spring retry依賴。

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

然後在application.properties文件中配置zuul.retryable=true以啓用自動重試。這樣轉發請求失敗時默認將對GET請求發起重試,且默認在同一目標機器發起的重試次數是0,最多跨域一臺目標機器。即當調用的服務S同時有機器A、B、C提供服務的時候,如果第一次調用的是機器A,失敗後不會再調用A,會轉而調用B或C一次。可以通過ribbon.OkToRetryOnAllOperations=true指定對所有請求類型都可以進行重試,不管是GET還是POST,還是其它。可以通過ribbon.MaxAutoRetries指定在同一機器上的最大重試次數。可以通過ribbon.MaxAutoRetriesNextServer指定最多重試的機器數。比如當擁有下面配置時,如果我們請求的hello服務同時有機器A、B、C提供服務,第一次調用A如果失敗了,會在A繼續重試兩次,如果重試了兩次都沒成功,就會轉而重試B,B一共最多重試3次,第一次不算重試,最終如果還是失敗的,那C也是一樣的重試。還可以通過ribbon.retryableStatusCodes來指定需要進行重試的Http狀態碼,比如只希望在狀態碼爲500或502時進行重試,則配置ribbon.retryableStatusCodes=500,502。默認情況只要服務器通訊正常都不會重試,即狀態碼不管是404還是502等都不會發起重試,只有建立連接失敗或者請求超時會重試。所以如果我們需要在狀態碼爲502的時候也能發起重試則需要指定retryableStatusCodes。

zuul.retryable=true
ribbon.OkToRetryOnAllOperations=true
hello.ribbon.MaxAutoRetries=2
ribbon.MaxAutoRetriesNextServer=2

使用ribbon.xxx配置的是對所有服務都通用的配置,使用<serviceId>.ribbon.xxx配置的是對特定服務的配置,如上面的hello.ribbon.MaxAutoRetries

也可以通過zuul.routes.routename.retryable來單獨控制某個服務是否允許重試。比如單獨指定可以對hello服務進行重試則可以配置zuul.routes.hello.retryable=true。如果全局的zuul.retryable=true,則也可以通過zuul.routes.hello.retryable=false指定hello服務不重試。

參考文檔

(注:本文是基於Spring cloud Finchley.SR1所寫)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章