Sentinel與OpenFeign 服務熔斷那些事

點贊再看,養(yǎng)成習慣,微信搜索【牧小農】關注我獲取更多資訊,風里雨里,小農等你,很高興能夠成為你的朋友。
項目源碼地址:公眾號回復 sentinel,即可免費獲取源碼

在上一篇中,我們講解了 Sentinel 限流詳解,其中詳細講解了各個規(guī)則下的限流是如何操作,有興趣的小伙伴可以了解一下,有不少小伙伴在后臺留言說,想了解一下 sentinel中如何使用@SentinelResource和openFeign來進行服務熔斷和降級的操作,大家知道小農對于小伙伴的要求,那都是盡量滿足,今天我們就來好好說一下,@SentinelResourceopenFeign

SentinelResource

在上一節(jié)中,我們也使用到過這個注解,我們需要了解的是其中兩個屬性:

  • value: 資源名稱,必填且唯一。
@SentinelResource(value = "test/get")
  • entryType:非必填,entry類型,標記流量的方向,指明是出口流量,還是入口流量;取值 IN/OUT ,默認是OUT。
@SentinelResource(value = "test/get",entryType = EntryType.IN)
  • blockHandler: 處理異常(BlockException)的函數名稱,不必填,使用時注意兩點:

    1. 函數訪問的方法需要為public。
    2. 返回類型和入參需要和作用在原方法上一致且需要額外加一個(BlockException)類型的參數。
  • blockHandlerClass: 非必填,存放blockHandler的類。對應的處理函數必須static修飾,否則無法解析,必須是public,返回類型與原方法一致,參數類型需要和原方法相匹配,并在最后加上BlockException類型的參數

  • fallback: 非必填,用于在拋出異常的時候提供fallback處理邏輯。fallback函數可以針對所有類型的異常(除了execptionsToIgnore 里面排除掉的異常類型)進行處理

  • exceptionsToIgnore:非必填,指定排除掉哪些異常。排除的異常不會計入異常統(tǒng)計,也不會進入fallback邏輯,而是原樣拋出

image.png

默認限流

今天我們就針對于上面的幾個點詳細的展開介紹,在實際應用中我們如何進行操作。我們先來編寫一個新的控制器類型,這里我們使用cloud-alibaba-sentinel-8006項目進行操作,對應源碼已經放在開頭位置,需要請自取。

