微服务 之 熔断模型
大纲:
- 云原生系统的弹性模式resiliency pattern
1.1 服务故障的雪崩效应
1.2 回应之前云原生 --弹性-- 疑问?
- 弹性模式: 作用在下游请求消息上
- 短期中断的响应码
- Polly经典策略
- Golang 断路器模式
hi,好久不见,之前意译并连载了《Microsoft Cloud-native toc.pdf》部分内容
- 什么是云原生
- 现代云原生设计理念
- .NET微服务
- 谈到云原生,绕不开容器化
- 支撑性服务 & 自动化能力
1. 云原生系统的弹性模式
结合最近的工作经验,本次继续聊一聊云原生的弹性请求策略 (resilience not scale),这也是回应《现代云原生设计理念》中
“在分布式体系结构中,当服务B不响应来自服务A的网络请求会发生什么?
或者,当服务C暂时不可用,其他调用C的服务被阻塞时该怎么办?”
由于网络原因或自身原因,B、C服务不能及时响应,服务A发起的请求将被阻塞(直到B、C响应),此时若大量请求涌入,服务A的线程资源将被消耗完毕,服务A的处理性能受到极大影响,进而影响下游依赖的external clients/backend srv。
故障会传播,造成连锁反应,对整个分布式结构造成灾难性后果,这就是服务故障的“雪崩效应”。
当B、C服务不可用,下游客户端/backend srv能做什么?
客观上请求不通,执行预定的弹性策略: 重试/断路?
2. 弹性模式: 作用在下游的请求消息上
弹性模式是系统面对故障仍然保持工作状态的能力,它不是为了避免故障,而是接受故障并尝试去面对它。
Polly
是一个全面的.NET弹性和瞬时错误处理库,允许开发者以流畅和线程安全的方式表达弹性策略。
策略 | 场景 | 行为 |
---|---|---|
Retry | 抖动/瞬时错误,短时间内自动恢复 | 在特定操作上配置重试行为 |
Circuit Breaker | 在短期内不大可能恢复 | 当故障超过阈值,在一段时间内快速失败 |
Timeout | 限制调用者等待响应的时间 | |
Bulkhead | 将操作限制在固定的资源池,防止故障传播 | |
Cache | 自动存储响应 | |
Bulkhead | 一旦失败,定义结构化的行为 |
一般将弹性策略作用到各种请求消息上(外部客户端请求或后端服务请求)。
其目的是补偿暂时不可用的服务请求。
本次我们主要聊一聊 云原生弹性模型中的断路器,俗称熔断:Circuit Breaker
断路器模式有三种状态:
- Close: 请求无故障,断路器关闭
- Open: 出现故障到达阈值,切换到[Open]状态, 快速失败。
- Half-Open:进入Open状态后,开启一个Timer, 到达Timer, 会进入Half-Open状态,会尝试发送1个请求。
成功-------> Closed模式
失败-------> Open 模式
----所以Halp-Open 是一个瞬间判断态----
之后又重置Timer, Timer timeout之后,又进入半开模式
为什么熔断会有重置的概念?
因为进入Open模式的断路器一直在骚动啊, 间隔Timer就会进去 Half-Open模式(这是一个瞬态),决策失败之后又进入Open模式(这里就有重置的概念)。
3. 短期中断的响应码
Http Status code | 原因 |
---|---|
404 | not found |
408 | request timeout |
429 | two many requests |
502 | bad gateway |
503 | service unavailable |
504 | gateway timeout |
正确规范的响应码能给开发者足够的信息,尽快确认故障。 |
执行故障策略时,也能有的放矢,比如只重试那些由失败引起的操作,对于403UnAuthorized不可重试。
4. Polly的经典策略
- retry: 对网络抖动/瞬时错误可以执行
retry
策略(预期故障可以很快恢复), - Circuit Breaker:为避免无效重试导致的故障传播,在特定时间内如果失败次数到达阈值,断路器打开(在一定时间内快速失败); 同时启动一个timer,断路器进入半开模式(发出少量请求,请求成功则认为故障已经修复,错误计数器重置。)
install-package Microsoft.Extensions.Http.Polly
services.AddHttpClient("small") //降级 .AddPolicyHandler(Policy<HttpResponseMessage>.HandleInner<Exception>().FallbackAsync(new HttpResponseMessage(),async b => { // 1、降级打印异常 Console.WriteLine($"服务开始降级,上游异常消息:{b.Exception.Message}"); // 2、降级后的数据 b.Result.Content= new StringContent("请求太多,请稍后重试", Encoding.UTF8, "text/html"); b.Result.StatusCode = HttpStatusCode.TooManyRequests; await Task.CompletedTask; })) //熔断 .AddPolicyHandler(Policy<HttpResponseMessage>.Handle<Exception>() .CircuitBreakerAsync( 3, // 打开断路器之前失败的次数 TimeSpan.FromSeconds(20), // 熔断器从open--->half-open的Timer Timeout时间 (ex, ts) => //熔断器进入open模式的动作 { Console.WriteLine($"服务断路器开启,异常消息:{ex.Exception.Message}"); Console.WriteLine($"服务断路器开启的时间:{ts.TotalSeconds}s"); }, () => { Console.WriteLine($"服务断路器重置"); }, // 熔断器进入closed模式的动作 () => { Console.WriteLine($"服务断路器半开启(一会开,一会关)"); } //断路器进入Half-open模式的动作 ) ) //重试 .AddPolicyHandler(Policy<HttpResponseMessage>.Handle<Exception>().RetryAsync(3)) // 超时 .AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(2)));
当一个应用存在多个Http调用,按照上面的经典写法,代码中会混杂大量重复、与业务无关的口水代码,
思考如何优雅的对批量HttpClient做弹性策略。
这里提供两个实践:
① 博客园驰名博主edisonchou: 使用AOP框架,动态织入Polly
② CSDN某佚名大牛,使用反射+配置 实现的PollyHttpClientServiceCollectionExtension
扩展类, 支持在配置文件指定HttpClientName
5. Golang的Circuit Breaker pkg
go get github.com/sony/gobreaker
调用func NewCircuitBreaker(st Settings) *CircuitBreaker
实例化断路器对象, 参数如下:
type Settings struct { Name string MaxRequests uint32 #半开状态允许的最大请求数量,默认为0,允许1个请求 Interval time.Duration Timeout time.Duration # 断路器进入半开状态的间隔,默认60s ReadyToTrip func(counts Counts) bool # 切换状态的逻辑 OnStateChange func(name string, from State, to State) }
下面这个示例演示了: 请求谷歌网站,失败比例达到60%,就切换到"打开"状态,同时开启60sTimer,到60s进入“半开”状态(允许发起一个请求),如果成功, 断路器进入"关闭"状态;失败则重新进入“打开”状态,并重置60sTimer
package main import ( "fmt" "io/ioutil" "log" "net/http" "github.com/sony/gobreaker" ) var cb *gobreaker.CircuitBreaker func init() { var st gobreaker.Settings st.Name = "HTTP GET" st.ReadyToTrip = func(counts gobreaker.Counts) bool { failureRatio := float64(counts.TotalFailures) / float64(counts.Requests) return counts.Requests >= 3 && failureRatio >= 0.6 } cb = gobreaker.NewCircuitBreaker(st) } // Get wraps http.Get in CircuitBreaker. func Get(url string) ([]byte, error) { body, err := cb.Execute(func() (interface{}, error) { resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } return body, nil }) if err != nil { return nil, err } return body.([]byte), nil } func main() { body, err := Get("http://www.google.com/robots.txt") if err != nil { log.Fatal(err) } fmt.Println(string(body)) }
总结
① 本文记录了云原生系统的弹性模式:通过工程策略直面失败,补偿暂时不可用的请求、避免故障传播, 这对于实现微服务高可用、弹性容错相当重要。
!!!!!!
② 云原生弹性模式一般有重试、熔断、超时、资源限制等几种模式,本文大篇幅讲解了 [熔断]模型,
③ 熔断模型一般有三种状态: 关闭---> 开启---> 半开,进入开启状态,请求会快速失败;
半开状态是瞬态,熔断模型一直在骚动,因为熔断器会保持开启,直到Timer到达,重置进入半开状态。
注意,Circuit Breaker与Retry的意图不同,Retry模式的目的是让应用程序重试一个可能成功的操作;
Circuit Breaker 防止程序应用程序执行可能失败的操作。
一般情况下,应用程序会结合这两种模式:断路器包装 重试操作。
- https://blog.csdn.net/weixin_44588495/article/details/106361934
- https://blog.csdn.net/qq_26900081/article/details/108071374
- https://www.cnblogs.com/edisonchou/p/9159644.html
- https://docs.microsoft.com/en-us/dotnet/architecture/cloud-native/application-resiliency-patterns
- https://docs.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker
本文来自博客园,作者:{有态度的马甲},转载请注明原文链接:https://www.cnblogs.com/JulianHuang/p/15002798.html
欢迎关注我的原创技术、职场公众号, 加好友谈天说地,一起进化
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?