微服务架构下请求调用失败的解决方案
文章收录在我的 GitHub 仓库,欢迎Star
Java-Interview-Tutorial
0 前言
相比单体架构,微服务架构下,服务调用从同一台机器内部的本地调用变成了不同机器间的远程方法调用,这就引入不确定因素:
调用的执行是在服务提供者一端,即使服务消费者本身正常,服务提供者也可能由于诸如 CPU、网络 I/O、磁盘、内存、网卡等硬件原因导致调用失败,还可能因本身程序执行问题如 GC 暂停导致调用失败
调用发生在两台机器间,所以要经过网络传输,而网络不可控:丢包、延迟及抖动都可能造成调用失败。
所以必须要针对服务调用失败进行特殊处理。
1 超时
微服务化后,一次用户调用可能会被拆分成多系统间的服务调用,任何一次服务调用若发生问题都可能导致用户请求最终是失败的。一个系统异常会影响所有依赖该系统所提供服务的服务消费者,可能导致服务雪崩。
所以针对服务调用,都要设置超时时间,避免依赖服务迟迟没有返回调用结果,把服务消费者拖死。但超时时间的设定也需考量:
太短,可能有些服务调用还没有来得及执行完,就被丢弃
太长,可能导致服务消费者被拖垮
因此,需根据正常情况下,服务提供者的服务水平来决定。按服务提供者线上真实服务水平,取 P999 或 P9999 值,即以 99.9%或者 99.99%的调用都在多少 ms 内返回为准。
2 重试
虽然设置超时时间可及时止损,但是服务调用结果毕竟还是失败,而大部分情况下,调用失败只是因为偶发的网络问题或个别服务提供者节点有问题,若能换个节点再访问说不定就能成功。
假如一次服务调用失败概率为 1%,则连续两次服务调用失败的概率 0.01%,失败率大大降低。
所以,实际服务调用时,一般还设置一个服务调用超时后的重试次数。若某服务调用的超时时间设置为 100ms,重试次数设置为 1,则当服务调用超过 100ms 后,服务消费者就会立即发起第二次服务调用,不会再等待第一次调用返回的结果。
3 双发
若一次调用不成功的概率 1%,则连续两次调用都失败概率 0.01%,可推得,一个简单的提高服务调用成功率的办法-双发,每次服务消费者要发起服务调用时,都同时发起两次服务调用:
可提高调用成功率
两次服务调用哪个先返回,就采用哪次的返回结果,平均响应时间也比一次调用更快
但这样,一次调用会给后端服务两倍压力,所消耗资源也加倍,所以一般“鲁莽”双发不可取。
更聪明的双发,“备份请求”(Backup Requests)。服务消费者发起一次服务调用后,在给定时间内,若没返回请求结果,则 Consumer 就立刻发起另一次服务调用。注意该设定时间通常比超时时间短得多,如超时时间取 P999,则备份请求时间可能取 P99 或 P90,因为若在 P99 或 P90 时间内调用还没返回结果,大概率可认为这次请求属于慢请求,再次发起调用理论上返回要更快。
实际线上服务运行时,P999 由于长尾效应,可能远大于 P99 和 P90。如一个服务的 P999=1s,而 P99=200ms、P90=50ms,这样,若备份请求时间取 P90,则第二次请求等待的时间只有 50ms。
不过注意,备份请求要设置一个最大重试比例,避免服务端异常时,大部分请求的响应时间都超过 P90,导致请求量翻倍,给服务提供者造成更大压力。经验之谈,最大重试比例可设置成 15%:
能尽量体现备份请求的优势
不会给服务提供者额外增加太大的压力
4 熔断
前面的手段在服务 Provider 偶发异常时很有效,但若 Provider 故障,短时间内都无法恢复,无论是超时重试还是双发:
无法提高服务调用成功率
由于重试,还给 Provider 带来更大压力,加剧故障
这时,就需要服务 Consumer 能探测到 Provider 故障了,并短时间内停止请求,给 Provider 故障恢复的时间,待 Provider 恢复后,再继续请求。就如一条电路,电流负载过高,保险丝就会熔断。
4.1 熔断原理
把客户端的每次服务调用,通过断路器封装,使用断路器来监控每一次服务调用。若某段时间内,服务调用失败次数达到一定阈值,则断路器就会被触发,后续的服务调用就直接返回,也就不会再向 Provider 发起请求。
熔断之后,一旦 Provider 恢复,服务调用如何恢复呢?这牵扯到熔断器的状态转换:
Closed 状态:正常情况下,断路器处关闭态,偶发的调用失败也不影响该状态的变更
Open 状态:当服务调用失败次数达到一定阈值,断路器处开启状态,后续服务调用直接返回,不会再向 Provider 发起请求
Half Open 状态:断路器开启后,每隔一段时间,会进入半打开状态,会向 Provider 发起探测性的调用,以确定 Provider 是否恢复。
若调用成功,断路器就关闭
若未成功,断路器继续保持开启状态,并等待下个周期重新进入半打开状态
断路器的最经典实现就是 Hystrix。Hystrix 就包含三种状态:关闭、打开、半打开。Hystrix 会把每次服务调用都用 HystrixCommand 封装,实时记录每次服务调用的状态,包括成功、失败、超时还是被线程拒绝。
当一段时间内服务调用失败率>阈值,断路器就会进入打开状态,新的服务调用会直接返回,不会向 Provider 发起调用。再等设定的时间间隔后,断路器又会进入半打开,新的服务调用又可重新发给 Provider;若一段时间内服务调用的失败率依然>阈值,断路器会重新打开,否则,断路器被关闭。
决定断路器是否打开的失败率阈值通过如下参数设定:
决定断路器何时进入半打开的时间间隔通过如下参数设定:
滑动窗口算法
统计指定时间段内服务调用失败率:
默认情况下,滑动窗口包含 10 个桶,每个桶时间宽度为 1s,每个桶内记录这 1s 内所有服务调用中成功的、失败的、超时的以及被线程拒绝的次数。
当新的 1s 到来时,滑动窗口就会往前滑动,丢弃掉最旧的 1 个桶,把最新 1 个桶包进来。
任意时刻,Hystrix 都会取滑动窗口内所有服务调用的失败率作为断路器开关状态的判断依据,这 10 个桶内记录:
滑动窗口内所有服务的调用失败率 =(失败的+超时的+被线程拒绝的调用次数)/总调用次数
5 总结
大部分服务调用都要设置超时时间及重试次数,但对非幂等的不可以重试,如大部分上行请求都是非幂等。
双发是在重试基础上的优化,减少超时等待的时间,对于长尾请求很有效。采用双发后,服务调用的 P999 能大幅减少,是提高服务调用成功率的有效手段。
熔断能很好地解决依赖服务故障引起的连锁反应,对于大规模服务调用的必不可少,尤其是对非关键路径的调用,即使调用失败也对最终结果影响不大的情况下,更应该引入熔断。
参考
https://martinfowler.com/bliki/CircuitBreaker.html
https://github.com/Netflix/Hystrix/wiki/How-To-Use
版权声明: 本文为 InfoQ 作者【JavaEdge】的原创文章。
原文链接:【http://xie.infoq.cn/article/c15084aa73202c5fc2d01820b】。文章转载请联系作者。
评论