如何提高微服务架构的可用性

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 本文来自中生代技术群的分享。本文作者陈爱珍具有多年企业级系统的应用运维及分布式系统实战经验。现专注于容器、微服务及devops落地的研究与实践。本文将为大家分享如何提高微服务架构的可用性,精彩不容错过。

业界通常用多少个9来衡量系统的可用性,如99.99%表示一年中有1小时左右的不可用时间。任何一个服务的可用性都不会是100%,意味着在服务运行时间里还是有可能发生故障。当把功能集中且运行在同一个应用中的单体架构拆分成多个相互独立的微服务架构后,虽然可以降低一损俱损的全局性故障风险,但由于微服务之间存在大量的依赖关系, 随着微服务个数的增多,依赖关系也将会变得越来越复杂,而且每个微服务都有可能发生故障,如果不能做好相互依赖的隔离,避免故障的连锁反应,结果可能比单体更糟糕。假设有100个微服务,并且每个微服务只会发生1种故障,那么总共会有2100种不同的故障场景,而每个微服务自身可能不止1种故障。当某个微服务发生故障时,如何确保不会导致其他依赖的微服务不可用, 如何确保系统自动降级把发生故障的微服务排除出去,如何确保故障不会扩展到整个系统? 那么如何有效确保微服务架构的可用性将会成为挑战。


下图是一个简化的用户请求示意图,设定一个用户请求依赖5个微服务的协作完成(pod为K8S容器框架中的定义,为一组相同功能的容器)。
  75ac4a93a47a5ba0f4fea001278fbe487306f348
在一开始每个依赖的Service都是正常的,现假设有一个Service异常了,这时可能会有三种情况:
1. 这个请求成功,假设因网络异常或宕机导致Service C某个节点不可用,但有高可用节点取代了这个失败节点,这时Service C不受影响,依然可用,如下图所示:
  4d87f2b4a4778f0136a098833399c5fbfeaf5bd5
2. 这个请求是成功的,假设是Service D故障,而这个Service不是关键性的,运行失败也可以继续进行,比如注册用户需要调用一个服务发送注册成功的邮件给用户,如果发邮件的这个Service不可用,但不会影响用户的注册,所以用户注册还是会成功,邮件可以等服务恢复后再发送,只有时间上的延迟。这时Service A不受影响,依然可用,如下图所示:
  fb7bfc44a31afee2a08a0baba79b3a828338cb40
3. 这个请求失败,比如异常的节点是Service E,而Service E是代码级逻辑异常,所有高可用节点都不可用,这时需要将Service E进行依赖隔离,否则ServiceA可能会受到ServiceE的影响而不可用。需要做一些措施确保Service A不会受影响,依然可用,如下图所示:
9f092a88ab2ab3438dcf39fc8c4def923d5fe27d
可以从以下几个策略可以提高微服务架构的可用性:
1) 失效转移
提高服务的高可用性,最基本的原则就是消除单点,通过负载均衡技术构建集群,所有的集群节点都是无状态且完全对等的。如上面讲的第1种情况。当一个节点异常时,负载均衡服务器会把用户发送的访问请求发送到可用的节点上。对用户来说,某个节点异常是无感的,用户请求会透明的转移到了可用的节点上执行。

2) 异步调用
避免一个服务失败导致整个应用请求失败很重要的是使用异步调用。如上面讲的第2种情况。如果采用的是同步调用,当邮件服务异常时,会导致其他两个服务也无法执行,最终导致用户注册失败。如果采用异步调用,Service A把用户注册信息发送给消息队列后立即返回用户注册成功的响应,虽然邮件服务不能用,但是写数据库的服务,权限开通等服务都能正常执行。所以即使邮件不能发送成功,也不会影响其他服务的执行,用户注册可顺利完成。

3) 依赖隔离
用户请求发送给Service A,Service A分配线程资源通过网络远程调用其他的Service,假设调用Service E发生异常时,Service A中对Service E调用的线程就可能会响应慢或僵死,而线程是系统的资源,如果短时间内得不到释放,在高并发的情况下资源就会被耗尽,结果会导致Service A也不可用,虽然其他的服务依然可用。
3a36239456eafb0218e603ab1d722d086a0e7ec8
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
4)设置限流
在服务访问的高峰期可能因为大量的并发导致性能下降,严重时将会有大量的请求排队,可能会导致服务宕机。为了保证应用的可用性,可拒绝低优先级的调用,让高优先级的请求成功,避免所有调用都失败的情况,并且为每个依赖服务提供一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队,可以加速失败判定时间。这样的结果是有些用户可以访问,而有些用户失败,但失败的用户重新访问又可以是正常的访问。这样能确保服务的可用性,而不是完全不可用。

