Hystrix一些概念

Hystrix 服务熔断

熔断机制是为了应对雪崩效应而出现的一种微服务链路保护机制,熔断一般配置在服务提供者,即作为一个服务给别人调用的时候,出现了某种异常情况对自己进行保护。

熔断状态

在熔断机制中涉及了三种熔断状态:

  • 熔断关闭状态(Closed):当服务访问正常时,熔断器处于关闭状态,服务调用方可以正常地对服务进行调用。
  • 熔断开启状态(Open):默认情况下,在固定时间内接口调用出错比率达到一个阈值(例如 50%),熔断器会进入熔断开启状态。进入熔断状态后,后续对该服务的调用都会被切断,熔断器会执行本地的降级(FallBack)方法。
  • 半熔断状态(Half-Open): 在熔断开启一段时间之后,熔断器会进入半熔断状态。在半熔断状态下,熔断器会尝试恢复服务调用方对服务的调用,允许部分请求调用该服务,并监控其调用成功率。如果成功率达到预期,则说明服务已恢复正常,熔断器进入关闭状态;如果成功率仍旧很低,则重新进入熔断开启状态。

Hystrix 实现服务熔断的步骤如下:

  1. 当服务的调用出错率达到或超过 Hystix 规定的比率(默认为 50%)后,熔断器进入熔断开启状态。
  2. 熔断器进入熔断开启状态后,Hystrix 会启动一个休眠时间窗,在这个时间窗内,该服务的降级逻辑会临时充当业务主逻辑,而原来的业务主逻辑不可用。
  3. 当有请求再次调用该服务时,会直接调用降级逻辑快速地返回失败响应,以避免系统雪崩。
  4. 当休眠时间窗到期后,Hystrix 会进入半熔断转态,允许部分请求对服务原来的主业务逻辑进行调用,并监控其调用成功率。
  5. 如果调用成功率达到预期,则说明服务已恢复正常,Hystrix 进入熔断关闭状态,服务原来的主业务逻辑恢复;否则 Hystrix 重新进入熔断开启状态,休眠时间窗口重新计时,继续重复第 2 到第 5 步。

下图是详细的流程图:

配置案例:

1.主配置类上添加@EnableEurekaClient (开启eureka客户端),@EnableCircuitBreaker(开启断路器)。

2.service方法上配置断路器熔断参数(开启断路器,在10秒的窗口期,调用次数超过10次,且失败率达到60%,即进行服务熔断)。

import cn.hutool.core.util.IdUtil;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.concurrent.TimeUnit;

@Service
public class PaymentService {

    //=====服务熔断 10s之内 10次请求有6次失败 就会开启断路器
    @HystrixCommand(fallbackMethod = "paymentCircuitBreakerFallback", commandProperties = {
            @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),// 是否开启断路器
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),// 请求次数
            @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 时间窗口期
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"),// 失败率达到多少后跳闸
    })
    public String paymentCircuitBreaker(Integer id) {
        if (id < 0) {
            throw new RuntimeException("******id 不能负数");
        }
        String serialNumber = IdUtil.simpleUUID();

        return Thread.currentThread().getName() + "\t" + "调用成功,流水号: " + serialNumber;
    }

    public String paymentCircuitBreakerFallback(Integer id) {
        return "id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: " + id;
    }

}

3.配置调用service的controller

import com.hyh.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;

@RestController
@Slf4j
public class PaymentController {
    @Resource
    private PaymentService paymentService;

    //====服务熔断
    @GetMapping("/payment/circuit/{id}")
    public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
        String result = paymentService.paymentCircuitBreaker(id);
        log.info("****result: " + result);
        return result;
    }
}

        我们浏览器先成功访问controller http://localhost:8001/payment/circuit/1 。控制台打印如下:

2024-05-19 11:29:37.872  INFO 5680 --- [io-8001-exec-10] c.h.s.controller.PaymentController       : ****result: hystrix-PaymentService-10	调用成功,流水号: afe8f78e6eb740dc8b6a2df2810b31e0
2024-05-19 11:29:46.966  INFO 5680 --- [nio-8001-exec-1] c.h.s.controller.PaymentController       : ****result: hystrix-PaymentService-10	调用成功,流水号: b2a5086c1aa24a77aff05142dcfea4d9
2024-05-19 11:29:47.335  INFO 5680 --- [nio-8001-exec-2] c.h.s.controller.PaymentController       : ****result: hystrix-PaymentService-10	调用成功,流水号: 3c9f32e0a7e74693abe2685b684ec685

        我们再把参数设置为-1,快速狂刷10次,控制台显示如下:

