用100行代码手写一个Hystrix

845e605d60192bcbe5a513d197ae9763.gif

01

GAOKAO

熔断与降级

离小眼睛家不远的地方,开了一个熟食店。店内有两个窗口总能排起长龙,一个窗口是选好的凉菜让师傅调味,一个窗口是买到的扒鸡让胖师傅现场脱骨。顾客的正常的流程,大致是这个样子滴:

7bd323ed3b65bb98059c3a3f9b1ed42e.png

炎炎夏日,邀三五好友,喝杯啤酒吹吹牛皮,岂不美哉。可能大家跟小眼睛想法一致,小店的生意日渐火爆。这天,小眼睛选好了菜,付了钱,正准备排队让师傅调口味、脱骨。目测两个窗口排队时间不会少于 20 分钟,加之几个朋友轮番催促,果断放弃,拎着菜直接回家。于是我到流程就变成了:

e22e248ce4da18fff7235674ddf80069.png

当下游的服务(调料、脱骨)因为某种原因突然变得不可用或响应过慢(买菜3分钟排队半小时),上游服务为了保证自己整体服务的可用性(等不及了),不再继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用。这就叫做服务熔断

小眼睛因为排队时间过长,果断放弃后续流程,提供了「降低品质」的菜品。这叫做服务降级

02

GAOKAO

熔断有多种方式

服务降级的方式有很多种,比如限流、开关、熔断,熔断是降级的一种。

熔断,在 Spring Cloud 中有熔断降级库 Hystrix ,在分布式项目中也可以使用阿里开源的 Sentinel 达到熔断降级目的。无论是 Hystrix 还是 Sentinel 都需要引入第三方组件,搞明白实现原理,不适合简单场景下的使用。

03

GAOKAO

手写熔断器的使用

本文介绍一种适合简单应用的熔断方法,核心代码不超过 100 行。使用方法大致如下:

// 初始化一个熔断器
private CircuitBreaker breaker = 
    new CircuitBreaker(0.1, 10, 
            true, "serviceDemo");

public void doSomething() {
    // 每次调用都检查服务状态
    breaker.checkStatus();
    // 如果熔断器返回 true 认
   //为服务可用,继续执行逻辑

    if (breaker.isWorked()) {
        try {
            service.doSomething();
        } catch (Exception e) {
            e.printStackTrace();
            // 出现调用失败,记录失败次数
            breaker.addFailTimes();
        } finally {
            // 每一次调用,增加调用次数
            breaker.addInvokeTimes();
        }
    }
    // 服务不可用,执行降级逻辑
}

这段伪代码中,熔断器做了三件事儿:

  1. 检查服务状态,并且输出统计日志

  2. 返回服务状态 breaker.isWorked()

  3. 记录调用次数和失败次数,作为熔断依据

04

GAOKAO

熔断器的实现

熔断器具体实现如下:

public class CircuitBreaker {
  /**
   * 记录失败次数
   */
  private AtomicLong failTimes =
          new AtomicLong(0);
  /**
   * 记录调用次数
   */
  private AtomicLong invokeTimes =
          new AtomicLong(0);
  /**
   * 降级阈值,比如 0.1
   * 请求失败次数/请求总次数的比例
   */
  private double failedRate = 0.1;
  /**
   * 降级最小条件,请求总次数大于该值
   * 才会执行阈值判断
   * 比如 设置为 10 ,
   * 当请求次数大于10次时才会执行判断
   */
  private double minTimes;
  /**
   * 熔断开关,默认关闭
   */
  private boolean enabled;
  /**
   * 熔断后是否发送邮件告警
   */
  private boolean mail;
  /**
   * 熔断后是否发送短信告警
   */
  private boolean sms;
  /**
   * 熔断器名字
   */
  private String name;
  /**
   * 保存上一次统计的时间戳,记录单位是分钟
   */
  private AtomicLong currentTime =
          new AtomicLong(
             System.currentTimeMillis() / 60000);
  /**
   * 记录服务是否是不可用状态
   */
  private AtomicBoolean isFailed =
          new AtomicBoolean(false);
  /**
   * 服务宕掉的状态放到线程容器中
   */
  private ThreadLocal<Boolean> fail =
          new ThreadLocal<Boolean>();
  private Logger log =
          LoggerFactory.getLogger(getClass());
  /**
   * 构造熔断器
   *
   * @param failedRate 熔断的阈值,
   *                   失败次数/总请求次数
   * @param minTimes   熔断的最小条件,
   *                   请求总次数大于该值才会根据阈值判断,
   *                   执行降级操作
   * @param enabled    是否需开启熔断操作
   */
  public CircuitBreaker(double failedRate,
                        double minTimes,
                        boolean enabled,
                        String name) {
    fail.set(false);
    this.failedRate = failedRate;
    this.minTimes = minTimes;
    this.enabled = enabled;
    this.name = name;
  }