@SentinelResource 既可以配置資源名稱也可以配置URL,當我們配置了blockHandler屬性時,如果達到閾值時,會調用對應的方法提示限流信息,如果沒有配置blockHandler屬性,系統(tǒng)會走默認的限流信息(Blocked by Sentinel (flow limiting)

首先我們使用默認的@SentinelResource注解,系統(tǒng)會針對對應的地址調用默認的異常處理方法。

    @GetMapping("/restUrl")
    @SentinelResource(value = "restUrl")
    public String restUrl(){
        return " restUrl";
    }

注意:我們重啟項目之后,要先訪問,才能去設置對應的限流規(guī)則

先訪問http://localhost:8006/restUrl,在添加流控規(guī)則

image.png

此時如果沒有自己定義限流處理方法,會走系統(tǒng)默認的

image.png

blockHandler

使用@SentinelResource注解同時使用blockHandler屬性

    @GetMapping("resourceTest")
    @SentinelResource(value = "resourceTest",blockHandler = "handler_resource")
    public String resourceTest(){
        return "resourceTest";
    }

    public String handler_resource(BlockException exception){
        return "系統(tǒng)繁忙,請稍后再試";
    }

先訪問http://localhost:8006/resourceTest,在添加流控規(guī)則

image.png

再去快速的去訪問http://localhost:8006/resourceTest 就會出現我們在代碼中配置的限流異常處理信息,如下圖所示:

image.png

上面就展示了我們使用blockHandler屬性時,出現的我們自己設置的異常提示,但是當我們使用上面兩種方案的時候,會出現一些問題,如果我們的業(yè)務邏輯比較復雜,熔斷的業(yè)務場景比較多,上面的顯然不能夠滿足我們的應用,而且這種自定義方法是和我們的業(yè)務代碼耦合在一起的,在實際開發(fā)中,會顯得不夠優(yōu)雅,每個業(yè)務方法對添加一個對應的限流處理方法,會讓代碼顯得臃腫,而且無法實現統(tǒng)一處理。在這里我們就需要提到我們另外一個屬性—blockHandlerClass

blockHandlerClass

此屬性中設置的方法必需為 static 函數,否則無法解析。首先我們需要創(chuàng)建一個類用于專門處理自定義限流處理邏輯,這里記住,方法一定要是靜態(tài),否則無法解析,如下所示:

import com.alibaba.csp.sentinel.slots.block.BlockException;

/**
 * Sentinel限流自定義邏輯
 */
public class SentinelExptioinHandler {
    public static String handlerMethodError(BlockException exception){
        return "handlerMethodError:服務異常,請稍后重試!";
    }
    public static String handlerMethodNetwork(BlockException exception){
        return "handlerMethodNetwork:網絡錯誤,連接超時,請稍后重試!";
    }
}

同時我們添加一個可訪問的接口方法,設置@SentinelResource注解和blockHandlerClass屬性對應的類型和這個類型中對應的處理方法。

    /**
     * 此方法用到了自定義限流處理類型CustomerBlockHandler
     * 中的handlerException1方法來處理限流邏輯。
     */
    @GetMapping("/buildExption")
    @SentinelResource(value = "buildExption",
            blockHandlerClass = SentinelExptioinHandler.class,blockHandler = "handlerMethodError")
    public String buildExption(){
        return "hello buildExption";
    }

然后我們先訪問http://localhost:8006/buildExption后,來給它添加限流規(guī)則。

image.png

我們再次訪問http://localhost:8006/buildExption后,這個時候我們來看一下如果超過閾值之后使用的處理方法是否是我們的SentinelExptioinHandler.handlerMethodError(),當我們頻繁的訪問地址,就會看到出現了我們在異常處理類中設置的方法。

image.png

如果我們想要體現,網絡異常的操作,我們只需要替換blockHandler中的handlerMethodError改為handlerMethodNetwork,重啟項目后,重復上面的步驟,再來看一下,就會出現下面的提示:

image.png

服務熔斷

在微服務中,由于業(yè)務的拆分,一般會出現請求鏈路過程的情況,當一個用戶發(fā)起一個請求,通常需要幾個微服務才能完成,在高并發(fā)的場景下,這種服務之間的依賴對系統(tǒng)的穩(wěn)定性影響比較大,如果其中一個環(huán)節(jié)出現網絡延遲或者請求超時等問題會導致其他服務的不可用并形成阻塞,從而導致雪崩,服務熔斷就是用來解決這種情況,當一個服務提供在無法提供正常服務時,為了放在雪崩的方式,會將當前接口和外部隔離,觸發(fā)熔斷,在熔斷時間內,請求都會返回失敗,直到服務提供正常,才會結束熔斷。簡單來說,服務熔斷就是應對微服務雪崩的一種鏈路保護機制

為了模擬實際的應用場景,我們需要整合Ribbon+openFeign,來搭建真實的應用場景。首先我們需要利用Ribbon進行負載均衡的調用,我們先來創(chuàng)建消費者(cloud-alibab-consumer-8083)和兩個服務提供者(cloud-alibaba-provider-9003/9004)

image.png

我們先來搭建服務提供者

服務提供者

pom文件

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
    <groupId>com.muxiaonong</groupId>
    <artifactId>cloud-alibaba-commons</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

yml文件

server:
  port: 9003

spring:
  application:
    name: nacos-provider
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #配置Nacos地址

management:
  endpoints:
    web:
      exposure:
        include: '*'

主啟動類添加@EnableDiscoveryClient注解

@SpringBootApplication
@EnableDiscoveryClient
public class CloudAlibabaProvider9003Application {

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

添加商品信息請求類

@RestController
public class GoodsController {

    @Value("${server.port}")
    private String serverPort;

    //模仿數據庫存儲數據
    public static HashMap<Long,String> hashMap = new HashMap<>();
    static {
        hashMap.put(1l,"面膜");
        hashMap.put(2l,"哈密瓜");
        hashMap.put(3l,"方便面");
    }

    @GetMapping("queryGoods/{id}")
    public Response<String> queryGoods(@PathVariable("id") Long id){
        Response<String> response = new Response(200,"成功請求:"+serverPort,hashMap.get(id));
        return response;
    }
}

到這里服務提供者就搭建完成了

注意:另外一個服務提供者一樣,只需要端口不一樣即可,在這里就不做重復性的演示

服務消費者

pom

 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>com.muxiaonong</groupId>
    <artifactId>cloud-alibaba-commons</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>2.2.6.RELEASE</version>
</dependency>

yml文件

server:
  port: 8083
spring:
  application:
    name: nacos-consumer
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    sentinel:
      transport:
        #配置Sentinel dashboard地址
        dashboard: localhost:8080
        #默認8719端口,假如被占用會自動從8719開始依次+1掃描,直至找到未被占用的端口
        port: 8719

#消費者將要去訪問的微服務名稱(注冊成功進nacos的微服務提供者)
service-url:
  nacos-user-service: http://nacos-provider

主啟動類添加@EnableDiscoveryClient

@SpringBootApplication
@EnableDiscoveryClient
public class CloudAlibabConsumer8083Application {

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

}

訪問類


/**
 * @program: spring-cloud-alibaba
 * @ClassName DemoController
 * @description:
 * @author: 牧小農
 * @create: 2022-06-04 23:10
 * @Version 1.0
 **/
@RestController
public class DemoController {

    @Autowired
    private RestTemplate restTemplate;


    /**
     * 消費者去訪問具體服務,這種寫法可以實現
     * 配置文件和代碼的分離
     */
    @Value("${service-url.nacos-user-service}")
    private String serverURL;

    @GetMapping("/consumer/goods/{id}")
        public Response<String> fallback(@PathVariable Long id){
        //通過Ribbon發(fā)起遠程訪問,訪問9003/9004
        if(id <= 3) {
            Response<String> result = restTemplate.getForObject(serverURL + "/queryGoods/" + id, Response.class);
            return result;
        }else {
            throw new NullPointerException("未查詢到對應的數據");
        }
    }

}

我們先啟動9003/9004,在啟動8083,然后訪問http://localhost:8083/consumer/goods/2,就可以看到在瀏覽器中,如果9003/9004相互切換,說明我們搭建成功。

image.png
image.png

fallback

SentinelResourcefallback屬性,是一個可選項,主要用于拋出異常的時候提供處理邏輯,該函數可以針對所有的異常類型(除了exceptionsToIgnore排除的異常類型,等下會講解)進行處理,對于fallback的函數簽名和位置要求:

  • 返回值需和原函數返回在一致
  • 方法參數列表需要和原函數一致,可以額外多一個Throwbale類型的參數用來接收對應的異常
  • fallback 函數默認需要和原方法在同一個類中,如果希望使用其他類的函數,則可以指定 fallbackClass 為對應的類的 Class 對象,注意對應的函數必需為 static 函數,否則無法解析

案例:


    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/consumer/goods/{id}")
    //如果不設置這個注解和fallback參數,異常會原樣彈出
    //如果設置SentinelResource注解的fallback屬性,會按照設置的方法處理Java異常
    @SentinelResource(value = "falllback",fallback = "fallbackHandler")//被標注的異常將會被 原樣拋出
    public Response<String> fallback(@PathVariable Long id){
        //通過Ribbon發(fā)起遠程訪問,訪問9003/9004
        if(id <= 3) {
            Response<String> result = restTemplate.getForObject(serverURL + "/queryGoods/" + id, Response.class);
            return result;
        }else {
            throw new NullPointerException("未查詢到對應的數據");
        }
    }
    
    //保證方法簽名基本保持一致,但是要添加異常類型參數
    public Response<String> fallbackHandler(Long id,Throwable e){
        Response<String> result = new Response<>(500,"出現未知商品id","商品不存在");
        return result;
    }

在這里如果我們去訪問id超過3的數字的時候請求時(http://localhost:8083/consumer/goods/6 ),如果我們沒有設置fallback屬性,會彈出NullPointerException的錯誤

image.png

現在當我們去訪問設置了 fallback屬性的時http://localhost:8083/consumer/goods/6 會出現我們設置的參數。

image.png

fallback屬性和blockHandler有點類似,也可以設置fallbackClass屬性,用來指定對應類型,來處理對應的異常類型,但是方法也是需要為靜態(tài)方法,否則無法解析。

那么既然fallback屬性和blockHandler都能進行限流,那么他們有什么不同,哪一個的優(yōu)先級更高?首先我們要知道blockHandler屬性 是針對于Sentinel異常,blockHandler 對應處理 BlockException 的函數名稱,而fallback屬性針對于Java異常,如果我們同時設置blockHandler和fallback,會執(zhí)行哪個方法呢?我們來看一下

 @GetMapping("/consumer/goods/{id}")
    //如果不設置這個注解和fallback參數,異常會原樣彈出
    //如果設置SentinelResource注解的fallback屬性,會按照設置的方法處理Java異常
    @SentinelResource(value = "falllback",fallback = "fallbackHandler",blockHandler = "blockHandler")
    public Response<String> fallback(@PathVariable Long id){
        //通過Ribbon發(fā)起遠程訪問,訪問9003/9004
        if(id <= 3) {
            Response<String> result = restTemplate.getForObject(serverURL + "/queryGoods/" + id, Response.class);
            return result;
        }else {
            throw new NullPointerException("未查詢到對應的數據");
        }
    }

    //保證方法簽名基本保持一致,但是要添加異常類型參數
    public Response<String> fallbackHandler(Long id,Throwable e){
        Response<String> result = new Response<>(500,"出現未知商品id","商品不存在");
        return result;
    }

    //處理Sentinel限流
    public Response<String> blockHandler(Long id, BlockException e){
        Response<String> result = new Response<>(501,"sentinel限流操作","blockHandler 限流");
        return result;
    }

添加熔斷規(guī)則,在一秒內最小請求次數為5,如果異常超過2個時,觸發(fā)熔斷規(guī)則。

image.png

這個時候我們再來訪問http://localhost:8083/consumer/goods/6時,沒有觸發(fā)熔斷之前出現異常,由fallback進行處理

image.png

當我們快速點擊,觸發(fā)熔斷規(guī)則時,這是時候則由blockHandler進行處理。

image.png

當我們介紹上面的操作后,我們再給大家介紹關于sentinel的另外一個屬性 exceptionsToIgnore

exceptionsToIgnore

用于指定哪些異常被排除,不會計入異常統(tǒng)計中,也不會進入 fallback屬性處理的方法,會原樣拋出

    @GetMapping("/consumer/goods/{id}")
    //添加SentinelResource注解的fallback屬性,同時設置方法來解決Java異常
    @SentinelResource(value = "falllback",fallback = "fallbackHandler",blockHandler = "blockHandler",
            exceptionsToIgnore = {NullPointerException.class})//被標注的異常將會被 原樣拋出
    public Response<String> fallback(@PathVariable Long id){
        //通過Ribbon發(fā)起遠程訪問,訪問9003/9004
        if(id <= 3) {
            Response<String> result = restTemplate.getForObject(serverURL + "/queryGoods/" + id, Response.class);
            return result;
        }else {
            throw new NullPointerException("未查詢到對應的數據");
        }
    }

image.png

啟動項目,當我們再去訪問http://localhost:8083/consumer/goods/6的時候,出現原有異常。

在這一節(jié)中,我們主要講解了sentinel服務熔斷的這些事,包括@SentinelResource注解的使用方式和場景,以及ribbon實現負載均衡的使用,服務熔斷場景我們主要講解兩個,一個是ribbon實現的,一個是openFeign實現。下面我們就來了解一下基于openFeign如何實現負載均衡和服務熔斷。

openFeign

OpenFeign是一種聲明式、模板化的HTTP客戶端。在Spring Cloud中使用OpenFeign,可以做到使用HTTP請求訪問遠程服務,就像調用本地方法一樣的,開發(fā)者完全感知不到這是在調用遠程方法,更感知不到在訪問HTTP請求,用法其實就是編寫一個接口,在接口上添加注解即可。

可以簡單理解它是借鑒Ribbon的基礎之上,封裝的一套服務接口+注解的方式的遠程調用器。由它來幫助我們定義和實現依賴服務接口的定義,只需創(chuàng)建一個接口并使用注解的方式進行配置。進一步簡化我們的操作。

演示項目為:cloud-alibaba-openFeign-8009,調用服務為9003/9004,源碼在開頭,需要請自取

pom

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>2.2.6.RELEASE</version>
</dependency>

yml配置

server:
  port: 8009
spring:
  application:
    name: nacos-consumer-openFeign
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        
management:
  endpoints:
    web:
      exposure:
        include: '*'

啟動類添加 @EnableFeignClients

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class CloudAlibabaOpenFeign8009Application {

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

}

@FeignClient

@Service
@FeignClient("nacos-provider")
public interface GoodsFeign {

    @GetMapping("queryGoods/{id}")
    public Response<String> queryGoods(@PathVariable("id") Long id);
}

請求控制類

@RestController
public class FeignController {

    @Autowired
    private GoodsFeign goodsFeign;

    @GetMapping("query/{id}")
    public Response<String> query(@PathVariable("id") Long id){
        return goodsFeign.queryGoods(id);
    }
}

我們一次啟動,9003/9004,以及我們的消費者服務cloud-alibaba-openFeign-8009,當我們的服務都啟動成功后,訪問http://localhost:8009/query/1,如果看到我們的端口切換展示就表示成功了

image.png

OpenFeign設置超時時間

OpenFeign 默認的超時時間為一秒鐘,如果服務端業(yè)務超過這個時間,則會報錯,為了避免這樣的情況,我們可以設置feign客戶端的超時控制。我們先來看一下如果我們設置一個延時任務openFeign會提示怎么樣的信息。我們需要在服務提供者(9003/9004)那里設置一個阻塞三秒的請求。

    @GetMapping("/readTimeOut")
    public String readTimeOut() {
        try {
            System.out.println(serverPort+"網絡連接超時,延遲響應");
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return serverPort;
    }

然后通過feign進行調用

   @GetMapping("/readTimeOut")
    public String readTimeOut();
   @GetMapping("/query/readTimeOut")
    public String readTimeOut() {
        String str = goodsFeign.readTimeOut();
        return str;
    }

這個時候當我們去訪問http://localhost:8009/query/readTimeOut時,客戶端會提示報錯,提示我們連接超時

image.png

image.png

這個時候我們可以設置feign的超時時間進行控制,由于OpenFeign 底層是ribbon 。所以超時控制由ribbon來控制。在yml文件中配置,只需要在8009中的yml添加這樣一段代碼即可。

ribbon: #設置feign客戶端超時時間(默認支持ribbon)
  ReadTimeout: 5000 #建立連接所用的時間,適用于網絡狀況正常的情況下,兩端連接所用的時間
  ConnectTimeout: 5000  #建立連接后從服務器讀取到可用資源所用的時間

當我們重新啟動項目后,再來訪問我們當前接口,成功返回正確信息

image.png

說起OpenFeign,我們不得不提它的一個很小,但是很實用的一個日志功能。我們可以通過配置調整日志級別,這樣有利于我們從feign中了解請求和響應的細節(jié),對接口的調用情況進行監(jiān)控。

OpenFeign 日志級別分類四種

  • NONE :默認的,不顯示任何日志;
  • BASIC :僅記錄請求方法、URL、響應狀態(tài)碼及執(zhí)行時間;
  • HEADERS :除了 BASIC 中定義的信息之外,還有請求和響應的頭信息;
  • FULL(推薦使用) :除了 HEADERS 中定義的信息之外,還有請求和響應的正文及元數據。

我們在啟動類中通過@Bean注解注入日志功能即可

@Bean
    Logger.Level feignLoggerLevel(){
        //開啟全日志
        return Logger.Level.FULL;
    }

yml中添加日志開啟功能

logging:
  level:
    # openfeign日志以什么級別監(jiān)控哪個接口
    com.muxiaonong.feign.GoodsFeign: debug
image.png

這樣我們就可以在請求調用以后看到日志的詳細信息了

image.png

我們已經了解了openFeign的基本使用,那么我們要如何將Sentinel和OpenFeign進行整合呢,下面我們就來帶大家通過Sentinel來進行整合OpenFegin

yml中添加Sentinel對OpenFeign的支持

# 激活Sentinel對OpenFeign的支持
feign:
  sentinel:
    enabled: true

在feign中添加對fallback的支持

@Service
@FeignClient(value = "nacos-provider",fallback = GoodsServiceImpl.class)
public interface GoodsFeign {

    @GetMapping("queryGoods/{id}")
    public Response<String> queryGoods(@PathVariable("id") Long id);

    @GetMapping("/readTimeOut")
    public String readTimeOut();
}

@Component
public class GoodsServiceImpl implements GoodsFeign {

    @Override
    public Response<String> queryGoods(Long id) {
        return new Response<>(501,"服務降級處理返回信息",null);
    }


    @Override
    public String readTimeOut() {
        return null;
    }
}

這個時候我們來請求http://localhost:8009/query/1,時是正常的,但是當我們關閉服務提供者(9003/9004)時,就出觸發(fā)服務降級操作,提示下面信息

image.png

總結

熔斷由服務不可用引起,降級由業(yè)務實際情況和系統(tǒng)資源負載設置等關系引起,不管是對于熔斷還是降級都是從系統(tǒng)穩(wěn)定性出發(fā),保證系統(tǒng)的最大可用。

到這里,我們今天的內容就講完了,有疑問或者想要交流的小伙伴記得在下方留言,小農看見了會第一時間回復大家。如果覺得文中內容對你有幫助,記得點贊關注,您的支持是我創(chuàng)作的最大動力!

我是牧小農,怕什么真理無窮,進一步有進一步的歡喜,大家加油!

?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容