2024-05-19 11:29:51.933  INFO 5680 --- [nio-8001-exec-3] c.h.s.controller.PaymentController       : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: -1
2024-05-19 11:29:53.860  INFO 5680 --- [nio-8001-exec-8] c.h.s.controller.PaymentController       : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: -1
2024-05-19 11:29:54.268  INFO 5680 --- [nio-8001-exec-9] c.h.s.controller.PaymentController       : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: -1
2024-05-19 11:29:54.636  INFO 5680 --- [nio-8001-exec-7] c.h.s.controller.PaymentController       : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: -1
2024-05-19 11:29:54.955  INFO 5680 --- [nio-8001-exec-6] c.h.s.controller.PaymentController       : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: -1
2024-05-19 11:29:55.292  INFO 5680 --- [nio-8001-exec-4] c.h.s.controller.PaymentController       : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: -1
2024-05-19 11:29:55.618  INFO 5680 --- [nio-8001-exec-5] c.h.s.controller.PaymentController       : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: -1
2024-05-19 11:29:55.947  INFO 5680 --- [io-8001-exec-10] c.h.s.controller.PaymentController       : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: -1
2024-05-19 11:29:56.308  INFO 5680 --- [nio-8001-exec-1] c.h.s.controller.PaymentController       : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: -1
2024-05-19 11:29:56.683  INFO 5680 --- [nio-8001-exec-2] c.h.s.controller.PaymentController       : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: -1

         我们再赶快把参数设置为原来正常的参数1,发现服务确实被熔断保护起来了,仍然不能调用,一段时间后(默认是5秒),这个时候断路器是半开状态,会让其中一个进行转发。如果成功,断路器会关闭,若失败,继续开启。

2024-05-19 11:29:56.683  INFO 5680 --- [nio-8001-exec-2] c.h.s.controller.PaymentController       : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: -1
2024-05-19 11:30:03.358  INFO 5680 --- [nio-8001-exec-3] c.h.s.controller.PaymentController       : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: 1
2024-05-19 11:30:05.115  INFO 5680 --- [nio-8001-exec-8] c.h.s.controller.PaymentController       : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: 1
2024-05-19 11:30:05.501  INFO 5680 --- [nio-8001-exec-9] c.h.s.controller.PaymentController       : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: 1
2024-05-19 11:30:05.875  INFO 5680 --- [nio-8001-exec-7] c.h.s.controller.PaymentController       : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: 1
2024-05-19 11:30:06.277  INFO 5680 --- [nio-8001-exec-6] c.h.s.controller.PaymentController       : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: 1
2024-05-19 11:30:06.646  INFO 5680 --- [nio-8001-exec-4] c.h.s.controller.PaymentController       : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: 1
2024-05-19 11:30:06.986  INFO 5680 --- [nio-8001-exec-5] c.h.s.controller.PaymentController       : ****result: hystrix-PaymentService-10	调用成功,流水号: f9307e7c12e14131acfda81a35e7bbe7

Hystrix 服务降级

        Hystrix 提供了服务降级功能,能够保证当前服务不受其他服务故障的影响,提高服务的健壮性。既然是这样,则说明降级配置在服务提供方,也能配在消费方。

服务降级的使用场景有以下 2 种:

  • 在服务器压力剧增时,根据实际业务情况及流量,对一些不重要、不紧急的服务进行有策略地不处理或简单处理,从而释放服务器资源以保证核心服务正常运作。
  • 当某些服务不可用时,为了避免长时间等待造成服务卡顿或雪崩效应,而主动执行备用的降级逻辑立刻返回一个友好的提示,以保障主体业务不受影响。

详细来说就是:

  1. 程序运行异常
  2. 超时
  3. 服务熔断触发服务降级
  4. 线程池/信号量打满也会导致服务降级
详细例子

1.服务端的服务降级。

假设我们有个服务PaymentService,自身有个paymentInfoTimeOut方法,若4900毫秒未执行完毕,就进行降级操作,这里我们给这个方法执行时间睡眠个5秒钟,模拟5秒此服务才执行完毕。

import cn.hutool.core.util.IdUtil;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.concurrent.TimeUnit;

@Service
public class PaymentService {

