什么是服务雪崩? 什么是服务保护? 服务保护的措施有哪些? 熔断怎么做? 限流怎么做? 服务隔离怎么做? 降级怎么做?
真实案例
举一个自己遇到的真实的例子. 接口 A 依赖了服务 B, 接口 A 的部署情况是有两个机房部署, 服务 B 的部署情况也是两个机房部署. 用户请求接口失败会重试, 部署架构图如下:
说明: 服务部署用到的是 Linux+Nginx+PHP 技术栈.
当时遇到的情况是服务 B 所在的机房挂了, 导致接口 A 调用服务 B 超时, 超时返回之后 nginx 重试到 A 的另一个节点, 继续调用服务 B,A 的所有节点都失败后, 返回给客户端失败结果, 客户端进行重试, 于是再进行一次刚刚的步骤, 这些超时的请求占用了 PHP 的进程没有释放, 同时用户侧体验感知到缓慢, 于是不断刷新重试, 导致流量暴涨, PHP 的进程池被耗尽了, 于是接口 A 就无法访问了, 其他依赖接口 A 的功能也无法使用, 导致整个站点雪崩.
这是典型的服务没有进行隔离导致功能雪崩的例子, 那么问题来了, 如果要对这次的故障进行改进, 为接口和服务之间加入一层服务保护, 那么要怎么做呢?
业界比较常见的服务保护主要有以下这些:
1, 限流
当发现服务失败数量达到某个阈值, 拒绝访问, 限制更多流量的到来, 防止过多失败的请求将资源耗尽.
2, 服务隔离
将不同类型的接口隔离部署, 单个类型接口的失败甚至进程池被耗尽不会影响其他接口的正常访问, 比如在资讯平台中, 如果发布和阅读的接口分开部署了, 那么即使发布功能失效, 阅读功能还能继续使用.
3, 熔断
从接口请求连接就拒绝访问, 类似家里用的保险丝, 使用的电器总和超过了电压就熔断保险丝, 整个电路短了, 保护整个区域的电路防止更多的损失.
4, 降级
对于简单的展示功能, 如果有失败的请求, 返回默认值. 对于整个站点或客户端, 如果服务器负载过高, 将其他非核心业务停止, 以让出更多资源给其他服务使用.
以上是笔者所知道服务出现雪崩的情况以及保护服务的措施, 在 Java 领域中, 业界用得比较多的是 Hystrix, 那么就来看看它是怎么实现上面这些措施.
Hystrix 是什么?
Hystrix 是一个通过增加延迟容错和容错逻辑来控制分布式服务之间交互的一个库. Hystrix 通过线程隔离, 防止错误级联传递, 导致服务雪崩, 从而提高服务稳定性.
Hystrix 的主要目标
1, 通过隔离第三方客户端库访问依赖关系, 防止和控制延迟和故障;
2, 防止复杂分布式系统的级联失败;
3, 快速响应失败并迅速恢复;
4, 提供回滚以及友好降级;
5, 实现近实时监控, 告警和操作控制
Hystrix 设计原则
1, 防止单个依赖耗尽了服务容器的用户线程
2, 脱落负载以及快速失败, 而不是排队
3, 当可以阻止服务的失败时提供回退策略
4, 使用隔离技术减少任意依赖的影响
5, 通过近实时指标, 监控和告警优化发现时间
6, 在 Hystrix 的大多数方面, 通过配置更改的低延迟和对动态属性更改的支持, 使得可以在低延迟的情况下进行实时修改操作, 从而优化恢复时间
7, 防止整个依赖关系客户端执行中的故障, 而不仅仅是网络流量
Hystrix 如何做到上面的目标
1, 所有外部的调用都封装到 HystrixCommand 或 HystrixObservableCommand 对象, 这些对象通常在单独的线程下执行.
2, 超时调用的时间, 超过定义的阈值. 有一个默认值, 但是对于大多数的依赖, 你可以自定义该属性使得略高于每个依赖测量的 99.5% 的性能.
3, 为每一个依赖项维护一个线程池 (或者信号), 如果依赖项的线程池满了, 新的依赖请求不会继续排队等待, 而是马上被拒绝访问.
4, 计算成功, 失败, 超时和线程拒绝的数量.
5, 如果依赖服务的失败百分比超过阈值, 则手动或自动启动断路器, 在一段时间内停止对指定服务的所有请求.
6, 为请求失败, 被拒绝, 超时或短路情况提供回退逻辑.
7, 近乎实时地监控指标和配置更改.
一段代码 demo
讲完这么多, 还是看看代码更实在, 从 Hystrix 官网上截取了一段代码如下:
- public class Order {
- private final int orderId;
- private UserAccount user;
- public Order(int orderId) {
- this.orderId = orderId;
- user = new GetUserAccountCommand(new HttpCookie("mockKey", "mockValueFromHttpRequest")).execute();
- }
- }
更多代码内容:
上面就是 Hystrix 使用的实例, 在实际代码中, 就是 new 一个 Command, 然后调用 execute 方法获取结果, 那么这一个过程中 Hystrix 做了什么呢?
Hystrix 的工作流程
上面这个图是从 Hystrix 的官方文档中找到的, 能看懂这个文档几乎就能看懂 Hystrix 是怎么执行的了. 通过图中的顺序来解读 Hystrix 的执行流程.
1, 初始化, 有两种方式初始化一个 Hystrix 命令, 通过 new HystrixCommand 或者 new HystrixObservableCommand 创建, 使用服务实例和请求服务需要的参数来构造一个 Hystrix 命令.
2, 成功创建 Hystrix 后, 有四种方法执行实际的命令并得到返回结果. 这里 Hystrix 还使用了响应式编程来设计, 这个主题比较大, 一时半会解释不出, 之后再深入探索.
对于使用 HystrixCommand 创建命令的实例, 执行 execute 或者 queue; 而对于使用 HystrixObservableCommand 创建命令的实例, 执行 observe 或者 toObservable 方法, 可以请求服务然后得到执行结果. 这四个方法的特性是:
execute - 会阻塞, 然后返回以来服务的结果
queue - 返回一个 Future, 然后可以通过 get 方法获得以来服务的结果.
observe - 订阅包含依赖服务响应结果的订阅器, 当有结果时返回一个订阅器.
toObservable - 返回一个订阅器, 当订阅它时, 会知晓 Hystrix 命令并返回结果.
execute 的源码如下:
- public R execute() {
- try {
- return queue().get();
- } catch (Exception e) {
- throw Exceptions.sneakyThrow(decomposeException(e));
- }
- }
- public Future<R> queue() {
- /*
- * The Future returned by Observable.toBlocking().toFuture() does not implement the
- * interruption of the execution thread when the "mayInterrupt" flag of Future.cancel(boolean) is set to true;
- * thus, to comply with the contract of Future, we must wrap around it.
- */
- final Future<R> delegate = toObservable().toBlocking().toFuture();
- // 其他定义
- }
从源码看到, execute 方法会调用 queue().get() 方法, queue() 会调用 toObservable().toBlocking().toFuture(), 说明每一个 Hystrix 命令最终都回到 Observable 对象的实现, 即使是为了返回一个简单的值.
3, 判断 Hystrix 是否启用缓存且对应请求有缓存值, 则返回缓存的结果.
4, 如果 3 没有缓存, Hystrix 会检查它的熔断器, 如果此时熔断器开启了, 那么 Hystrix 不会执行命令, 直接返回降级结果.
5, 如果信号或者线程池拒绝请求, 返回降级结果.
6,Hystrix 通过调用 HystrixCommand.run() 或者 HystrixObservableCommand.construct() 方法来触发调用外部服务的操作, 如果超时或者失败, 返回降级结果. 如果 run 或者 construct 方法超过了命令定义的超时值, 线程会抛出 TimeoutException, 此时 Hystrix 捕捉到异常, 就会忽略 run 或 construct 方法的返回值, 进入 fallback.
注意: 没有任何方式可以阻止延迟的线程停止工作, 在 JVM 中, Hystrix 可以做到最好的就是抛出一个 InterruptedException, 如果 Hystrix 封装的服务没有捕获 InterruptedException,Hystrix 线程池中的线程会继续它的工作.
7, 不管请求如何进行: 成功, 失败, 超时, 熔断, Hystrix 都会上报健康状态到熔断器, 记录服务状态, 用于判断是否启动 / 半启动熔断器.
8,fallback, 进行降级操作, 会触发回退操作的条件: construct 或者 run 方法抛出异常 熔断器开启 线程池以及队列或者信号容量不足 Hystrix 命令超时
对于每一个 Hystrix 命令, 都需要覆盖 getFallback 方法, 在 fallback 函数中实现降级的方案, 如果需要在 fallback 中使用网络调用, 那么需要通过另一个 HystrixCommand 或者 HystrixObservableCommand. 在 HystrixCommand 中是实现 getFallback 方法, 在 HystrixObservableCommand 中, 是实现 sumeWithFallback 方法.
如果没有实现 fallback 方法, 或者 fallback 方法抛出了异常, Hystrix 还是会返回一个 Observerable, 但是不会返回内容并通过一个 onError 通知来马上终止. 通过 onError 通知, 发生异常的会被返回 Hystrix 的调用者. 尽量不要写出可能会抛出异常的 fallback 实现.
9, 如果一切正常, 那么 Hystrix 会发送成功的结果到 Observable, 程序再去获取.
总结
以上就是 Hystrix 的执行流程, 因为最近想了解在 PHP 中如何实现服务熔断, 于是在学习 Java 中做的比较好的 Hystrix 是怎么实现的. 接下来会继续深入学习 Hystrix 的熔断器实现, 下次再分享 Hystrix 熔断器的实现原理.
了解一个库的执行流程, 除了有助于开发时排查遇到的较棘手的问题, 还可以学习一个库的设计理念, 从这些库中吸收一些框架设计优点, 之后如果需要实现相关功能时, 就可以作为参考.
原创文章, 文笔有限, 才疏学浅, 文中若有不正之处, 万望告知.
如果本文对你有帮助, 请点个赞吧, 谢谢 ^_^
来源: http://www.tuicool.com/articles/JRRbUb7