在微服务架构中,服务之间相互调用,形成了复杂的调用链。一个服务的故障,可能会通过调用链蔓延,导致整个系统雪崩。
熔断和降级,是防止服务雪崩、提高系统可用性的重要手段。我做微服务架构已经有几年了,在熔断降级方面踩过很多坑,也积累了一些经验。
今天,我想分享一下我总结的服务熔断降级最佳实践,包括熔断和降级的基本概念、常见的实现方式、关键参数的配置、踩过的坑、以及一些实用的建议。希望这些经验,能帮助正在做微服务架构的朋友,少走一些弯路,构建更稳定、更可靠的微服务系统。
一、为什么需要熔断和降级
在讲最佳实践之前,先说说为什么需要熔断和降级。
在微服务架构中,一个用户请求,可能需要调用多个服务才能完成。比如,一个电商下单请求,可能需要调用用户服务、商品服务、库存服务、订单服务、支付服务等。这些服务之间,形成了一条复杂的调用链。
在这条调用链中,如果某个服务出了问题(比如响应变慢、或者直接挂了),会发生什么呢?
- 请求堆积:调用这个服务的请求,会因为等待响应而阻塞,请求会越积越多。
- 资源耗尽:阻塞的请求会占用线程、连接、内存等资源,时间长了,这些资源会被耗尽。
- 服务雪崩:资源耗尽之后,调用这个服务的服务也会变得不可用,然后它的调用者也会变得不可用……故障会沿着调用链蔓延,最终导致整个系统崩溃。这就是所谓的"服务雪崩"。
服务雪崩,是微服务架构中最可怕的故障之一。一旦发生雪崩,整个系统都会瘫痪,而且恢复起来很困难。
熔断和降级,就是为了防止服务雪崩,提高系统的可用性。
熔断(Circuit Breaker),借鉴了电路中的保险丝的概念。当一个服务的故障达到一定的阈值(比如错误率超过50%,或者响应时间超过某个阈值),熔断器就会"跳闸",后续的请求不再调用这个服务,而是直接返回失败或者走降级逻辑。这样,就不会有大量的请求阻塞在这个故障服务上,避免了资源耗尽和服务雪崩。
熔断之后,过一段时间,熔断器会进入"半开"状态,允许少量请求通过,测试服务是否恢复。如果这些请求成功了,熔断器就会"关闭",恢复正常调用;如果还是失败,熔断器继续"打开",继续熔断。
降级(Degradation),是指当系统压力过大,或者某个服务不可用的时候,主动关闭一些非核心功能,或者返回一些默认值/兜底数据,保证核心功能的可用性。
比如,电商网站在大促的时候,压力很大,可以暂时关闭商品评价、推荐等非核心功能,保证下单、支付等核心功能的正常运行。再比如,商品服务不可用的时候,可以返回缓存的商品数据,或者返回一个默认的商品信息,保证用户能看到页面,而不是直接报错。
熔断和降级,通常是配合使用的。熔断之后,通常会走降级逻辑,返回兜底数据,保证用户体验。
二、常见的实现方式
了解了为什么需要熔断和降级之后,我们来看看常见的实现方式。
1. Netflix Hystrix
Hystrix,是Netflix开源的一个熔断降级库,也是最经典、最常用的熔断降级实现。Hystrix提供了完整的熔断、降级、隔离、监控等功能,在Java微服务领域应用非常广泛。
Hystrix的核心概念:
- HystrixCommand:封装一个远程调用,支持同步、异步、响应式调用。
- 熔断机制:基于错误率、响应时间等指标,自动熔断。
- 降级机制:熔断或者调用失败的时候,执行fallback逻辑,返回兜底数据。
- 线程池/信号量隔离:通过线程池或者信号量,隔离不同的依赖调用,防止一个依赖的故障耗尽所有资源。
- 实时监控:提供实时的监控指标和Dashboard,方便查看熔断状态、错误率、响应时间等。
Hystrix虽然已经进入维护模式(Netflix宣布不再积极开发,推荐使用Resilience4j等替代方案),但是它依然是最成熟、应用最广泛的熔断降级库,很多公司还在使用。
2. Resilience4j
Resilience4j,是一个比较新的熔断降级库,设计上比Hystrix更轻量、更灵活,支持Java 8+的函数式编程,是Hystrix的推荐替代方案之一。
Resilience4j提供了以下核心模块:
- CircuitBreaker:熔断器。
- RateLimiter:限流器。
- Retry:重试机制。
- Bulkhead:舱壁隔离。
- TimeLimiter:超时控制。
- Cache:缓存。
Resilience4j的设计更模块化,你可以只使用你需要的模块,而且它的API更函数式,使用起来更灵活。
3. Sentinel
Sentinel,是阿里巴巴开源的一个熔断降级和流量控制库,在国内应用很广泛,特别是在Spring Cloud Alibaba生态中。
Sentinel的特点:
- 流量控制:支持多种流量控制策略(QPS、线程数、关联流量、链路流量等)。
- 熔断降级:支持基于响应时间、错误率、错误数的熔断。
- 系统保护:支持基于系统负载、CPU使用率、入口QPS等的系统级保护。
- 实时监控:提供实时的监控Dashboard。
- 规则动态配置:支持动态配置规则,不需要重启服务。
Sentinel的功能很全面,特别是在流量控制方面,比Hystrix更强大,而且和Spring Cloud Alibaba集成得很好,国内的用户很多。
4. Service Mesh(Istio/Linkerd)
除了在代码层面实现熔断降级,还可以在Service Mesh层面实现。比如Istio,通过Sidecar代理,可以在不修改业务代码的情况下,实现熔断、超时、重试、流量控制等功能。
Service Mesh的好处是零侵入,多语言无关,统一的控制平面。但是,它的配置和运维比较复杂,而且目前还不够成熟,适合有一定技术能力的团队。
5. 网关层实现
还可以在API网关层实现熔断降级。比如,Spring Cloud Gateway、Kong、Nginx等网关,都支持一定的熔断、限流、降级功能。
网关层的熔断降级,适合对入口流量的控制,但是对于服务之间的内部调用,还是需要在服务层面或者Service Mesh层面实现。
三、关键参数的配置
熔断降级的效果,很大程度上取决于参数的配置。参数配置得好,能有效防止服务雪崩;配置得不好,可能会导致误熔断(服务正常的时候熔断了),或者该熔断的时候不熔断(服务故障了还在调用)。
下面,我总结一些关键参数的配置经验:
1. 超时时间(Timeout)
超时时间,是最基础、最重要的参数。调用远程服务的时候,一定要设置超时时间,不能无限等待。
超时时间怎么设置?这要根据具体的服务来定。一般来说,超时时间应该设置为服务正常响应时间的2-3倍。比如,一个服务正常响应时间是100ms,那么超时时间可以设置为200-300ms。
超时时间不能设置得太长,也不能太短。太长了,故障服务会占用资源太久,容易导致雪崩;太短了,正常的慢请求会被误判为超时,导致不必要的失败。
而且,不同的接口,超时时间应该不一样。核心接口、查询接口,可以设置短一点的超时;非核心接口、写入接口,可以设置长一点的超时。
2. 熔断阈值
熔断阈值,是指什么时候触发熔断。常见的熔断阈值有:
- 错误率:比如,错误率超过50%,触发熔断。
- 错误数:比如,10秒内错误数超过20个,触发熔断。
- 响应时间:比如,平均响应时间超过500ms,触发熔断。
熔断阈值怎么设置?这要根据服务的重要性和正常的错误率来定。
- 对于核心服务,正常错误率很低(比如1%以下),可以设置比较低的熔断阈值(比如错误率超过30%就熔断),这样能更快地发现故障,防止雪崩。
- 对于非核心服务,正常错误率可能高一些,可以设置高一点的熔断阈值(比如错误率超过70%才熔断),避免误熔断。
而且,熔断阈值要结合时间窗口来设置。比如,10秒内的错误率超过50%,才触发熔断。这样可以避免因为瞬时的错误波动而误熔断。
3. 熔断时间
熔断时间,是指熔断之后,持续多久再进入半开状态。
熔断时间不能太短,也不能太长。太短了,服务还没恢复,就进入半开状态,可能会再次失败,反复熔断;太长了,服务已经恢复了,还在熔断,影响可用性。
一般来说,熔断时间可以设置为5-30秒,根据服务的恢复时间来定。如果服务恢复比较快,可以设置短一点;如果服务恢复比较慢,可以设置长一点。
4. 半开状态的请求数
半开状态,是指熔断一段时间后,允许少量请求通过,测试服务是否恢复。半开状态的请求数,不能太多,也不能太少。
太多了,如果服务还没恢复,这些请求都会失败,可能会再次导致资源耗尽;太少了,测试结果不准确,可能因为一两个请求的成功就关闭了熔断,但是服务实际上还没完全恢复。
一般来说,半开状态的请求数,可以设置为5-10个,根据服务的QPS来定。QPS高的服务,可以设置多一点;QPS低的服务,可以设置少一点。
5. 线程池/信号量大小
如果用线程池隔离,每个依赖调用的线程池大小,要设置合理。
线程池太小,正常的流量也会被拒绝;线程池太大,起不到隔离的作用,一个依赖的故障还是可能耗尽所有线程。
一般来说,线程池的大小,可以根据依赖调用的QPS和平均响应时间来计算。比如,一个依赖的QPS是100,平均响应时间是100ms,那么需要的线程数大约是100 * 0.1 = 10个。可以在这个基础上,留一些余量,设置为15-20个。
如果用信号量隔离,信号量的大小也可以参考这个计算方式。
6. 重试次数和间隔
重试,是提高可用性的一种手段,但是也要谨慎使用。重试次数不能太多,重试间隔不能太短,否则可能会加重故障服务的负担,甚至导致雪崩。
一般来说,重试次数设置为1-2次就够了,重试间隔可以设置为100-500ms,而且最好用指数退避(每次重试的间隔翻倍)。
而且,不是所有的请求都适合重试。只有幂等的请求(比如查询、幂等的写入)才适合重试,非幂等的请求(比如创建订单、支付)不能重试,否则可能会导致重复创建、重复支付等问题。
四、踩过的那些坑
在熔断降级方面,我踩过很多坑。下面分享几个让我印象最深刻的坑:
坑1:没有设置超时时间,导致服务雪崩
这是我踩过的最严重的坑。最开始做微服务的时候,经验不足,调用远程服务的时候没有设置超时时间,用的是框架的默认超时时间(很长,有的甚至没有超时)。
结果,有一次,一个下游服务出了问题,响应变得很慢,几十秒都不返回。我们的服务调用这个下游服务,请求都阻塞在那里,线程池很快就被占满了,然后我们的服务也变得不可用,然后上游服务也不可用……最终导致了整个系统的雪崩。
那次故障,影响了很长时间,恢复起来也很困难。从那以后,我就记住了:调用远程服务,一定要设置超时时间,而且要合理设置。
坑2:降级逻辑写得太复杂,导致降级也失败
有一次,我们给一个服务加了熔断降级,降级逻辑是调用另一个服务,获取缓存的数据。结果,有一次,主服务故障了,触发了熔断,走降级逻辑,但是降级逻辑调用的那个服务也出问题了,结果降级也失败了,用户还是看到了错误页面。
那次故障让我明白:降级逻辑一定要简单、可靠,最好是返回静态数据、缓存数据、或者默认值,不要在降级逻辑里再调用远程服务,否则降级也可能失败,起不到兜底的作用。
降级逻辑的原则是:越简单越好,越可靠越好。最好是纯内存操作,或者读取本地缓存,不要有外部依赖。
坑3:熔断阈值设置不合理,导致误熔断
有一次,我们给一个服务设置了熔断,错误率超过30%就熔断。结果,有一次,因为一个第三方依赖的短暂波动,这个服务的错误率短暂地升到了40%,触发了熔断。但是,第三方依赖很快就恢复了,服务实际上已经正常了,但是熔断还在持续,导致用户在一段时间内看到的都是降级页面,影响了用户体验。
那次故障让我明白:熔断阈值不能设置得太低,而且要结合时间窗口,避免因为瞬时的波动而误熔断。另外,熔断时间也不能太长,要让服务恢复后能尽快恢复正常调用。
坑4:没有监控,熔断了都不知道
有一次,我们的一个服务已经熔断了很久,但是我们都不知道,因为没有监控和告警。直到用户反馈说某个功能用不了,我们才去排查,发现那个服务已经熔断了几个小时了。
那次故障让我明白:熔断降级一定要有监控和告警。要监控每个服务的熔断状态、错误率、响应时间、请求量等指标,当熔断发生的时候,要及时告警,让开发和运维人员能及时知道,及时排查问题。
没有监控的熔断降级,就像盲人摸象,出了问题都不知道,更谈不上及时处理。
坑5:线程池隔离,线程数设置不合理
有一次,我们用Hystrix的线程池隔离,给一个依赖调用设置了10个线程。结果,大促的时候,这个依赖的QPS很高,10个线程不够用,很多请求被拒绝了,导致用户看到错误页面。实际上,这个依赖服务是正常的,只是因为我们的线程池设置太小,导致请求被拒绝了。
那次故障让我明白:线程池的大小,要根据服务的QPS和响应时间合理设置,不能拍脑袋设置。而且,要监控线程池的使用率,当使用率持续很高的时候,要及时调整线程池大小。
坑6:重试风暴,加重了故障
有一次,我们给一个服务加了重试,失败了自动重试2次。结果,有一次,下游服务出了问题,我们的服务调用失败,然后自动重试,重试又失败,再重试……本来下游服务已经很吃力了,我们的重试又给它增加了2倍的流量,结果下游服务直接被打挂了,故障更严重了。
那次故障让我明白:重试要谨慎使用,特别是在下游服务已经故障的时候,重试可能会加重故障,形成"重试风暴"。重试次数不能太多,而且最好配合熔断使用,当下游服务故障触发熔断后,就不要再重试了。
五、最佳实践总结
踩了这么多坑,我总结了一些熔断降级的最佳实践:
1. 超时是基础,一定要设置
调用远程服务,一定要设置超时时间,而且要合理设置。不同的接口,设置不同的超时时间。这是防止服务雪崩的基础,也是最重要的一步。
2. 熔断和降级配合使用
熔断之后,一定要有降级逻辑,返回兜底数据,保证用户体验。不要熔断之后直接返回错误,那样用户体验很差。
降级逻辑要简单、可靠,最好是返回静态数据、缓存数据、或者默认值,不要在降级逻辑里再调用远程服务。
3. 合理配置参数,不要拍脑袋
超时时间、熔断阈值、熔断时间、线程池大小、重试次数等参数,都要根据服务的实际情况合理配置,不要拍脑袋设置。
配置参数的时候,要参考服务的QPS、响应时间、错误率、重要性等指标。配置好之后,要通过监控验证参数是否合理,根据实际情况调整。
4. 隔离很重要,防止故障蔓延
不同的依赖调用,要做隔离(线程池隔离或者信号量隔离),防止一个依赖的故障耗尽所有资源,影响其他依赖的调用。
而且,核心功能和非核心功能,也要做隔离,非核心功能的故障,不能影响核心功能的可用性。
5. 监控和告警必不可少
熔断降级,一定要有监控和告警。要监控每个服务的熔断状态、错误率、响应时间、请求量、线程池使用率等指标。当熔断发生、错误率升高、响应时间变长的时候,要及时告警,让相关人员能及时知道,及时排查问题。
没有监控的熔断降级,等于盲人摸象,出了问题都不知道。
6. 重试要谨慎,配合熔断使用
重试能提高可用性,但是也要谨慎使用。重试次数不能太多(1-2次足够),重试间隔不能太短,最好用指数退避。而且,只有幂等的请求才适合重试。
重试一定要配合熔断使用,当下游服务故障触发熔断后,就不要再重试了,避免加重故障,形成重试风暴。
7. 核心功能优先,非核心功能可以牺牲
在系统压力大或者故障的时候,要优先保证核心功能的可用性,非核心功能可以降级或者关闭。
比如,电商网站,下单、支付是核心功能,商品评价、推荐、排行榜是非核心功能。在大促或者故障的时候,可以关闭非核心功能,保证核心功能的正常运行。
8. 定期演练,验证熔断降级的有效性
熔断降级配置好了之后,要定期演练,验证它是否真的有效。比如,故意让一个服务故障,看看熔断是否能正常触发,降级是否能正常返回,系统是否能保持可用。
很多时候,熔断降级配置好了,但是从来没有验证过,真的出故障的时候,才发现配置有问题,或者降级逻辑有bug,起不到作用。定期演练,能提前发现问题,确保真的出故障的时候,熔断降级能正常工作。
9. 不要过度设计,根据实际需求选择
熔断降级的方案很多(Hystrix、Resilience4j、Sentinel、Service Mesh等),不要盲目追求新技术,也不要过度设计。要根据自己的实际需求、技术栈、团队能力,选择最适合的方案。
如果是Java微服务,用Spring Cloud,那么Hystrix或者Sentinel都是不错的选择;如果追求轻量和灵活,可以考虑Resilience4j;如果是多语言微服务,有足够的运维能力,可以考虑Service Mesh。
10. 熔断降级不是万能的,还要配合其他高可用手段
熔断降级,是提高系统可用性的重要手段,但不是唯一的手段。还要配合其他高可用手段,比如:
- 服务冗余:服务多实例部署,避免单点故障。
- 负载均衡:请求均匀分布到多个实例,避免单个实例压力过大。
- 缓存:用缓存减轻数据库和下游服务的压力。
- 异步化:非核心流程异步化,提高响应速度和系统吞吐量。
- 限流:限制请求量,防止系统被打垮。
- 容量规划:合理规划系统容量,应对流量高峰。
熔断降级,只是高可用体系中的一环,要配合其他手段,才能构建真正高可用的系统。
六、写在最后
服务熔断降级,是微服务架构中非常重要的一环,也是防止服务雪崩、提高系统可用性的关键手段。
但是,熔断降级不是简单地引入一个库、加几个注解就完事了。它需要合理的参数配置、可靠的降级逻辑、完善的监控告警、定期的故障演练,才能真正发挥作用。
我做微服务架构这几年,在熔断降级方面踩了很多坑,也积累了一些经验。这些经验,可能不是最先进的,也不是最完美的,但是都是我从实际故障中总结出来的,希望能给正在做微服务架构的朋友一些参考。
最后,用一句话来总结这篇文章:"熔断降级,不是为了不出问题,而是为了出问题的时候,问题不会蔓延,系统还能保持核心功能可用。合理配置、可靠降级、完善监控、定期演练,才能构建真正高可用的微服务系统。"
愿每一个做微服务的朋友,都能构建出稳定、可靠、高可用的系统,少踩坑,少故障。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录