    /**
     * 正常访问,肯定OK
     */
    public String paymentInfoOk(Integer id) {
        return "线程池:  " + Thread.currentThread().getName() + "  paymentInfo_OK,id:  " + id + "\t" + "O(∩_∩)O哈哈~";
    }

    @HystrixCommand(fallbackMethod = "paymentInfoTimeOutHandler", commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "4900")
    })
    public String paymentInfoTimeOut(Integer id) {
        //int age = 10/0;
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "线程池:  " + Thread.currentThread().getName() + " id:  " + id + "\t" + "O(∩_∩)O哈哈~" + "  耗时(秒): ";
    }

    public String paymentInfoTimeOutHandler(Integer id) {
        return "线程池:  " + Thread.currentThread().getName() + "  8001系统繁忙或者运行报错,请稍后再试,id:  " + id + "\t" + "o(╥﹏╥)o";
    }

}

写个controller来暴漏这个service的访问接口。

import com.hyh.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;

@RestController
@Slf4j
public class PaymentController {
    @Resource
    private PaymentService paymentService;

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

    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfoOk(@PathVariable("id") Integer id) {
        String result = paymentService.paymentInfoOk(id);
        log.info("*****result: " + result);
        return result;
    }

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfoTimeOut(@PathVariable("id") Integer id) {
        String result = paymentService.paymentInfoTimeOut(id);
        log.info("*****result: " + result);
        return result;
    }

}

然后我们再来编写一个客户端,来请求这个服务端。

先配置远程调用服务端的接口。

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@Component
@FeignClient(value = "cloud-provider-hystrix-payment",fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {
    @GetMapping("/payment/hystrix/ok/{id}")
    String paymentInfOk(@PathVariable("id") Integer id);

    @GetMapping("/payment/hystrix/timeout/{id}")
    String paymentInfTimeOut(@PathVariable("id") Integer id);
}

顺便指定客户端降级的方法PaymentFallbackService。

import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

@Component
public class PaymentFallbackService implements PaymentHystrixService {

    @Override
    public String paymentInfOk(Integer id) {
        return "PaymentFallbackService 失败降级了 "+id;
    }

    @Override
    public String paymentInfTimeOut(Integer id) {
        return "PaymentFallbackService 超时降级了 "+id;
    }
}

编写客户端的请求接口。

import com.hyh.springcloud.service.PaymentHystrixService;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Slf4j
@DefaultProperties(defaultFallback = "paymentGlobalFallbackMethod")
public class OrderHystirxController {
    @Resource
    private PaymentHystrixService paymentHystrixService;

    @GetMapping("/consumer/payment/hystrix/ok/{id}")
    public String paymentInfoOk(@PathVariable("id") Integer id) {
        return paymentHystrixService.paymentInfOk(id);
    }

    /*@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5500")
    })*/
    @HystrixCommand
    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
    public String paymentInfoTimeOut(@PathVariable("id") Integer id) {
        //int age = 10 / 0;
        String s = paymentHystrixService.paymentInfTimeOut(id);
        log.info(s);
        return s;
    }

    public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
        return "我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
    }

    /**
     * 下面是全局fallback方法
     */
    public String paymentGlobalFallbackMethod() {
        return "(客户端)Global异常处理信息,请稍后再试,/(ㄒoㄒ)/~~";
    }
}

访问客户端接口http://localhost:80/consumer/payment/hystrix/timeout/1

服务端的控制台发现服务端直接超时降级了:

客户端页面返回:

客户端的控制台:

2.客户端降级操作。

我们在客户端的paymentInfoTimeOut方法中,报一个客户端上的异常(int age = 10 / 0;)。

import com.hyh.springcloud.service.PaymentHystrixService;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @author summerday
 */
@RestController
@Slf4j
@DefaultProperties(defaultFallback = "paymentGlobalFallbackMethod")
public class OrderHystirxController {
    @Resource
    private PaymentHystrixService paymentHystrixService;

    @GetMapping("/consumer/payment/hystrix/ok/{id}")
    public String paymentInfoOk(@PathVariable("id") Integer id) {
        return paymentHystrixService.paymentInfOk(id);
    }

    /*@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5500")
    })*/
    @HystrixCommand
    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
    public String paymentInfoTimeOut(@PathVariable("id") Integer id) {
        int age = 10 / 0;
        String s = paymentHystrixService.paymentInfTimeOut(id);
        log.info(s);
        return s;
    }

