业界通常用多少个9来衡量系统的可用性,如99.99%表示一年中有1小时左右的不可用时间。任何一个服务的可用性都不会是100%,意味着在服务运行时间里还是有可能发生故障。当把功能集中且运行在同一个应用中的单体架构拆分成多个相互独立的微服务架构后,虽然可以降低一损俱损的全局性故障风险,但由于微服务之间存在大量的依赖关系, 随着微服务个数的增多,依赖关系也将会变得越来越复杂,而且每个微服务都有可能发生故障,如果不能做好相互依赖的隔离,避免故障的连锁反应,结果可能比单体更糟糕。假设有100个微服务,并且每个微服务只会发生1种故障,那么总共会有2100种不同的故障场景,而每个微服务自身可能不止1种故障。当某个微服务发生故障时,如何确保不会导致其他依赖的微服务不可用, 如何确保系统自动降级把发生故障的微服务排除出去,如何确保故障不会扩展到整个系统? 那么如何有效确保微服务架构的可用性将会成为挑战。
![75ac4a93a47a5ba0f4fea001278fbe487306f348](https://i-blog.csdnimg.cn/blog_migrate/f0d784e79791643a8da4864c3fe3beda.png)
在一开始每个依赖的Service都是正常的,现假设有一个Service异常了,这时可能会有三种情况:
1. 这个请求成功,假设因网络异常或宕机导致Service C某个节点不可用,但有高可用节点取代了这个失败节点,这时Service C不受影响,依然可用,如下图所示:
![4d87f2b4a4778f0136a098833399c5fbfeaf5bd5](https://i-blog.csdnimg.cn/blog_migrate/e19bfb7a30c7db16bec58339ecaa54aa.png)
2. 这个请求是成功的,假设是Service D故障,而这个Service不是关键性的,运行失败也可以继续进行,比如注册用户需要调用一个服务发送注册成功的邮件给用户,如果发邮件的这个Service不可用,但不会影响用户的注册,所以用户注册还是会成功,邮件可以等服务恢复后再发送,只有时间上的延迟。这时Service A不受影响,依然可用,如下图所示:
![fb7bfc44a31afee2a08a0baba79b3a828338cb40](https://i-blog.csdnimg.cn/blog_migrate/fc88c15ba5a63de8658445835a38781e.png)
3. 这个请求失败,比如异常的节点是Service E,而Service E是代码级逻辑异常,所有高可用节点都不可用,这时需要将Service E进行依赖隔离,否则ServiceA可能会受到ServiceE的影响而不可用。需要做一些措施确保Service A不会受影响,依然可用,如下图所示:
![9f092a88ab2ab3438dcf39fc8c4def923d5fe27d](https://i-blog.csdnimg.cn/blog_migrate/cef3d93049dbe38e1cc015f50c577f76.png)
可以从以下几个策略可以提高微服务架构的可用性:
1) 失效转移
提高服务的高可用性,最基本的原则就是消除单点,通过负载均衡技术构建集群,所有的集群节点都是无状态且完全对等的。如上面讲的第1种情况。当一个节点异常时,负载均衡服务器会把用户发送的访问请求发送到可用的节点上。对用户来说,某个节点异常是无感的,用户请求会透明的转移到了可用的节点上执行。
2) 异步调用
避免一个服务失败导致整个应用请求失败很重要的是使用异步调用。如上面讲的第2种情况。如果采用的是同步调用,当邮件服务异常时,会导致其他两个服务也无法执行,最终导致用户注册失败。如果采用异步调用,Service A把用户注册信息发送给消息队列后立即返回用户注册成功的响应,虽然邮件服务不能用,但是写数据库的服务,权限开通等服务都能正常执行。所以即使邮件不能发送成功,也不会影响其他服务的执行,用户注册可顺利完成。
3) 依赖隔离
用户请求发送给Service A,Service A分配线程资源通过网络远程调用其他的Service,假设调用Service E发生异常时,Service A中对Service E调用的线程就可能会响应慢或僵死,而线程是系统的资源,如果短时间内得不到释放,在高并发的情况下资源就会被耗尽,结果会导致Service A也不可用,虽然其他的服务依然可用。
![3a36239456eafb0218e603ab1d722d086a0e7ec8](https://i-blog.csdnimg.cn/blog_migrate/36605f7b3f1698786dc9df06c50fa728.png)
Service A的资源是有限的,比如Service A启动时分配了400个线程,当400个线程都因调用Service E时异常不能及时正常的释放,如线程死锁,响应时间慢,导致 400个线程全部僵死在调用Service E上,这里Service A就没有空闲的线程来接收新的用户请求,这时就会导致Service A挂起或僵死。所以避免Service A被依赖的服务拖垮就是要确保Service A的线程资源不会被调用的依赖服务耗尽,在 《Release It!》一书中总结了非常重要的两条方法: 设置超时和使用断路器。
设置超时
在应用中设置服务调用的超时时间后,一旦线程的执行时间超过了所设置的时间,就抛出异常信息,自动断开连接,这样服务的线程就不会都长时间僵死在调用异常的服务上,导致没有空闲线程接收新的用户请求,可以避免Service A因为调用Server E 异常而被拖垮,自身不可用。所以通过网络调用外部依赖服务时,都必须设置超时。
使用断路器
断路器大家都不陌生,家里电表在电流过载或者短路时就会跳闸,如果不跳闸,电路就不断开,电线就会升温,造成火灾。有了断路器之后,电流过载时就会自动跳闸断开电路,避免引起更大的灾难。在程序中也是如此,当知道服务调用某个依赖服务有大量超时的时候,再让新的请求去访问也只会超时,并不能得到想到的结果,还会消耗现有资源,增加负载,导致服务不可用。这个时候使用断路器就能避免这种资源浪费,在自身服务和依赖服务之间放一个断路器,通过断路器的监控实时统计访问的状态,当访问超时或者失败达到某个阈值的时候(如50%请求超时,或者连续20次请失败),就打开断路器,那么后续的请求就直接返回失败,而不是一个长时间的等待,再根据一个时间间隔(如30秒)或请求超时的情况(如0%的超时)尝试关闭断路器(或者更换保险丝),看依赖是否恢复服务了。
一个服务依赖多个服务时,如果其中一个非核心的依赖不可用,通过设置超时和使用断路器,可以确保Service A在调用异常的Service E并不会导致自身的异常,在大部分情况下服务还能健康运转,可以很好的做到依赖隔离。如下图所示:
![a475b6d1e95cacdc23071ea6ae38b7d5c297b93e](https://i-blog.csdnimg.cn/blog_migrate/92cae6203a3b1772ffd191ff009fa39e.png)
4)设置限流
在服务访问的高峰期可能因为大量的并发导致性能下降,严重时将会有大量的请求排队,可能会导致服务宕机。为了保证应用的可用性,可拒绝低优先级的调用,让高优先级的请求成功,避免所有调用都失败的情况,并且为每个依赖服务提供一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队,可以加速失败判定时间。这样的结果是有些用户可以访问,而有些用户失败,但失败的用户重新访问又可以是正常的访问。这样能确保服务的可用性,而不是完全不可用。
虽然有了上面的一些可提高系统可用性的措施,但系统是复杂的,一个简单的修复都有可能造成不可想像的后果,且系统又是动态的,有些系统可能一天都发布几次,几十次。在这种情况下 故障依然是不可避免的。比起半夜深睡或正在享受节假日的美好时光时系统故障来当救火队员,会做更多的措施来提高系统的可用性。比如在某些企业里会定期举行生产环境的故障应急演练。过去都是在业务低峰时进行人为故障测试高可用方案是否生效,包括主机,网络,应用,存储等每一层架构都进行演练,而现在也逐渐的在正常的生产时间进行故障应急演练,检查系统的高可用性。问题在于可能在演练时能够立即恢复,但真实故障发生时还是会出现长时间故障得不到恢复的情况。一个是演练是按照已知的场景制定的方案实施,二是演练的范围基本是高可用节点的切换或灾备系统的切换,第三个问题是这个演练是人为操作,需要全员的参与,并不会频繁的举行。但系统是动态的,这次是高可用的,不代表下周,或下个月还是高可用的。
当单体架构变成微服务架构后,应用层的演练就会变得复杂,就像前面提到的,如果每个服务只有一个故障可能都会有2100种不同。因此需要有一种自动的故障测试方式来回避微服务化后演练实施的可操作性。Netflix公司提出了一种自动故障测试的方案来提高微服务架构的可用性。这个测试方案也是在生产环境中进行,而故障测试的最终目的,是为了当真的有故障发生时,生产环境不会停止服务,并且整套系统可以在没有人为干预的情况下,非常优雅地通过降级将发生故障的部分组件排除出去。他们认为如果只在测试环境中测试,而真实生产环境的业务压力,业务场景,环境配置、网络性能和硬件性能都没有测试过,当故障在生产环境中真实发生时发现缓解问题的方案可能会失效。而且这个测试只在工作时间运行,这样工程师可以得到告警并及时响应。
![e9d8babee193fd1861274b31f351a96784e91c9c](https://i-blog.csdnimg.cn/blog_migrate/64b597797b78368e0787847fc0971c0e.png)
(A or R or P or B)
在最开始,上图中的四个节点都是必须的,且正常的。然后从这个正确输出反推,随机选择一个节点注入故障,找到并构建支持其正确性的逻辑链条图 。当节点注入故障后,这时可能有三种情况:
2.这个请示是成功的-但这个失败的节点不是关键性的
3.这个请求成功,有高可用节点取代了这个失败
在这个例子中,首先在Ratings中注入失败,但请求是成功的。说明Rating失败并不会影响服务的使用,那就先把这个节点排除,重新绘制请求树:
![e594b9a7f6982cd2f3a9b9dea1074e200a30a8f7](https://i-blog.csdnimg.cn/blog_migrate/76eadffb6e4159bb7ddaa1435ef93c19.png)
(A or P or B) and (A or P or B or R)
这时可以看到,请求可以通过(A or P or B)的方式实现,也可以通过 (A or P or B or R)的方式实现。接下来再在Playlist中注入故障,这时请求还是成功的,因为请求转发到备用节点上执行,这里将会有一个新的节点可以访问。
![b2c83811692c1a25e2c3fd8dd6e08420ec528016](https://i-blog.csdnimg.cn/blog_migrate/016c794323911707e3c027a725e714ec.png)
(A or PF or B) and (A or P or B) and (A or P or B or R)
这时可以更新公式,说明可以通过(A or PF or B) and (A or P or B) and (A or P or B or R)三种方式请求服务。然后通过这样不停的测试直到遍历完所有正确输出,没有失败的节点可以找到。
Molly没有规定怎么搜索空间,所以实现时会估算所有的方案,然后随机选择最小的方案的集合。比如,最后的方案可能是[{A}, {PF}, {B}, {P,PF}, {R,A}, {R,B} …]。先选择所有的单节点注入失败,再选择所的有双节点的组合注入失败,依此类推。
![8e7567a195bd116d08bf79877f77df27dcbc7741](https://i-blog.csdnimg.cn/blog_migrate/a3198ba8f3b391b5d24145a3cd7f29ce.png)
故障模拟测试从FIT服务把故障模拟元数据注入到Zuul(缘边网关服务)开始,如果请求符合故障范围(failure scope)则注入失败。这个故障可能是延迟服务调用,或达到持久层失败。每个被接触到的注入点(injection points)检查这个请求的上下文是否为指定要被注入故障的组件,如果是,在这个注入点模拟故障。
参考:
http://techblog.netflix.com/2016/01/automated-failure-testing.html
http://techblog.netflix.com/2014/10/fit-failure-injection-testing.html
作者简介:
陈爱珍,七牛云布道师。多年企业级系统的应用运维及分布式系统实战经验。现专注于容器、微服务及devops落地的研究与实践。
本文来自中生代技术交流群
微信公众号: freshmanTechnology