Micro In Action(七):熔断与限流

polaris · · 2747 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

![Micro In Action](https://s1.ax1x.com/2020/03/28/GkW63D.png) > 本文作者:Che Dan > > 原文链接:https://medium.com/@dche423/micro-in-action-7-cn-ce75d5847ef4 本文是[Micro](https://micro.mu/)系列文章的第七篇。前面文章覆盖了创建与调用服务的基本流程。 接下来我们将转到一些高级特性。 今天来谈谈**熔断与限流**。 熔断与限流一直大型系统架构的重要话题。 当我们开始把系统拆分成由很多微服务组成分布式系统时, 这些话题变得比以往更加重要。没有熔断与限流, 系统很容易因为单个组件的故障(这在分布式系统中不可避免)而形成“雪崩”效应, 进而导致整个系统的瘫痪。 在Micro的可插拔架构之下,可以非常容易地引入以上机制。我们知道Micro支持用中间件实现请求的拦截与控制。而这些中间件中最关键的两类是: 1. micro.WrapClient ,用于包装对外发出的请求,即客户端包装 2. micro.WrapHandler ,用于包装外界发来的请求,即服务端包装 这两类中间件刚好分别适用于熔断和限流两个场景。下面我们以实例来说明如何利用这类两中间件来提高系统的鲁棒性。 ------ ## 熔断 你可能已经猜到了, 如此常见的工具一般情况下不需要我们自己开发。 社区中已经有一些非常优秀的开源熔断组件,例如 [hystrix-go](https://github.com/afex/hystrix-go/),[gobreaker](https://github.com/sony/gobreaker) 。 不仅如此, Micro 也提供了对上述组件进行简单封装的插件, 如 [hystrix 插件](https://github.com/micro/go-plugins/tree/master/wrapper/breaker/hystrix), [gobreaker 插件](https://github.com/micro/go-plugins/tree/master/wrapper/breaker/gobreaker)。 有了这些插件的帮助, 在Micro中实现熔断就变得非常简单了。 以hystrix为例: ```go import ( ... "github.com/micro/go-plugins/wrapper/breaker/hystrix" ... ) func main(){ ... // New Service service := micro.NewService( micro.Name("com.foo.breaker.example"), micro.WrapClient(hystrix.NewClientWrapper()), ) // Initialise service service.Init() ... } ``` 只需要在创建`service`实例的时候指定hystrix插件,系统便具备了自动熔断能力。 所有从此节点发出的Micro服务调用都会受到熔断插件的限制和保护。 当请求超时或并发数超限,调用方会立即接收到熔断错误。那么默认的并发限制和时间限制是多少呢?答案在hystrix-go 的源码中。查看**github.com/afex/hystrix-go/hystrix/settings.go** 将看到几个包级变量: ```go ... // DefaultTimeout is how long to wait for command to complete, in milliseconds DefaultTimeout = 1000 // DefaultMaxConcurrent is how many commands of the same type can run at the same time DefaultMaxConcurrent = 10 ... ``` 可见默认的超时时间是1000毫秒, 默认最大并发数是10。 **注:**可调整的熔断设置不只这两项, 但对他们每一项的详细讨论超过了本文范围, 因此不作更多说明。 如果有兴趣,可以到[hystrix-go官网](https://github.com/afex/hystrix-go/)查看详细文档。 如果默认设置不能满足我们的要求, 则可以通过如下方式修改: ```go import ( ... hystrixGo "github.com/afex/hystrix-go/hystrix" "github.com/micro/go-plugins/wrapper/breaker/hystrix" ... ) func main(){ ... // New Service service := micro.NewService( micro.Name("com.foo.breaker.example"), micro.WrapClient(hystrix.NewClientWrapper()), ) // Initialise service service.Init() hystrix.DefaultMaxConcurrent = 3//change concurrrent to 3 hystrix.DefaultTimeout = 200 //change timeout to 200 milliseconds... } ``` 如代码所示,默认限制被调整成了3个并发及200毫秒。 你可能会对 **DefaultMaxConcurrent** 有疑问: 这个并发限制的作用域是什么呢? 假设我们需要同时调用3个服务,每个服务有3个不同方法。 那么是不是说我们必须把**DefaultMaxConcurrent**设置为成大于3*3的数字才可以彻底并发呢? **要回答这个问题, 需要搞清楚两点:** 首先, **DefaultMaxConcurrent** 的限制的目标是什么。从hystrix 文档可以看到, 它的目标是hystrix 中的 **command:** > DefaultMaxConcurrent is how many commands of the same type can run at the same time 接下来就需要看看hystrix插件如何处理不同方法与**command**的关系。查看 **github.com/micro/go-plugins/wrapper/breaker/hystrix/hystrix.go** 相关代码如下: ```go import( "github.com/afex/hystrix-go/hystrix" ... ) ...func (c *clientWrapper) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error { return hystrix.Do(req.Service()+"."+req.Endpoint(), func() error { return c.Client.Call(ctx, req, rsp, opts...) }, nil) } ... ``` 可以看到每一个方法调用都被 `hystrix.Do` 包装起来。 注意其中的`req.Service()+”.”+req.Endpoint()`,它就是hystrix的**command。**由于这个字符串中没有包含节点信息, 这意味着对于同一个服务,部署单个还是多个节点对于熔断来说是没有区别的,**所有节点都共享一组限制**。 至此便可以明确: 每一个服务的每一个方法会独立进行并发限制计数**,**彼此并不影响。**DefaultMaxConcurrent 的作用域是方法级的,与节点数无关。**因此, 当某个服务的节点数被扩容一倍, 那么也有必要修改相应hystrix 限制,否则此扩容可能无法发挥全部效果。 实践中, 不同方法可能需要不同的熔断指标。如果我们想要更细粒度地进行控制,应该怎么办呢?从上面的源码分析可以看出, 服务方法被映射成了 hystrix **command,**而hystrix是支持对不同**command**分别设限的, 具体作法如下: ```go ... hystrix.ConfigureCommand("com.serviceA.methodFoo", hystrix.CommandConfig{ MaxConcurrentRequests: 50, Timeout: 10, }) hystrix.ConfigureCommand("com.serviceB.methodBar", hystrix.CommandConfig{ Timeout: 60, }) ... ``` 通过上述代码,我们为两个方法设制了不同的限制。 如果某一项没有特别说明, 便会使用系统默认值。 **小结:** 熔断功能作用于客户端,设置恰当阈值以后, 它可以保障客户端资源不会被耗尽 —— 哪怕是它所依赖的服务处于不健康的状态,也会快速返回错误,而不是让调用方长时间等待。 ## 限流 与熔断类似, 限流也是分布式系统中常用的功能。 不同的是, 限流在服务端生效,它的作用是保护服务器: 在请求处理速度达到设定的限制以后, 便不再接收和处理更多新请求,直到原有请求处理完成, 腾出空闲。 避免服务器因为客户端的疯狂调用而整体垮掉。 打个比方, 假设我们运营一间能容纳10位客人的餐馆。如果同时来了100位客人想要用餐,最好的处理方式是选取前10位进行服务,同时告诉另外90位客人:我们目前无法服务,请改天再来。虽然这90位客人会不开心, 但我们至少保证了前10位可以开心地用餐。 若没有限流措施, 结果是100位客人全进入餐厅, 厨房忙不过来, 客人无处落座。 任何人都得不到服务,又不甘心离开。 最终的结果是整个餐厅人满为患并最终瘫痪。 在Micro中使用限流功能非常简单,只需增加一行代码就可以实现基于QPS的限流。目前有两个的现成的限流插件可供使用, 本文以 [uber rate limiter 插件](https://github.com/micro/go-plugins/tree/master/wrapper/ratelimiter/uber)为例进行说明(当然如果现有插件不满足需求, 完全可以自行开发更适用的插件)。修改**hello-srv/main.go**: ```go package main import ( ... limiter "github.com/micro/go-plugins/wrapper/ratelimiter/uber" ... ) func main() { const QPS = 100 // New Service service := micro.NewService( micro.Name("com.foo.srv.hello"), micro.Version("latest"), micro.WrapHandler(limiter.NewHandlerWrapper(QPS)), ) ...} ``` 以上代码便为**hello-srv**增加了服务器限流能力, QPS上限为100。这个限制由此服务的所有handler所有method 共享。换句话说,此限制的作用域是服务级别的。 ------ ## 总结 熔断和限流很重要。 得益于Micro的可插拨架构, 我们可以非常方便地应用这两个重要的功能。 熔断的出发点是保护客户端, 不被外部服务的问题所拖累, 永远快速响应(哪怕得到一个错误,也好于长时间等待)。 永远避免资源的过度消耗。 限流的出发点是保护服务器。 只处理自己能力之内的流量,实现过载保护。 当流量超过设定限制时立即返回错误。 二者结合体现了一个哲学:**永远首先为自己负责, 无论外部依赖状态如何, 一个服务应保证自身的稳健**。 当这种哲学应用到分布系统的每一个组件以后, 这个系统将变得非常健壮,不会被突发流量高峰击垮。 因此, 我们在分布式系统开发中应该遵循这样一个最佳实践:**为每一个服务增加熔断和限流能力**。 起步时可以是粗粒度的限制, 随着业务的演进, 可以逐步细化控制, 为每一个(每一类)服务设置与其业务相匹配的限制策略。

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2747 次点击  
加入收藏 微博
被以下专栏收入,发现更多相似内容
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传