在使用了微服务架构以后,整体的业务流程被拆分成小的微服务,并组合在一起对外提供服务,微服务之间使用轻量级的网络协议通信,通常是RESTful风格的远程调用。由于服务与服务的调用不再是进程内的调用,而是通过网络进行的远程调用,众所周知,网络通信是不稳定、不可靠的,一个服务依赖的服务可能出错、超时或者宕机,如果没有及时发现和隔离问题,或者在设计中没有考虑如何应对这样的问题,那么很可能在短时间内服务的线程池中的线程被用满、资源耗尽,导致出现雪崩效应。本节针对微服务架构中可能遇到的这些问题,讲解应该采取哪些措施和方案来解决。
1. 舱壁隔离模式
这里用航船的设计比喻舱壁隔离模式,若一艘航船遇到了意外事故,其中一个船舱进了水,则我们希望这个船舱和其他船舱是隔离的,希望其他船舱可以不进水,不受影响。在微服务架构中,这主要体现在如下两个方面。
1)微服务容器分组
笔者所在的支付平台应用了微服务,将微服务的每个节点的服务池分为三组:准生产环境、灰度环境和生产环境。准生产环境供内侧使用;灰度环境会跑一些普通商户的流量;大部分生产流量和VIP商户的流量则跑在生产环境中。这样,在一次比较大的重构过程中,我们就可以充分利用灰度环境的隔离性进行预验证,用普通商户的流量验证重构没有问题后,再上生产环境。
另外一个案例是一些社交平台将名人的自媒体流量全部路由到服务的核心池子中,而将普通用户的流量路由到另外一个服务池子中,有效隔离了普通用户和重要用户的负载。
其服务分组如下图所示。
2)线程池隔离
在微服务架构实施的过程中,我们不一定将每个服务拆分到微小的力度,这取决于职能团队和财务的状况,我们一般会将同一类功能划分在一个微服务中,尽量避免微服务过细而导致成本增加,适可而止。
这样就会导致多个功能混合部署在一个微服务实例中,这些微服务的不同功能通常使用同一个线程池,导致一个功能流量增加时耗尽线程池的线程,而阻塞其他功能的服务。
线程池隔离如下图所示。
2. 熔断模式
可以用家里的电路保险开关来比喻熔断模式,如果家里的用电量过大,则电路保险开关就会自动跳闸,这时需要人工找到用电量过大的电器来解决问题,然后打开电路保险开关。在这个过程中,电路保险开关起到保护整个家庭电路系统的作用。
对于微服务系统也一样,当服务的输入负载迅速增加时,如果没有有效的措施对负载进行熔断,则会使服务迅速被压垮,服务被压垮会导致依赖的服务都被压垮,出现雪崩效应,因此,可通过模拟家庭的电路保险开关,在微服务架构中实现熔断模式。
微服务化的熔断模式的状态流转如下图所示。
3. 限流模式
服务的容量和性能是有限的,在第3章中会介绍如何在架构设计过程中评估服务的最大性能和容量,然而,即使我们在设计阶段考虑到了性能压力的问题,并从设计和部署上解决了这些问题,但是业务量是随着时间的推移而增长的,突然上量对于一个飞速发展的平台来说是很常见的事情。
针对服务突然上量,我们必须有限流机制,限流机制一般会控制访问的并发量,例如每秒允许处理的并发用户数及查询量、请求量等。
有如下几种主流的方法实现限流。
1)计数器
通过原子变量计算单位时间内的访问次数,如果超出某个阈值,则拒绝后续的请求,等到下一个单位时间再重新计数。
在计数器的实现方法中通常定义了一个循环数组(见下图),例如:定义5个元素的环形数组,计数周期为1s,可以记录4s内的访问量,其中有1个元素为当前时间点的标志,通常来说每秒程序都会将前面3s的访问量打印到日志,供统计分析。
在上图中,当前时间为1 000 000 002s,对应的计数器在第3个元素,下标为2,当前请求是在这个时间周期内的第1个访问请求,程序首先需要对后一个元素即第4个元素,也就是下标为3的元素清零;在1 000 000 002s内,任何一个请求如果发现下标为3的元素不为0,则都会将原子变量3清零,并记录清零的时间。
这时程序可以对第3个元素即下标为2的元素,进行累加并判断是否达到阈值,如果达到阈值,则拒绝请求,否则请求通过;同时,打印本次及之前3秒的数据访问量,打印结果如下。
当前:1次,前1s:302次,前2s:201次,前3s:518次
然而,如果当前秒一直没有请求量,下一秒的计数器始终不能清零,则下一秒的请求到达后要首先清零再使用,并更新清零时间。
在下一秒的请求到达后,若检查到当前秒对应的原子变量计数器不为0,而且最后的清零时间不是上一秒,则先对当前秒的计数器清零,再进行累加操作,这避免发生上一秒无请求的场景,或者上一秒的请求由于线程调度延迟而没有清零下一秒的场景,后面这种场景发生的概率较小。
另外一种实现计数器的简单方法是单独启动一个线程,每隔一定的时间间隔执行对下一秒的原子变量计数器清零操作,这个时间间隔必须小于计数时间间隔。
2)令牌筒
令牌筒是一个流行的实现限流的技术方案,它通过一个线程在单位时间内生产固定数量的令牌,然后把令牌放入队列,每次请求调用需要从桶中拿取一个令牌,拿到令牌后才有资格执行请求调用,否则只能等待拿到令牌再执行,或者直接丢弃。
令牌筒的结构如下图所示。
3)信号量
限流类似于生活中的漏洞,无论倒入多少油,下面有漏管的流量是有限的,实际上我们在应用层使用的信号量也可以实现限流。
使用信号量的示例如下:
public class SemaphoreExample {
private ExecutorService exec = Executors.newCachedThreadPool();
public static void main(String[] args) {
final Semaphore sem = new Semaphore(5);
for (int index = 0; index < 20; index++) {
Runnable run = new Runnable() {
public void run() {
try {
// 获得许可
sem.acquire();
// 同时只有5个请求可以到达这里
Thread.sleep((long) (Math.random()));
// 释放许可
sem.release();
System.out.println("剩余许可:" + sem.availablePermits());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
exec.execute(run);
}
exec.shutdown();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
4. 失效转移模式
若微服务架构中发生了熔断和限流,则该如何处理被拒绝的请求呢?解决这个问题的模式叫作失效转移模式,通常分为下面几种。
- 采用快速失败的策略,直接返回使用方错误,让使用方知道发生了问题并自行决定后续处理。
- 是否有备份服务,如果有备份服务,则迅速切换到备份服务。
- 失败的服务有可能是某台机器有问题,而不是所有机器有问题,例如OOM问题,在这种情况下适合使用failover策略,采用重试的方法来解决,但是这种方法要求服务提供者的服务实现了幂等性。