1. 背景
微服务化之后,系统分布式部署,传统单个流程的本地API调用被拆分成多个微服务之间的跨网络调用,由于引入了网络通信、序列化和反序列化等操作,系统发生故障的概率提高了很多。
微服务故障,有些是由于业务自身设计或者编码不当导致,有些是底层的微服务化框架容错能力不足导致。在实际项目中,需要从业务和平台两方面入手,提升微服务的可靠性。
1.1. 无处不在的故障
1.1.1. 分布式部署和调用
传统单体架构一个完整的业务流程往往在同一个进程内部完成处理,不需要进行分布式协作,它的工作原理如下所示:
图1-1 传统单体架构本地方法调用
微服务化之后,不同的微服务采用分布式集群部署方式,服务的消费者和提供者通常运行在不同的进程中,需要跨网络做RPC调用,它的工作原理如下所示:
图1-2 微服务分布式RPC调用
分布式调用之后,相比于传统单体架构的本地方法调用,主要引入了如下潜在故障点:
- 序列化与反序列化:微服务的请求和应答都需要经过序列化和反序列化,做消息的跨网络通信,由于数据结构不一致、不支持的数据类型、对方编解码错误等都会导致序列化和反序列化失败,进而导致微服务调用失败。
- 网络问题:常见的包括网络超时、网络闪断、网络单通、网络拥塞等,都可能会导致微服务远程调用的失败。
1.1.2. 大型系统微服务进程内合设
理想情况下,每个微服务都独立打包和部署,微服务之间天然就支持进程级隔离,但事实上,对于一个大规模的企业IT系统、或者大型网站,是由成百上千个微服务组成的,在实践中,微服务通常是不可能做到百分之百独立部署的,原因如下:
- 方便开发:通常会按照业务域划分团队,同一个业务域往往包含多个微服务,由一个团队负责开发。为了方便CI/CD,同一业务域的微服务往往打包和部署在一起,而不是每个微服务独立打包部署。
- 方便运维:海量的微服务进程(以1000个微服务 * 10个进程实例为例),会增加部署、数据采集(性能KPI和日志等)、告警、问题定位等成本,如果运维自动化程度不高,很难支撑大规模的微服务独立部署。
- 提升性能:一些业务对时延非常敏感,如果该业务链上的所有微服务调用都跨网络通信,时延往往无法满足业务要求。通过将微服务合设在同一个进程之内,利用路由短路,把RPC调用转化成本地方法调用,可以极大的提升性能。
- 简化分布式事务处理:分布式部署之后,会带来分布式事务问题。有时候业务为了简化分布式事务的处理,将事务相关的微服务部署在同一个进程中,把分布式事务转换成本地事务,简化事务处理。
不同的微服务合设在同一个进程之中,就会引入一系列潜在的故障点,例如:
- 处理较慢的微服务会阻塞其它微服务
- 某个微服务故障蔓延,可能导致整个进程不可用
- 低优先级的微服务,抢占高优先级微服务的资源
1.1.3. 微服务健康度
传统情况下,往往使用服务注册中心检测微服务的状态,当检测到服务提供者不可用时,会将故障的服务信息广播到集群所有节点,消费者接收到服务故障通知消息之后,根据故障信息中的服务名称、IP地址等信息,对故障节点进行隔离。它的工作原理如下所示:
图1-3 微服务状态检测
使用基于心跳或者会话的微服务状态检测,可以发现微服务所在进程宕机、网络故障等问题,但在实际业务中,微服务并非“非死即活”,它可能处于“亚健康状态”,服务调用失败率很高,但又不是全部失败。或者微服务已经处于过负荷流控状态,业务质量受损,但是又没有全部中断。
使用简单的微服务状态检测,很难应对上述这些场景。通过对微服务的运行质量建模,利用微服务健康度模型,根据采集的各种指标对微服务健康度实时打分,依据打分结果采取相应的可靠性对策,可以更有针对性的保障系统的可靠性。
1.1.4. 同步的I/O操作
在整个微服务调用过程中,主要会涉及到三类I/O操作:
- 网络I/O操作,涉及到网络读写
- 磁盘I/O操作,主要是记录日志、话单、写本地文件等
- 数据库访问,例如Java使用JDBC驱动进行数据库操作
图1-4 微服务涉及的主要I/O操作
凡是涉及到I/O操作的,如果I/O操作是同步阻塞模式,例如Java的BIO、文件File的读写操作、数据库访问的JDBC接口等,都是同步阻塞的。只要访问的网络、磁盘或者数据库实例比较慢,都会导致调用方线程的阻塞。由于线程是Java虚拟机比较重要的资源,当大量微服务调用线程被阻塞之后,系统的吞吐量将严重下降。
1.1.5. 第三方SDK API调用
在微服务中,调用第三方SDK API,也可能会引入新的故障点,例如通过FTP客户端访问远端的FTP服务,或者使用MQ客户端访问MQ服务,如果这些客户端API的容错性设计不好,也会导致调用方的级联故障,这些故障是潜在和隐性的,在设计的时候往往容易被忽视,但它带来的风险和危害是巨大的。
1.2. 微服务可靠性
软件可靠性是指在给定时间内,特定环境下软件无错运行的概率。软件可靠性包含了以下三个要素:
1) 规定的时间:软件可靠性只是体现在其运行阶段,所以将运行时间作为规定的时间的度量。运行时间包括软件系统运行后工作与挂起(启动但空闲)的累计时间。由于软件运行的环境与程序路径选取的随机性,软件的失效为随机事件,所以运行时间属于随机变量。
2) 规定的环境条件:环境条件指软件的运行环境。它涉及软件系统运行时所需的各种支持要素,如支持硬件、操作系统、其它支持软件、输入数据格式和范围以及操作规程等。
3) 规定的功能:软件可靠性还与规定的任务和功能有关。由于要完成的任务不同,则调用的子模块就不同(即程序路径选择不同),其可靠性也就可能不同。所以要准确度量软件系统的可靠性必须首先明确它的任务和功能。
1.2.1. 关键的可靠性因素
微服务的运行质量,除了自身的可靠性因素之外,还受到其它因素的影响,包括网络、数据库访问、其它相关联的微服务运行质量等。微服务的可靠性设计,需要考虑上述综合因素,总结如下:
图1-5 微服务可靠性设计模型
2. 异步I/O操作
2.1. 网络I/O
2.1.1. 使用同步阻塞I/O的问题
以Java为例,在JDK 1.4推出JAVA NIO1.0之前,基于JAVA的所有Socket通信都采用了同步阻塞模式(BIO),这种一请求一应答的通信模型简化了上层的应用开发,但是在可靠性和性能方面存在巨大的弊端:
2-1 传统Java 同步阻塞I/O模型
采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,接收到客户端连接之后为客户端连接创建一个新的线程处理请求消息,处理完成之后,返回应答消息给客户端,线程销毁,这就是典型的一请求一应答模型。该架构最大的问题就是不具备弹性伸缩能力,当并发访问量增加后,服务端的线程个数和并发访问数成线性正比,由于线程是JAVA虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能急剧下降,随着并发量的继续增加,可能会发生句柄溢出、线程堆栈溢出等问题,并导致服务器最终宕机。
2.1.2. 使用非阻塞I/O通信
微服务进行远程通信时,通过使用非阻塞I/O,可以解决由于网络时延大、高并发接入等导致的服务端线程数膨胀或者线程被阻塞等问题。
以Java为例,从JDK1.4开始,JDK提供了一套专门的类库支持非阻塞I/O,可以在java.nio包及其子包中找到相关的类和接口。JDK1.7之后,又提供了NIO2.0类库,支持异步I/O操作。
利用JDK的异步非阻塞I/O,可以实现一个I/O线程同时处理多个客户端链路,读写操作不会因为网络原因被阻塞,I/O线程可以高效的并发处理多个客户端链路,实现I/O多路复用,它的工作原理如下所示:
2-2 Java非阻塞I/O模型
使用非阻塞I/O进行通信,以Java语言为例,建议策略如下:
1) TCP私有协议:建议直接基于Netty开发。
2) HTTP/Restful/SOAP等:选择支持非阻塞I/O的Web框架。也可以选择基于Netty构建的开源应用层协议栈框架,例如支持异步Restful的RestExpress。
2.2. 磁盘I/O
微服务对磁盘I/O的操作分为两类:
- 直接文件操作:例如调用File的open、write、read等接口,进行文件操作。
- 间接文件操作:例如调用日志类库写日志,虽然微服务并没有直接操作日志文件,但是日志类库底层还是会进行文件的读写等操作。
在实际项目中,最容易被忽视的就是日志操作。不同的日志类库,写日志的机制不同,以Log4j 1.2.X版本为例,当日志队列满之后,有多种策略:
- 同步等待,直到新的日志消息能够入队列,它会阻塞当前业务线程。
- 丢弃当前的日志消息,不会阻塞当前业务线程。
- 不入队列,由当前调用写日志的业务线程执行日志I/O操作,如果此时磁盘I/O写入速度慢,则会阻塞当前业务线程。
在实际生产环境中,我们就遇到过类似问题,在某些时段,磁盘WIO达到10+持续几秒钟-10几秒钟,然后又恢复正常。WIO较高的时段,需要写接口日志、话单等,由于系统默认采用的是同步等待策略,结果导致通信I/O线程、微服务调度线程等都被阻塞,最终链路因为心跳超时被强制关闭、微服务被大量阻塞在消息队列中导致内存居高不小、响应超时等。
由于偶现的WIO高导致同步写日志被阻塞,继而引起通信线程、微服务调用线程级联故障,定位起来非常困难,平时Code Review也很难被注意到。所以,隐性的磁盘I/O操作,更需要格外关注。
要解决上面的问题,有三种策略:
- 使用非阻塞I/O,对文件进行异步读写操作。
- 业务层面封装一个异步的I/O操作,最简单的策略就是由一个独立的线程或者线程池来执行磁盘I/O操作。
- 选择支持非阻塞方式调用的I/O类库,例如使用log4j的异步日志API。
以JDK1.7为例,它提供了异步的文件I/O操作类库,基于该类库,就不需要担心磁盘I/O操作被阻塞:
2-3 JDK1.7异步非阻塞文件接口
自己在上层封装异步I/O操作,也比较简单,它的优点是可以实现磁盘I/O操作与微服务之间的线程隔离,但是底层仍然使用的是同步阻塞I/O,如果此时磁盘的I/O比较高,依然会阻塞写磁盘的I/O线程。它的原理如下所示:
2-4 应用层封装的异步文件操作
将文件I/O操作封装成一个Task或者Event,投递到文件I/O线程池的消息队列中,根据投递结果,构造I/O操作相关联的Future对象给微服务调用线程。通过向Future对象注册Listener并实现callback接口,可以实现异步回调通知,这样微服务和文件I/O操作就实现了线程隔离。文件I/O操作耗时,并不会阻塞微服务调度线程。
当使用第三方文件I/O操作类库时,需要注意下相关API,尽量使用支持异步非阻塞接口的API,如果没有,则需要考虑是否做上层的异步封装。
2.3. 数据库操作
部分数据库访问支持非阻塞方式,例如Oracle的OCI,它支持non-blocking模式和blocking模式:阻塞方式就是当调用 OCI操作时,必须等到此OCI操作完成后服务器才返回客户端相应的信息,不管是成功还是失败。非阻塞方式是当客户端提交OCI操作给服务器后,服务器立即返回OCI_STILL_EXECUTING信息,而并不等待服务端的操作完成。对于non-blocking方式,应用程序若收到一个OCI函数的返回值为OCI_STILL_EXECUTING时必须再次对每一个OCI函数的返回值进行判断,判断其成功与否。 可通过设置服务器属性为OCI_ATTR_NONBLOCKING_MODE来实现。
对于Java语言而言,由于JDK本身提供了数据库连接驱动相关的接口定义,JDBC驱动本身就是同步API接口,因此,Java语言的开源ORM框架也都是同步阻塞的,例如MyBatis、Hibernate等。
尽管大部分数据库访问接口是同步阻塞的,但是由于数据库中间件的超时控制机制都比较成熟,因此通过合理设置超时时间,可以避免微服务的数据库访问被长时间挂住。
也可以在应用上层封装异步数据库操作层,实现微服务调度与数据库操作的线程级隔离,原理2.2章节已经介绍过,采用该方式同样存在两点不足:
- 排队现象:如果某个数据库操作非常耗时,超时时间配置的又比较大(例如30S),会导致后续的数据库操作在队列中排队。
- 无法充分发挥数据库效能:由于底层数据库访问采用同步阻塞的方式,所以不能高效发挥数据库的效能。
3. 故障隔离
由于大部分微服务采用同步接口调用,而且多个领域相关的微服务会部署在同一个进程中,很容易发生“雪崩效应”,即某个微服务提供者故障,导致调用该微服务的消费者、或者与故障微服务合设在同一个进程中的其它微服务发生级联故障,最终导致系统崩溃。
为了避免“雪崩效应”的发生,需要支持多种维度的依赖和故障隔离,以实现微服务的HA。
3.1. 通信链路隔离
由于网络通信本身通常不是系统的瓶颈,因此大部分服务框架会采用多线程+单个通信链路的方式进行通信,原理如下所示:
3-1 多线程-单链路P2P通信模式
正如前面章节所述,由于微服务使用异步非阻塞通信,单个I/O线程可以同时并发处理多个链路的消息,而且网络读写都是非阻塞的,因此采用多线程+单链路的方式进行通信性能本身问题不大。但是从可靠性角度来看,只支持单链路本身又存在一些可靠性隐患,我们从下面的案例中看下问题所在。
某互联网基地微服务架构上线之后,发现在一些时段,经常有业务超时,超时的业务没有固定规律。经定位发现当有较多的批量内容同步、语音和视频类微服务调用时,系统的整体时延就增高了很多,而且存在较突出的时延毛刺。由于这些操作获取的消息码流往往达到数M到数十兆,微服务之间又采用单链路的方式进行P2P通信,导致大码流的传输影响了其它消息的读写效率,增大了微服务的响应时延。
问题定位出来之后,对微服务之间的通信机制做了优化,节点之间支持配置多链路,每个链路之间还可以实现不同策略的隔离,例如根据消息码流大小、根据微服务的优先级等策略,实现链路级的隔离,优化之后的微服务通信机制:
图3-2 支持多链路隔离
3.2. 调度资源隔离
3.2.1. 微服务之间隔离
当多个微服务合设运行在同一个进程内部时,可以利用线程实现不同微服务之间的隔离。
对于核心微服务,发布的时候可以独占一个线程/线程池,对于非核心微服务,则可以共享同一个大的线程池,在实现微服务隔离的同时,避免线程过于膨胀:
图3-3 微服务之间故障隔离
假如非核心服务3发生故障,长时间阻塞线程池1的工作线程,其它与其共用线程池消息队列的非核心服务1和服务2只能在队列中排队等待,当服务3释放线程之后,排队的服务1和服务2可能已经超时,只能被丢弃掉,导致业务处理失败。
采用线程池隔离的核心服务1和服务2,由于各自独占线程池,拥有独立的消息队列,它的执行不受发生故障的非核心服务1影响,因此可以继续正常工作。通过独立线程池部署核心服务,可以防止故障扩散,保障核心服务的正常运行。
3.2.2. 第三方依赖隔离
在微服务中通常会调用第三方中间件服务,例如分布式缓存服务、分布式消息队列、NoSQL服务等。只要调用第三方服务,就会涉及跨网络操作,由于客户端SDK API的封装,很多故障都是隐性的,因此,它的可靠性需要额外关注。
整体而言,第三方依赖隔离可以采用线程池 + 响应式编程(例如RxJava)的方式实现,它的原理如下所示:
1) 对第三方依赖进行分类,每种依赖对应一个独立的线程/线程池。
2) 微服务不直接调用第三方依赖的API,而是使用异步封装之后的API接口。
3) 异步调用第三方依赖API之后,获取Future对象。利用响应式编程框架,可以订阅后续的事件,接收响应,针对响应进行编程。
利用Netflix开源的hystrix + RxJava,可以快速实现第三方依赖的隔离,后续章节我们会详细介绍下如何使用。
3.3. 进程级隔离
对于核心的微服务,例如商品购买、用户注册、计费等,可以采用独立部署的方式,实现高可用性。
3.3.1. 容器隔离
微服务鼓励软件开发者将整个软件解耦为功能单一的服务,并且这些服务能够独立部署、升级和扩容。如果微服务抽象的足够好,那么微服务的这一优点将能够提升应用的敏捷性和自治理能力。
利用Docker容器部署微服务,可以带来如下几个优点:
- 高效:Docker容器的启动和停止不需要几分钟,只要几百毫秒就足够了。使用Docker部署微服务,微服务的启动和销毁速度非常快,在高压力时,可以实现秒级弹性伸缩。
- 高性能:Docker容器的性能接近裸的物理机,比VM平均高20%+。
- 隔离性:利用Docker,可以实现0.1 core的隔离。基于细粒度的资源隔离机制,可以实现高密度的部署微服务,同时实现它们之间的资源层隔离,保障微服务的可靠性。
- 可移植性:在基于虚拟机的解决方案中,应用的可移植性通常来说会受到云提供商所提供的虚拟机格式限制。如果应用程序需要部署到不同类型的虚拟机中,需要针对特定的虚拟机格式做镜像文件,新增很多额外的开发和测试工作量。Docker容器的设计理念是“一次编写,到处运行”,这可以使开发者避免上面这种限制。
基于Docker容器部署微服务,实现物理资源层隔离示意图如下所示:
图3-4 基于Docker容器的微服务隔离
3.3.2. VM隔离
除了Docker容器隔离,也可以使用VM对微服务进行故障隔离,相比于Docker容器,使用VM进行微服务隔离存在如下优势:
- 微服务的资源隔离性更好,CPU、内存、网络等可以实现完全的资源隔离。
- 对于已经完成硬件虚拟化的遗留系统,可以直接使用已有的VM,而不需要在VM中重新部署Docker容器。
4. 集群容错
当微服务不可用时,需要根据预置的策略做容错处理,大部分的容错能力和策略是公共的,因此可以下沉到服务框架中实现。
4.1. 路由容错
当集群环境中微服务调用失败之后,利用路由容错机制,可以在底层实现微服务的自动容错处理,提升系统的可靠性。
常用的容错策略包括:
- 失败自动切换机制:微服务调用失败自动切换策略指的是当发生服务调用异常时,重新选路,查找下一个可用的微服务提供者。微服务发布的时候,可以指定服务的集群容错策略。消费者可以覆盖服务提供者的通用配置,实现个性化的容错策略。
- 失败回调机制:微服务调用失败之后,提供异常回调接口,执行微服务消费者自定义的失败处理逻辑。
- 快速失败机制:在业务高峰期,对于一些非核心的服务,希望只调用一次,失败也不再重试,为重要的核心服务节约宝贵的运行资源。此时,快速失败是个不错的选择。快速失败策略的设计比较简单,获取到服务调用异常之后,直接忽略异常,记录异常日志。
4.2. 服务降级
大促或者业务高峰时,为了保证核心服务的SLA,往往需要停掉一些不太重要的业务,例如商品评论、论坛或者粉丝积分等。
另外一种场景就是某些服务因为某种原因不可用,但是流程不能直接失败,需要本地Mock服务端实现,做流程放通。以图书阅读为例,如果用户登录余额鉴权服务不能正常工作,需要做业务放通,记录消费话单,允许用户继续阅读,而不是返回失败。
通过服务治理的服务降级功能,即可以满足上述两种场景的需求。
4.2.1. 强制降级
当外界的触发条件达到某个临界值时,由运维人员/开发人员决策,对某类或者某个服务进行强制降级。
强制降级的常用策略:
- 不发起远程服务调用,直接返回空。例如mock = force: return null。
- 不发起远程服务调用,直接抛出指定异常。例如mock = force: throw Exception。
- 不发起远程服务调用,直接执行本地模拟接口实现类。mock = force: execute Bean: <Spring beanName>。
4.2.2. 容错降级
当非核心服务不可用时,可以对故障服务做业务逻辑放通,以保障核心服务的运行。
容错降级与屏蔽降级的主要差异是:
- 触发条件不同:容错讲解是根据服务调用结果,自动匹配触发的;而屏蔽降级往往是通过人工根据系统运行情况手工操作触发的。
- 作用不同:容错降级是当服务提供者不可用时,让消费者执行业务放通;屏蔽降级的主要目的是将原属于降级业务的资源调配出来供核心业务使用。
- 调用机制不同:一个发起远程服务调用,一个只做本地调用。
容错降级的常用策略如下:
- 异常转义:mock = fail: throw Exception。
- 自定义降级逻辑:mock = fail: execute Bean: <beanName>。将异常屏蔽掉,直接执行本地模拟接口实现类,返回Mock接口的执行结果。
4.2.3. 服务降级Portal
利用服务治理Portal,可以在线的动态修改微服务的降级策略,实时生效,它的界面如下所示:
图4-1 服务降级配置界面
4.3. 熔断机制
熔断机制(Circuit Breaker),也叫自动停盘机制,是指当股指波幅达到规定的熔断点时,交易所为控制风险采取的暂停交易措施。
在微服务领域,熔断机制是从消费端保护微服务提供者的措施,当微服务的运行质量低于某个临界值时,启动熔断机制,暂停微服务调用一段时间,以保障后端的微服务不会因为持续过负荷而宕机。
4.3.1. 工作原理
微服务的熔断机制原理如下所示:
- 微服务调用时,对熔断开关状态进行判断,当熔断器开关关闭时, 请求被允许通过熔断器。如果当前微服务健康度高于指定阈值, 开关继续保持关闭。否则开关切换为打开状态。
- 当熔断器开关打开时,微服务调用请求被禁止通过。调用失败,执行本地降级逻辑,如果没有实现降级逻辑,默认返回异常。
- 当熔断器开关处于打开状态时, 经过指定周期T, 熔断器会自动进入半开状态, 这时熔断器会允许请求通过,当请求调用成功时, 熔断器恢复到关闭状态。若失败, 则继续保持打开状态。
它的工作原理示意如下:
图4-2 微服务熔断器工作原理
熔断器机制能保证微服务消费者在微服务运行状态不佳时,快速返回结果,避免大量的同步等待。并且能在指定周期T后继续侦测微服务是否可用, 以实现故障恢复之后的自动感知。
4.3.2. 微服务健康度
熔断器开关的状态取决于微服务的运行质量,微服务的运行质量通常由多种因素决定,具有多个衡量因子。通过对微服务健康度建模,可以实现对微服务运行质量的360°实时评估。
微服务健康度模型如下所示:
图4-3 微服务健康度模型
微服务运维体系通过分布式日志采集系统、告警系统、性能KPI数据采集等,利用在线大数据实时分析技术,通过健康度模型,对微服务的健康度按照周期进行实时打分,同时将微服务的得分通过消息队列订阅发布出去,各个节点订阅微服务的健康度得分,与熔断器阈值进行比较,修改熔断器开关的状态。
5. 流量控制
当资源成为瓶颈时,服务框架需要对消费者做限流,启动流控保护机制。流量控制有多种策略,比较常用的有:针对访问速率的静态流控、针对资源占用的动态流控等。
在实践中,各种流量控制策略需要综合使用才能起到较好的效果。
5.1. 动态流控
动态流控的最终目标是为了保命,并不是对流量或者访问速度做精确控制。当系统负载压力非常大时,系统进入过负载状态,可能是CPU、内存资源已经过载,也可能是应用进程内部的资源几乎耗尽,如果继续全量处理业务,可能会导致消息严重积压或者应用进程宕机。
动态流控检测的资源包括:
- CPU使用率。
- 内存使用率(对于Java,主要是JVM内存使用率)。
- 队列积压率。
主机CPU、内存使用率采集算法非常多,例如使用java.lang.Process执行top、sar等外部命令获取系统资源使用情况,然后解析后计算获得资源使用率。也可以直接读取操作系统的系统文件获取相关数据,需要注意的是,无论是执行操作系统的本地命令,还是直接读取操作系统的资源使用率文件,都是操作系统本地相关的,不同的操作系统和服务器,命令和输出格式可能存在很大差异。在计算时需要首先判断操作系统类型,然后调用相关操作系统的资源采集接口实现类,通过这种方式就可以支持跨平台。
动态流控是分级别的,不同级别拒掉的消息比例不同,这取决于资源的负载使用情况。例如当发生一级流控时,拒绝掉1/4的消息;发生二级流控时,拒绝掉1/2消息;发生三级流控时,所有的消息都被流控掉。
不同的级别有不同的流控阈值,系统上线后会提供默认的;流控阈值,不同流控因子的流控阈值不同,业务上线之后通常会根据现场的实际情况做阈值调优,因此流控阈值需要支持在线修改和动态生效。
需要指出的是为了防止系统波动导致的偶发性流控,无论是进入流控状态还是从流控状态恢复,都需要连续采集N次并计算平均值,如果连续N次平均值大于流控阈值,则进入流控状态;同理,只有连续N次资源使用率平均值低于流控阈值,才能脱离流控恢复正常。
5.2. 静态流控
静态流控主要针对客户端访问速率进行控制,它通常根据服务质量等级协定(SLA)中约定的QPS做全局流量控制,例如计费服务的静态流控阈值为200 QPS,则无论集群有多少个计费服务实例,它们总的处理速率之和不能超过200 QPS。
由于微服务具备弹性伸缩、动态上线和下线等特性,因此集群中某个微服务实例的节点个数是动态变化的,采用传统的平均分配制无法做到精准的控制。
在实践中,比较成熟的集群静态流控策略是动态配额申请制,它的工作原理如下:
- 系统部署的时候,根据微服务节点数和静态流控QPS阈值,拿出一定比例的配额做初始分配,剩余的配额放在配额资源池中。
- 哪个微服务节点使用完了配额,就主动向服务注册中心申请配额。配额的申请策略:如果流控周期为T,则将周期T分成更小的周期T/N(N为经验值,默认值为10),当前的服务节点数为M个,则申请的配额为 (总QPS配额 - 已经分配的QPS配额)/M * T/N。
- 总的配额如果被申请完,则返回0配额给各个申请配额的服务节点,服务节点对新接入的请求消息进行流控。
5.3. 用户自定义流控机制
不同的业务,存在不同的流控策略,例如基于微服务优先级的流控、基于节假日的流控、基于业务字段的流控等。底层的服务框架无法实现所有业务级的定制流控策略,因此,过于业务化的流控往往由业务通过自定义流控机制定制实现。
服务框架提供服务调用入口的拦截点和切面接口,由业务实现自定义流控。也可以提供基础的流控框架,供业务实现流控条件判断、流控执行策略等,简化业务的定制工作量。
6. 使用Hystrix提升微服务可靠性
6.1. Hystrix简介
Hystrix是Netflix开源的一个可靠性组件,主要用于分布式环境中的依赖解耦,Hystrix library通过添加延迟容忍和容错逻辑来控制分布式服务之间的相互影响,通过服务之间访问的隔离点阻止连锁故障,并提供了失败回调机制,来改进系统的可靠性。
Hystrix提供如下机制来提升分布式系统的可靠性:
- 保护通过第三方客户端API依赖访问,控制其延迟和故障
- 阻止级联故障和“雪崩效应”
- 提供熔断机制,快速失败和恢复
- 失败回调和优雅降级机制
- 近实时检测、报警和KPI指标展示
6.2. Hystrix的核心功能
Hystrix提供了一些非常有价值、与具体微服务框架实现无关的特性,方便不同的分布式系统集成使用。
6.2.1. 依赖隔离
Hystrix使用命令模式HystrixCommand(Command)包装依赖调用逻辑,每个命令在单独线程/信号授权下执行。依赖调用的超时时间可配置,如果超时,则则返回失败或者执行fallback逻辑。原理如下所示:
图6-1 基于线程/信号的依赖隔离
6.2.2. 熔断器
Hystrix会先经过熔断器,此时如果熔断器的状态是打开,则说明已经熔断,这时将直接进行降级处理,不会继续将请求发到线程池。
熔断器的开关状态由熔断算法决定,它的原理如下:
- 判断是否熔断:根据bucket中记录的次数,计算错误率。如果错误率达到熔断预置的阈值,则开启熔断开关。
- 熔断恢复:对于被熔断的请求,暂停处理一段时间之后(HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()),允许单个请求通过,若请求成功,则取消熔断,否则,继续熔断。
Hystrix熔断器的工作原理如下所示:
图6-2 Hystrix熔断机制
6.2.3. 优雅降级
当微服务调用异常、超时,或者熔断时,可以通过回调Fallback()的方式实现业务的优雅降级,它的原理如下所示:
图6-3 Hystrix优雅降级机制
6.2.4. Reactive编程
Hystrix支持响应式编程,并提供了相关接口给用户,如下所示:
利用响应式编程,可以更加优雅和灵活的实现异步回调逻辑的处理。
6.2.5. 信号量隔离
为了降低线程资源的开销,Hystrix提供了信号量Semaphores,用于实现轻量级的依赖隔离。
开发者可以限制系统对某一个依赖的最高并发数,这个基本上等同于并发流控策略。每次微服务调用依赖时都会检查一下是否到达信号量的限制值,如达到则拒绝。该隔离策略的优点是不新起线程,减少上下文切换和线程数,缺点是无法配置断路,每次都一定会去尝试获取信号量。
6.3. 集成Hystrix
由于Hystrix与特定的分布式系统、微服务框架无关,是个通用的分布式系统可靠性组件,可以通过类库集成的方式方便的集成到已有的微服务架构体系中。
6.3.1. 集成架构
在已有微服务体系中集成Hystrix的策略如下:
- 微服务框架中,对于通用的微服务调用、磁盘I/O操作、数据库操作和网络I/O操作等使用HystrixCommand做一层异步包装,实现业务的微服务调用线程和第三方依赖的线程隔离。
- 对于非通用的第三方依赖,或者业务微服务自身引入的第三方依赖,直接基于HystrixCommand做异步隔离。
- 对第三方依赖进行分类、分组管理,根据依赖的特点设置熔断策略、优雅降级策略、超时策略等,以实现差异化的处理。
集成架构示例如下:
图6-4 集成Hystrix的微服务架构
6.3.2. 集成Hystrix带来的优点
第三方依赖隔离具备一定的通用性,例如数据库隔离、磁盘I/O隔离、第三方服务调用隔离等,如果各自构建一套隔离机制,除了增加工作量之外,后续维护起来也比较麻烦。
另外,业务微服务自身也会引入第三方依赖,如果没有通用的隔离机制,则业务需要自己构建业务级的隔离体系,相应的开发难度和工作量都较大,架构上也很难统一。
集成Hystrix,可以快速的构建微服务的隔离、熔断、优雅降级和响应式编程体系,提升系统的可靠性。
另外,Hystrix非常成熟,在Netflix已经经历过苛刻的生产环境考验,它的可靠性和成熟度完全能够满足大部分业务场景的需要。