业界通常用多少个9来衡量系统的可用性,如99.99%表示一年中有1小时左右的不可用时间。任何一个服务的可用性都不会是100%,意味着在服务运行时间里还是有可能发生故障。当把功能集中且运行在同一个应用中的单体架构拆分成多个相互独立的微服务架构后,虽然可以降低一损俱损的全局性故障风险,但由于微服务之间存在大量的依赖关系, 随着微服务个数的增多,依赖关系也将会变得越来越复杂,而且每个微服务都有可能发生故障,如果不能做好相互依赖的隔离,避免故障的连锁反应,结果可能比单体更糟糕。
假设有100个微服务,并且每个微服务只会发生1种故障,那么总共会有 2 100 种不同的故障场景,而每个微服务自身可能不止1种故障。当某个微服务发生故障时,如何确保不会导致其他依赖的微服务不可用, 如何确保系统自动降级把发生故障的微服务排除出去,如何确保故障不会扩展到整个系统? 那么如何有效确保微服务架构的可用性将会成为挑战。
下图是一个简化的用户请求示意图,设定一个用户请求依赖5个微服务的协作完成(pod为K8S容器框架中的定义,为一组相同功能的容器)。
在一开始每个依赖的Service都是正常的,现假设有一个Service异常了,这时可能会有三种情况:
1. 这个请求成功,假设因网络异常或宕机导致Service C某个节点不可用,但有高可用节点取代了这个失败节点,这时Service C不受影响,依然可用,如下图所示:
2. 这个请求是成功的,假设是Service D故障,而这个Service不是关键性的,运行失败也可以继续进行,比如注册用户需要调用一个服务发送注册成功的邮件给用户,如果发邮件的这个Service不可用,但不会影响用户的注册,所以用户注册还是会成功,邮件可以等服务恢复后再发送,只有时间上的延迟。这时Service A不受影响,依然可用,如下图所示:
3. 这个请求失败,比如异常的节点是Service E,而Service E是代码级逻辑异常,所有高可用节点都不可用,这时需要将Service E进行依赖隔离,否则ServiceA可能会受到ServiceE的影响而不可用。需要做一些措施确保Service A不会受影响,依然可用,如下图所示:
可以从以下几个策略可以 提高微服务架构的可用性:
1) 失效转移
提高服务的高可用性,最基本的原则就是消除单点,通过负载均衡技术构建集群,所有的集群节点都是无状态且完全对等的。如上面讲的第1种情况。当一个节点异常时,负载均衡服务器会把用户发送的访问请求发送到可用的节点上。对用户来说,某个节点异常是无感的,用户请求会透明的转移到了可用的节点上执行。
2) 异步调用
避免一个服务失败导致整个应用请求失败很重要的是使用异步调用。如上面讲的第2种情况。如果采用的是同步调用,当邮件服务异常时,会导致其他两个服务也无法执行,最终导致用户注册失败。如果采用异步调用,Service A把用户注册信息发送给消息队列后立即返回用户注册成功的响应,虽然邮件服务不能用,但是写数据库的服务,权限开通等服务都能正常执行。所以即使邮件不能发送成功,也不会影响其他服务的执行,用户注册可顺利完成。
3) 依赖隔离
用户请求发送给Service A,Service A分配线程资源通过网络远程调用其他的Service,假设调用Service E发生异常时,Service A中对Service E调用的线程就可能会响应慢或僵死,而线程是系统的资源,如果短时间内得不到释放,在高并发的情况下资源就会被耗尽,结果会导致Service A也不可用,虽然其他的服务依然可用。
Service A的资源是有限的,比如Service A启动时分配了400个线程,当400个线程都因调用Service E时异常不能及时正常的释放,如线程死锁,响应时间慢,导致 400个线程全部僵死在调用Service E上,这里Service A就没有空闲的线程来接收新的用户请求,这时就会导致Service A挂起或僵死。所以避免Service A被依赖的服务拖垮就是要确保Service A的线程资源不会被调用的依赖服务耗尽,在 《Release It!》 一书中总结了非常重要的两条方法: 设置超时和使用断路器。