天天看點

服務網關zuul之五:熔斷

路由熔斷

當我們的後端服務出現異常的時候,我們不希望将異常抛出給最外層,期望服務可以自動進行一降級。Zuul給我們提供了這樣的支援。當某個服務出現異常時,直接傳回我們預設的資訊。

如果沒有配置fallback,zuul調用時逾時了,

服務網關zuul之五:熔斷
服務網關zuul之五:熔斷

我們通過自定義的fallback方法,并且将其指定給某個route來實作該route通路出問題的熔斷處理。主要繼承ZuulFallbackProvider接口來實作,ZuulFallbackProvider預設有兩個方法,一個用來指明熔斷攔截哪個服務,一個定制傳回内容。

public interface FallbackProvider extends ZuulFallbackProvider {

    /**
     * Provides a fallback response based on the cause of the failed execution.
     *
     * @param cause cause of the main method failure
     * @return the fallback response
     */
    ClientHttpResponse fallbackResponse(Throwable cause);
}      

實作類通過實作getRoute方法,告訴Zuul它是負責哪個route定義的熔斷。而fallbackResponse方法則是告訴 Zuul 斷路出現時,它會提供一個什麼傳回值來處理請求。

後來Spring又擴充了此類,豐富了傳回方式,在傳回的内容中添加了異常資訊,是以最新版本建議直接繼承類

FallbackProvider

我們以上面的spring-cloud-producer服務為例,定制它的熔斷傳回内容。

package com.dxz.zuul.fallback;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;

@Component
public class ProducerFallback implements FallbackProvider {
    private final Logger logger = LoggerFactory.getLogger(FallbackProvider.class);

    // 指定要處理的 service。
    @Override
    public String getRoute() {
        return "service-producter";
    }

    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("The service is unavailable.".getBytes());
            }

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

    @Override
    public ClientHttpResponse fallbackResponse(Throwable cause) {
        if (cause != null && cause.getCause() != null) {
            String reason = cause.getCause().getMessage();
            logger.info("Excption {}", reason);
        }
        return fallbackResponse();
    }
}      

路由轉發配置:

zuul.routes.api-test.path: /api-test/**
zuul.routes.consuer.path: /service-consumer/**
zuul.routes.consuer.serviceId: service-consumer

zuul.routes.producter.path: /service-producter/**
zuul.routes.producter.serviceId: service-producter      

為了便于示範,将zuul裡的逾時配置短些:

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 6000
hystrix.command.default.execution.timeout.enabled: true
feign.hystrix.enabled: true
spring.cloud.loadbalancer.retry.enabled: true
ribbon.ReadTimeout: 6000
ribbon.ConnectTimeout: 6000      

通路: http://127.0.0.1:8091/service-producter/book/getbook5/2?token=1

服務網關zuul之五:熔斷

當服務出現異常時,列印相關異常資訊,并傳回”The service is unavailable.”。

Zuul 目前隻支援服務級别的熔斷,不支援具體到某個URL進行熔斷。

路由重試

有時候因為網絡或者其它原因,服務可能會暫時的不可用,這個時候我們希望可以再次對服務進行重試,Zuul也幫我們實作了此功能,需要結合Spring Retry 一起來實作。下面我們以上面的項目為例做示範。

添加Spring Retry依賴

首先在spring-cloud-zuul項目中添加Spring Retry依賴。

compile \'org.springframework.retry:spring-retry:1.2.2.RELEASE\'      

開啟Zuul Retry

再配置檔案中配置啟用Zuul Retry

#是否開啟重試功能
zuul.retryable=true
#對目前服務的重試次數
ribbon.MaxAutoRetries=2
#切換相同Server的次數
ribbon.MaxAutoRetriesNextServer=0      

這樣我們就開啟了Zuul的重試功能。

測試

我們對service-producer進行改造,在getbook5方法中添加定時,并且在請求的一開始列印參數。

@RestController
@RequestMapping("/book")
public class BookProducter {

    @Autowired
    private RestTemplate restTemplate;
    
    @RequestMapping(value = "/getbook5/{id}", method = RequestMethod.GET)
    public Book getbook5(@ApiParam("id編号") @PathVariable("id") Integer id) throws InterruptedException {
        System.out.println(">>>>>>>>/getbook5/" + id);
        int i = new Random().nextInt(20);
        TimeUnit.SECONDS.sleep(i);
        System.out.println("SLEEP=" + i + ">>>>>>>>/getbook5/" + id);
        if (id == 1) {
            return new Book(id, "《李自成》", 55, "姚雪垠", "人民文學出版社");
        } else if (id == 2) {
            return new Book(id, "中國文學簡史", 33, "林庚", "清華大學出版社");
        }
        return new Book(id, "文學改良刍議", 33, "胡适", "無");
    }
}      

重新開機 service-producter和zuul-demo項目。

通路位址:http://127.0.0.1:8091/service-producter/book/getbook5/3?token=1,當頁面傳回:

The service is unavailable.

時檢視項目service-producter背景日志如下:

服務網關zuul之五:熔斷

說明進行了三次的請求,也就是進行了兩次的重試。這樣也就驗證了我們的配置資訊,完成了Zuul的重試功能。

注意

開啟重試在某些情況下是有問題的,比如當壓力過大,一個執行個體停止響應時,路由将流量轉到另一個執行個體,很有可能導緻最終所有的執行個體全被壓垮。說到底,斷路器的其中一個作用就是防止故障或者壓力擴散。用了retry,斷路器就隻有在該服務的所有執行個體都無法運作的情況下才能起作用。這種時候,斷路器的形式更像是提供一種友好的錯誤資訊,或者假裝服務正常運作的假象給使用者。

不用retry,僅使用負載均衡和熔斷,就必須考慮到是否能夠接受單個服務執行個體關閉和eureka重新整理服務清單之間帶來的短時間的熔斷。如果可以接受,就無需使用retry。

服務網關zuul之五:熔斷