虽然有了上面的一些可提高系统可用性的措施,但系统是复杂的,一个简单的修复都有可能造成不可想像的后果,且系统又是动态的,有些系统可能一天都发布几次,几十次。在这种情况下 故障依然是不可避免的。比起半夜深睡或正在享受节假日的美好时光时系统故障来当救火队员,会做更多的措施来提高系统的可用性。比如在某些企业里会定期举行生产环境的故障应急演练。过去都是在业务低峰时进行人为故障测试高可用方案是否生效,包括主机,网络,应用,存储等每一层架构都进行演练,而现在也逐渐的在正常的生产时间进行故障应急演练,检查系统的高可用性。问题在于可能在演练时能够立即恢复,但真实故障发生时还是会出现长时间故障得不到恢复的情况。一个是演练是按照已知的场景制定的方案实施,二是演练的范围基本是高可用节点的切换或灾备系统的切换,第三个问题是这个演练是人为操作,需要全员的参与,并不会频繁的举行。但系统是动态的,这次是高可用的,不代表下周,或下个月还是高可用的。


当单体架构变成微服务架构后,应用层的演练就会变得复杂,就像前面提到的,如果每个服务只有一个故障可能都会有2100种不同。因此需要有一种自动的故障测试方式来回避微服务化后演练实施的可操作性。Netflix公司提出了一种自动故障测试的方案来提高微服务架构的可用性。这个测试方案也是在生产环境中进行,而故障测试的最终目的,是为了当真的有故障发生时,生产环境不会停止服务,并且整套系统可以在没有人为干预的情况下,非常优雅地通过降级将发生故障的部分组件排除出去。他们认为如果只在测试环境中测试,而真实生产环境的业务压力,业务场景,环境配置、网络性能和硬件性能都没有测试过,当故障在生产环境中真实发生时发现缓解问题的方案可能会失效。而且这个测试只在工作时间运行,这样工程师可以得到告警并及时响应。


Netflix通过Peter Alvaro在论文 《路径驱动的故障注入(Lineage-driven fault injection)》中提到了一套名为“Molly”的算法和自身的故障注入测试FIT(failure injection testing)实现了这套安全地自动化故障注入测试。Molly是从一套系统的无故障状态出发,然后试图去回答说“系统是如何达到目前这种无故障的状态的?”简单举例介绍一下这个算法的原理。先利用自身的追踪系统绘制一个树来表示每个请求经过的所有的微服务,假设如下图所示:
  e9d8babee193fd1861274b31f351a96784e91c9c
                                                                        (A or R or P or B)

在最开始,上图中的四个节点都是必须的,且正常的。然后从这个正确输出反推,随机选择一个节点注入故障,找到并构建支持其正确性的逻辑链条图 。当节点注入故障后,这时可能有三种情况:


1.这个请求失败,我们已经找到一个节点会故障,从而我们可以删除未来的实验中包含这个故障。
2.这个请示是成功的-但这个失败的节点不是关键性的
3.这个请求成功,有高可用节点取代了这个失败
在这个例子中,首先在Ratings中注入失败,但请求是成功的。说明Rating失败并不会影响服务的使用,那就先把这个节点排除,重新绘制请求树:
e594b9a7f6982cd2f3a9b9dea1074e200a30a8f7
                                                             (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
                                                    (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} …]。先选择所有的单节点注入失败,再选择所的有双节点的组合注入失败,依此类推。


这个测试的目的是在影响大量成员前找到和修复故障,在生产环境上进行故障测试时,不能接受引起大量的问题节点。为了避免这个风险,只能在指定的范围构建测试,指定的范围包含两个关键的概念:故障范围(failure scope)和注入点(injection points)。故障范围指的是,把一次故障测试可能产生的影响,限制在一个可控的范围内,这个范围可以小到某个特定的用户或者设备,也可以大到所有用户的1%。而注入点指的是系统内计划会发生故障的组件,比如RPC层,缓存层,或者持久层。下图是这个测试的流程示意图:
             8e7567a195bd116d08bf79877f77df27dcbc7741
故障模拟测试从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


目录
相关文章
|
10天前
|
API 数据库 开发者
构建高效可靠的微服务架构:后端开发的新范式
【4月更文挑战第8天】 随着现代软件开发的复杂性日益增加,传统的单体应用架构面临着可扩展性、维护性和敏捷性的挑战。为了解决这些问题,微服务架构应运而生,并迅速成为后端开发领域的一股清流。本文将深入探讨微服务架构的设计原则、实施策略及其带来的优势与挑战,为后端开发者提供一种全新视角,以实现更加灵活、高效和稳定的系统构建。
18 0
|
9天前
|
Kubernetes 安全 Java
构建高效微服务架构:从理论到实践
【4月更文挑战第9天】 在当今快速迭代与竞争激烈的软件市场中,微服务架构以其灵活性、可扩展性及容错性,成为众多企业转型的首选。本文将深入探讨如何从零开始构建一个高效的微服务系统,覆盖从概念理解、设计原则、技术选型到部署维护的各个阶段。通过实际案例分析与最佳实践分享,旨在为后端工程师提供一套全面的微服务构建指南,帮助读者在面对复杂系统设计时能够做出明智的决策,并提升系统的可靠性与维护效率。
|
1天前
|
负载均衡 Java 开发者
细解微服务架构实践:如何使用Spring Cloud进行Java微服务治理
【4月更文挑战第17天】Spring Cloud是Java微服务治理的首选框架,整合了Eureka(服务发现)、Ribbon(客户端负载均衡)、Hystrix(熔断器)、Zuul(API网关)和Config Server(配置中心)。通过Eureka实现服务注册与发现,Ribbon提供负载均衡,Hystrix实现熔断保护,Zuul作为API网关,Config Server集中管理配置。理解并运用Spring Cloud进行微服务治理是现代Java开发者的关键技能。
|
3天前
|
监控 JavaScript 安全
构建微服务架构下的API网关
【4月更文挑战第15天】在微服务架构中,API网关扮演着至关重要的角色。它作为系统的唯一入口,不仅负责请求的路由、负载均衡和认证授权,还涉及到监控、日志记录和服务熔断等关键功能。本文将探讨如何构建一个高效且可靠的API网关,涵盖其设计原则、核心组件以及实现策略,旨在为后端开发人员提供一套实用的指导方案。
14 4
|
4天前
|
监控 负载均衡 API
构建高性能微服务架构:后端开发的最佳实践
【4月更文挑战第14天】 在当今快速发展的软件开发领域,微服务架构已成为构建可扩展、灵活且容错的系统的首选方法。本文深入探讨了后端开发人员在设计和维护高性能微服务时需要遵循的一系列最佳实践。我们将从服务划分原则、容器化部署、API网关使用、负载均衡、服务监控与故障恢复等方面展开讨论,并结合实际案例分析如何优化微服务性能及可靠性。通过本文的阅读,读者将获得实施高效微服务架构的实用知识与策略。
|
6天前
|
运维 监控 自动驾驶
构建可扩展的应用程序:Apollo与微服务架构的完美结合
构建可扩展的应用程序:Apollo与微服务架构的完美结合
27 10
|
12天前
|
运维 负载均衡 网络协议
探索微服务架构下的服务发现机制
【4月更文挑战第6天】 随着现代软件工程的发展,微服务架构因其灵活性、可扩展性而日益受到重视。在此架构模式下,服务发现成为了确保系统高可用性和弹性的关键组件。本文将深入探讨微服务环境中服务发现的核心概念、实现方式以及面临的挑战,旨在为开发者提供一套明晰的服务发现指南和实践建议。
|
16天前
|
负载均衡 网络协议 Java
构建高效可扩展的微服务架构:利用Spring Cloud实现服务发现与负载均衡
本文将探讨如何利用Spring Cloud技术实现微服务架构中的服务发现与负载均衡,通过注册中心来管理服务的注册与发现,并通过负载均衡策略实现请求的分发,从而构建高效可扩展的微服务系统。
|
17天前
|
消息中间件 安全 API
构建高效微服务架构:策略与实践
【4月更文挑战第1天】在数字化转型的浪潮中,微服务架构已成为企业追求敏捷、可扩展和灵活部署的重要技术手段。本文将深入探讨如何通过合理的设计原则和先进的技术栈,构建一个高效的微服务系统。我们将剖析微服务设计的核心要点,包括服务的划分、通信机制、数据一致性以及安全性问题,并结合案例分析,展示如何在现实世界中应用这些策略以提升系统的可靠性和性能。
|
17天前
|
设计模式 API 持续交付
构建高效微服务架构:从理论到实践
在当今快速迭代和部署的软件开发环境中,微服务架构已成为一种流行的设计模式,它允许开发团队以模块化的方式构建、维护和扩展应用程序。本文将深入探讨微服务的核心概念,包括其定义、优势、挑战以及如何在实际项目中实施。我们将通过一个实际案例来展示如何将传统的单体应用拆分成一系列独立、松耦合的服务,并通过容器化、服务发现、API网关和持续集成/持续部署(CI/CD)等技术手段来管理这些服务。