用100行代码手写一个Hystrix


熔断与降级
离小眼睛家不远的地方 , 开了一个熟食店 。 店内有两个窗口总能排起长龙 , 一个窗口是选好的凉菜让师傅调味 , 一个窗口是买到的扒鸡让胖师傅现场脱骨 。 顾客的正常的流程 , 大致是这个样子滴:
【用100行代码手写一个Hystrix】
用100行代码手写一个Hystrix
本文插图
炎炎夏日 , 邀三五好友 , 喝杯啤酒吹吹牛皮 , 岂不美哉 。 可能大家跟小眼睛想法一致 , 小店的生意日渐火爆 。 这天 , 小眼睛选好了菜 , 付了钱 , 正准备排队让师傅调口味、脱骨 。 目测两个窗口排队时间不会少于 20 分钟 , 加之几个朋友轮番催促 , 果断放弃 , 拎着菜直接回家 。 于是我到流程就变成了:
用100行代码手写一个Hystrix
本文插图
当下游的服务(调料、脱骨)因为某种原因突然变得不可用或响应过慢(买菜3分钟排队半小时) , 上游服务为了保证自己整体服务的可用性(等不及了) , 不再继续调用目标服务 , 直接返回 , 快速释放资源 。 如果目标服务情况好转则恢复调用 。 这就叫做服务熔断 。
小眼睛因为排队时间过长 , 果断放弃后续流程 , 提供了「降低品质」的菜品 。 这叫做服务降级 。
熔断有多种方式
服务降级的方式有很多种 , 比如限流、开关、熔断 , 熔断是降级的一种 。
熔断 , 在 Spring Cloud 中有熔断降级库 Hystrix, 在分布式项目中也可以使用阿里开源的 Sentinel 达到熔断降级目的 。 无论是 Hystrix 还是 Sentinel 都需要引入第三方组件 , 搞明白实现原理 , 不适合简单场景下的使用 。
手写熔断器的使用
本文介绍一种适合简单应用的熔断方法 , 核心代码不超过 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. 记录调用次数和失败次数 , 作为熔断依据
熔断器的实现
熔断器具体实现如下:
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 fail = new ThreadLocal(); 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); } } }总体思路: