1. 引言
作为一种架构模式,微服务将复杂系统切分为数十乃至上百个小服务,每个服务负责实现一个独立的业务逻辑。这些小服务易于被小型的软件工程师团队所理解和修改,并带来了语言和框架选择灵活性,缩短应用开发上线时间,可根据不同的工作负载和资源要求对服务进行独立缩扩容等优势。另一方面,当应用被拆分为多个微服务进程后,进程内的方法调用变成了了进程间的远程调用,服务和服务之间的关系由“流量“来连接。
如何从流量的角度出发,来保证整个系统的稳定性,是这篇文章的出发点。下图是一个常见的完整的服务流量图。用户的请求从手机端或者浏览器发出,经过DNS解释,到达公司的网关。网关背后的防火墙对恶意流量进行清洗,最后到达公司的网关。最终这个请求到达前端应用。前端应用或者直接返回请求,或者会把部分请求交给下游组件处理。这个下游组件可能会是数据库,缓存,也可能是下游服务,甚至第三方应用。每个公司的结构或者会和这个系统有细节上的不同,例如客户请求是长连接还是短链接,应用和应用之间是否需要加设网关等。
图一:流量在系统里的流转
为了保证系统的稳定性,每个用户的请求能够得到最好的用户体验,我们在需要在运行态的情况下,在这个链路的不同的点,进行不同的防护。接下来的内容,我们根据这条链路上不同的点,采取不同的原则,依次讨论线上流量防护的最佳实践。
2. 最佳实践
从图一里我们可以看到,用户的请求从网关层真正开始进入系统,根据业务链路的不同的深度,逐步由前端应用,后端服务处理,最后到达缓存,数据库,甚至第三方的应用。接下来,我们会根据链路上上的不同位置,逐一介绍不同的原则。
2.1 尽早拦截无效的请求
无效的请求,会占用系统的处理能力,但又无法给业务上带来任何价值。我们需要尽早的识别并拦截把这些无效的请求。这些请求到达链路越下层,对系统带来的负担越重。
无效的请求主要分为两种:
- 恶意请求:通常是指恶意攻击,例如DDOS攻击,黄牛刷单等
- 对业务不产生实际意义的请求。例如秒杀活动中,必定会引起在一定的时间段,到来大量的请求都是针对秒杀商品的流量。而这些请求,仅仅只有和商品库存数量相当的请求是有效请求,其余都是无效请求。
基于这个原则,我们常常在网关层,对恶意请求进行安全拦截,最常见的恶意流量拦截有DDOS清洗,拦截大量的来自同个ip,同个用户的请求等等。随着黑产的不断发展,防护手段也变得越来越专业,到现在,拦截已经能够快速的根据请求的行为模式,来判断是否黑产流量了。
对于后者,则有很强的业务属性。对这种无效请求的拦截,先要梳理出一条完整的业务链路,找到这条业务链路的有效最大流量,再根据这个流量,在链路前端对请求进行收紧。举一个例子,一个由前端系统,商品服务系统(处理商品具体信息),交易服务(交易处理),库存服务(商品扣减库存)组成的秒杀系统。这个业务的链路的最大有效请求量是秒杀商品的最大库存。超过了这个秒杀商品的库存的请求都是无效的。为此,我们需要在这个系统的最前端,例如前端系统设置拦截,禁止单个商品超过秒杀商品的请求通过。
2.2 漏斗原则
刚刚我们讨论了尽早在链路入口拦截无效流量的原则,因为越到链路的后端,耗费的资源就越多。基于同样的原因,我们也希望系统后端处理的请求能够依次形成一个漏斗。假如处理系统由前端页面,WEB服务器,应用服务器,DB这个链路组成,那么理想的流量模型则是:
前端页面通过动静分离,仅仅将动态的部分交给下游的WEB服务器处理;静态的部分由缓存,或者CDN直接返回。
当请求到达WEB服务器之后,WEB服务器能够根据业务链路的短板,尽早的把无效流量拦截住,仅把有效的流量传递给服务方。
同理,下游的应用服务器,也应该尽量通过缓存等手段,给下游的数据库拦截大部分的流量。
通过使用动静分离,缓存等手段,让后端的处理量变得越来越少,形成一个如下图所示的流量处理漏斗。
图二:漏斗原则
2.3 隔离原则
这个理念是由Hystrix
https://github.com/Netflix/Hystrix)提出来的。它的核心理念是,能够快速的识别出请求链路上的异常,并且快速的把改异常隔离,从而让整个服务回到正常。下图很好的诠释了它的理念:
图三:隔离异常
经过多年在阿里巴巴集团使用的经验,我们可以把隔离细分到下面几个原则:
2.3.1 下游弱依赖服务可降级
弱依赖应用是指整个链路中可以被降级,而不影响最后业务效果的下游应用。打个比方,我们在淘宝浏览某个商品的时候,这个请求先到某个前端应用,而这个前端应用又会向下游服务,例如商品,评价中心,推荐中心等询问,最终返回商品的基础信息,购买过的人对该商品的评价,甚至对同类商品的推荐等等。对于这种场景,当提供商品基础信息的服务出现异常的时候,用户是无法浏览商品的,这个服务则是一个强依赖;而评价,推荐这两种服务,如果出现异常,用户仍然是可以正常的浏览商品的。对于这两种服务,我们称它为弱依赖。
当弱依赖出现异常的时候,最快的方法是对改弱依赖进行降级。当发现下游应用出现异常(响应时间过长,占用线程池过多,异常比例变大)的时候,迅速拒绝的方式,返回给上游系统。
如下图所示:
图四:弱依赖降级
通过这种手段,可以防止异常层层下传,下游应用拖垮上有应用,最大的把异常圈定在一定的范围内。这种方式往往用于第三方应用,下游弱依赖等。
2.3.2 强依赖下游异常可隔离
上面降级的方法,仅仅适用于弱依赖下游异常的场景。而对强依赖,即处理链路中必须经过的下游应用,则无法如此处理。举个常见的例子,应用A在处理某个业务请求的时候,必须对数据库进行读写。这个时候,对数据库进行读写的操作,就是这条业务链路的强依赖。而当对数据库进行读写正好是个慢SQL的时候,大量的读写,会占用整个数据库的链接数,导致其它的数据库读写无法操作,最终拖慢了整个应用。
对于这种强依赖,我们无法简单的进行降级。但是我们可以通过慢sql的并发数量的方式,将异常操作在一定的范围之内,而不会抢占其它正常数据库操作。这个我们称为错误隔离。业内有非常多的隔离的方案,比如通过不同业务逻辑使用不同线程池来隔离业务自身之间的资源争抢(线程池隔离),信号量,并发线程数等。这种隔离方案虽然隔离性比较好,但是代价就是线程数目太多,线程上下文切换的 overhead 比较大,特别是对低延时的调用有比较大的影响。我们更推荐使用信号量或者并发线程数的方式。这种方式无需创建和管理线程池,而是简单统计当前请求上下文的线程数目,如果超出阈值,新的请求会被立即拒绝,从而达到将慢SQL控制在一定的并发范围之内,效果如下所示:
图五:强依赖下游可隔离
通过这个方式,把不稳定的内部处理控制在一定的范围之内,并且不受响应时间的影响。如果处理请求的速度较慢,那么通过的请求就会响应减少;反之,如果请求的处理速度恢复,通过的请求就会回升。
2.3.3 异常机器可隔离
除了对整个下游应用进行隔离以外,我们也需要考虑从服务当中的个别机器出现异常的维度,来隔离异常。即我们常说的线上会出现单点、局部问题。
分布式环境中, 大家可能默认会认为我每台机器的服务能力都是一样的, 但实际上, 我们线上机器会由于软件、硬件、网络等因素导致机器的实时服务能力有差异。大家往往不会太在意单台机器的服务能力, 然而,当我们单机、局部服务能力出现问题时, 带来的影响, 远比我们预估的要严重。
如下图所示:
图六:隔离异常机器
- 首先,分布式环境调用链路局部问题会被放大到整个链路。在今天这么大流量的情况下, 任何单个系统, 都无法处理如今这复杂的业务逻辑。任意一个请求, 涉及到的决不仅仅是一个系统, 而是一整条链路。而链路中任何一个单点出现问题, 比如任意一台机器的RT变长、或者调用链路上的单点不可用, 会直接导致我们整个调用链路RT变长或者调用链路不可用。
- 其次,单点、局部问题会被放大成面。线上所有的调用链路真是的情况其实是网状结构, 我们的一个应用会有着多个上、下游应用, 因而一旦我们的单点、局部出现问题, 可能导致的是下游的应用都都到影响。1%的机器出现故障, 可能导致100%的业务出现问题。
因此,我们需要具备在集群中把出现异常的机器摘取识别出来的能力。这里的异常机器包括:
- 超卖问题带来的资源争抢问题。超卖并不是一件坏事, 淘系成千上万个应用, 大部分长尾应用, 即使在双十一零点, 流量也是非常低的。这些应用机器的计算能力其实是有剩余的, 合理的超卖, 可以提升我们机器的资源利用率。但从目前的部署结构来开, 核心应用其实也是存在部分超卖的机器的。这些超卖的机器, 一旦出现资源争抢, 很可能就导致故障。
- 启动时部分机器LOAD飙高问题。部分应用, 部分机器启动的时候, 容易出现个别机器load飙高, 导致这部分机器RT变高。流量调度会根据机器的实际负载情况, 减少这部分机器的流量, 必要时, 迁移这部分刚发布机器的流量到正常机器。
- JVM假死、VM假死等问题
- 受宿主机影响, load飙高问题
- JVM GC 影响RT问题
- 网络抖动导致RT抖动
- 其它单机、局部问题...
把这些影响因子进行收集,整合,作为筛选的依据。
有了这些依据意外,我们需要对线上的异常机器进行筛选。结合离线在线算法,推断出可能异常的机器。并且根据一定的业务规则,来保证筛选出来的机器都是有效机器,并且不会对全局造成影响。
如下图所示:
图七:异常机器自动检测
筛选出了异常机器之后,我们可以把这些机器做下线处理。
最后,涉及到一个异常机器恢复的过程。我们需要根据筛选出异常机器的状态,进行恢复。这里涉及到一定的决策算法。以最小的代价,恢复到正常的状态。如下图所示。
图八:异常机器自动恢复策略
2.3.4 正常流量/刷单流量可隔离
我们需要能够把热点流量和非热点流量隔离出来。防止热点的流量抢占正常流量的处理能力,如下图所示:
图九:正常流量/非正常流量隔离
当大量的用户流量去抢一个热点商品的时候,这些流量的有效性只会由热点商品的库存量决定。而如果无区别的进行控制,则会把正常的流量抢占。我们需要有手段,能够把热点流量和正常流量识别并且隔离出来。不要让无效的流量抢占了正常流量的处理能力。
2.4 流量错峰
用户的请求可能会出现突刺。如下图所示。如果此时要处理所有请求,很可能会导致系统负载过高,影响稳定性。但其实可能后面几秒之内都没有请求,若直接把多余的请求丢掉则没有充分利用系统处理消息的能力。我们希望可以把请求突刺均摊到一段时间内,让系统负载保持在请求处理水位之下的同时尽可能地处理更多消息,从而起到“削峰填谷”的效果,如下图所示:
图十:流量错峰
最理想的效果是把上图中红色的部分推迟到系统空闲的时候处理,如绿色区域所示。
这种场景往往用于消息,异步处理的场景。
我们可以通过匀速器的方式,可以把突然到来的大量请求以匀速的形式均摊,以固定的间隔时间让请求通过,以稳定的速度逐步处理这些请求,起到“削峰填谷”的效果,从而避免流量突刺造成系统负载过高。同时堆积的请求将会排队,逐步进行处理;当请求排队预计超过最大超时时长的时候则直接拒绝,而不是拒绝全部请求。
如下图所示,通过配置匀速模式下请求 QPS 为 5,系统会每 200 ms 处理一条消息,多余的处理任务将排队;同时设置了超时时间为 5 s,预计排队时长超过 5 s 的处理任务将会直接被拒绝。示意图如下图所示:
图十一: 匀速器原理
2.5按需分配
服务提供方用于向外界提供服务,处理各个消费者的调用请求,我们称之为Service Provider.对服务提供方的流量控制可分为服务提供方的自我保护能力和服务提供方对服务消费方的请求分配能力两个维度:
服务提供方的自我保护能力:为了保护 Provider 不被激增的流量拖垮影响稳定性,可以给 Provider 配置QPS 模式的限流,这样当每秒的请求量超过设定的阈值时会自动拒绝多的请求。限流粒度可以是 服务接口 和 服务方法 两种粒度。
根据调用方的需求来分配服务提供方的处理能力也是常见的限流方式。比如有两个服务 A 和 B 都向 Service Provider 发起调用请求,我们希望只对来自服务 B 的请求进行限流
此外,当出现限流的时候,必须要考虑对限流的处理。这个能力我们称之为Fallback.由用户自定义限流之后的处理逻辑。
3 总结
流控是保证服务SLA(服务等级协议)的重要措施,也是业务高峰期故障预防和恢复的有效手段。在实践中,各种流量控制策略需要综合使用才能起到较好的效果,一个好的流控框架 也不许做到不需要重启应用即可在线生效,提升上线服务治理的效率和敏捷性。
文章来源:AlibabaTechQA
开发者社区整理