  /**
   * 判断服务是否是失败状态
   *
   * @return
   */
  public boolean isFailed() {
    return isFailed.get();
  }

  /**
   * 增加错误次数
   */
  public void addFailTimes() {
    fail.set(true);
    if (enabled) {
      failTimes.incrementAndGet();
    }
  }

  /**
   * 增加一次调用次数
   */
  public void addInvokeTimes() {
    if (enabled) {
      invokeTimes.incrementAndGet();
    }
  }

  /**
   * 判断服务是否可用
   *
   * @return
   */
  public boolean isWorked() {
    if (!enabled) {
      return true;
    }
    // 当服务不可用时,牺牲掉 1% 的流量做探活请求
    if (isFailed.get() &&
        System.currentTimeMillis() % 100 == 0) {
      return true;
    }
    if (isFailed.get()) {
      fail.set(true);
      return false;
    }
    return true;
  }

  public void checkStatus() {
    if (!enabled) {
      return;
    }
    long newTime =
       System.currentTimeMillis() / 60000;
    if ((newTime > currentTime.get())
       && (invokeTimes.get() > minTimes)) {

      double percent =
              failTimes.get() * 1.0 /
                      invokeTimes.get();
      if (percent > failedRate) {
        if (isFailed.get()) {
          // 日志输出
          if (mail) {
            // 发送邮件通知
          }
        } else {
          // 日志输出
          isFailed.set(true);
          if (sms) {
            // 发送短信通知
          }
          if (mail) {
            // 发送邮件通知
          }
        }
      } else { // 服务恢复
        if (isFailed.get()) {
          // 日志输出
          if (sms) {
            // 发送短信通知
          }
          if (mail) {
            // 发送邮件通知
          }
        }
        isFailed.set(false);
      }
      if (log.isInfoEnabled()) {
        // 日志输出
      }
      currentTime.set(newTime);
      failTimes.set(0);
      invokeTimes.set(0);
    }
  }

}

总体思路:

  1. 基于统计信息做熔断,错误请求占比超过阈值做熔断

  2. 统计周期在分钟级别内(1 分钟内的统计达到阈值)

  3. 如果分钟内,总请求次数未达到 minTimes 次数不做熔断(请求频次太低,统计信息无意义)

  4. 即便是到达熔断条件,仍然牺牲 1% (可修改)的请求做探活

isFailed.get()&&System.currentTimeMillis() % 100 == 0

05

GAOKAO

优缺点

Hystrix 提供了服务熔断、线程隔离等一系列服务保护功能。我们手写的熔断器只能提供基于调用方的手工熔断方法。

Hystrix 提供了线程池、信号量两种方式。手写熔断器功能相对单一只基于统计信息,且以分钟为维度的颗粒度较为粗糙。

Hystrix 命令式编程和注册回调的方式,代码复杂度高。手写熔断器在侵入代码过程中,偏面向过程,理解成本低。

去掉注释和无效空行后实际有效代码不足 100 行,我们用了不到一百行代码实现了熔断功能。虽然应用到大型服务场景下会有诸多缺陷,也希望至少能为大家提供了一个思路。

今天的分享就到这里,感谢您的关注,欢迎与我交流沟通,欢迎阅读往期文章:

NIO 看破也说系列

那个业务大拿死在了这个地方

手写负载均衡算法

眼中有码,心中无码

一个包子铺看懂 I/O 模型演变

57秒让狗哥搞懂反向代理

be2920a75bfd88893357d62d7eabb71c.gif

戳“阅读原文”我们一起进步

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值