    public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
        return "我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
    }

    /**
     * 下面是全局fallback方法
     */
    public String paymentGlobalFallbackMethod() {
        return "(客户端)Global异常处理信息,请稍后再试,/(ㄒoㄒ)/~~";
    }
}

然后再次访问客户端接口http://localhost:808/consumer/payment/hystrix/timeout/1

发现客户端抛出了它的全局异常。

当然我们也可以针对这个异常方法,定义属于自己的异常处理方法:

    @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5500")
    })
    //@HystrixCommand
    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
    public String paymentInfoTimeOut(@PathVariable("id") Integer id) {
        int age = 10 / 0;
        String s = paymentHystrixService.paymentInfTimeOut(id);
        log.info(s);
        return s;
    }

    public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
        return "我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
    }

再次访问:

这里要着重注意的是:如果异常属于服务端的,那么它的异常处理会走 PaymentFallbackService对应的降级方法;如果异常属于客户端自己的,它的异常处理会走客户端自己定义的paymentTimeOutFallbackMethod方法。

这里还需要注意的是:客户端访问服务端,如果服务端产生了异常,我们如何获取服务端的异常呢?

        在使用Feign作为声明式HTTP客户端时,如果后端服务调用失败(例如,超时、网络错误、服务端返回非2xx状态码等),你可以使用@FeignClient注解的fallbackfallbackFactory属性来定义回退逻辑。然而,默认情况下,Feign的Fallback实现并不直接提供后端服务产生的具体异常或错误消息。

为了获取服务端的异常提示,你可以采取以下几种方法:

  1. 使用ErrorDecoder
    你可以自定义一个ErrorDecoder,并在其中捕获和处理HTTP响应。如果响应指示了一个错误(例如,非2xx状态码),你可以从这个响应中提取错误消息,并将其封装到一个自定义的异常中,然后抛出这个异常。你的Fallback实现可以捕获这个异常并据此提供回退逻辑。

  2. 使用FallbackFactory
    相比于简单的Fallback类,FallbackFactory允许你访问触发回退的请求和异常(如果有的话)。你可以使用这个信息来定制你的回退逻辑。例如,你可以检查异常的类型或消息,并据此提供不同的回退行为。

  3. 使用Hystrix(如果你在使用它)
    如果你的Feign客户端与Hystrix集成,那么当命令失败时,Hystrix会触发回退逻辑。你可以在Hystrix的Command执行逻辑中捕获异常,并尝试从异常中提取服务端的错误信息。然而,请注意,Hystrix已经被宣布进入维护模式,并且不再接受新功能。

  4. 在服务端统一错误处理
    另一个策略是在你的服务端应用中统一处理错误,并确保所有错误都返回一个标准的错误格式(例如,JSON格式的错误对象)。这样,无论服务端发生什么错误,你的Feign客户端都可以解析这个标准的错误格式,并据此提供适当的回退逻辑。

  5. 日志记录
    如果你只是想在开发或调试过程中查看服务端的错误提示,那么你可以简单地配置Feign或你的日志框架来记录详细的请求和响应。这样,当后端服务调用失败时,你可以查看日志以获取更多信息。

(备注)spring cloud 一些相关组件的默认超时时间如下:

  1. Zuul网关

    • zuul.host.socket-timeout-millis:默认值为1000毫秒(1秒)。
    • zuul.host.connect-timeout-millis:默认值为(可能是)2000毫秒(2秒)。但请注意,这些默认值可能因Spring Cloud的版本而异。
  2. Ribbon

    • ReadTimeout:负载均衡超时时间,默认值为5000毫秒(5秒)。
    • ConnectTimeout:Ribbon请求连接的超时时间,默认值可能是2000毫秒(2秒),但这也可能因版本而异。
    • MaxAutoRetries:对当前实例的重试次数,默认值为0。
    • MaxAutoRetriesNextServer:对切换实例的重试次数,默认值为1。
  3. Feign

    • Feign调用服务的默认时长是1秒钟。但Feign的底层使用Ribbon,因此也可以通过配置Ribbon来调整Feign的超时时间。
    • 使用ribbon.ReadTimeoutribbon.ConnectTimeout可以设置Feign的连接和读取超时时间。
  4. Hystrix

    • Hystrix默认的超时时间(阻塞)是2000毫秒(2秒)。超过这个时间,将发生熔断请求。但请注意,只有在你的项目中引入了Hystrix并使用它作为熔断器时,这个默认超时时间才适用。

整体调用流程:

  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值