🌟 后端 | SpringCloud -> 微服务保护



保证服务运行的健壮性,避免级联失败导致的雪崩问题。有时某个服务的业务并发较高,占用过多 Tomcat 连接,这将导致服务所有接口的响应时间增加,延迟变高甚至长时间阻塞,整个服务中与该服务有调用关系的服务都会出现问题。

微服务保护

保护方案

  • 请求限流
  • 线程隔离
  • 服务熔断

这些方案都属于服务降级的方案,会导致服务的体验有所下降,但是通过这些方案,服务的健壮性得到提升。

请求限流,降低了并发上限;线程隔离,降低了可用资源数量;服务熔断,降低了服务的完整度,部分服务变的不可用或弱可用。

请求限流

请求限流,就是限制或控制接口访问的并发流量,避免服务因流量激增而出现故障。

请求限流往往会有一个限流器,数量高低起伏的并发请求曲线,经过限流器就变的非常平稳。这就像是水电站的大坝。

线程隔离

当一个业务接口响应时间长,而且并发高时,就可能耗尽服务器的线程资源,导致服务内的其它接口受到影响。

为了避免某个接口故障或压力过大导致整个服务不可用,我们可以限定每个接口可以使用的资源范围,也就是将其“隔离”起来。

实现方式有两种:

  1. 线程池隔离
    给每个服务调用业务分配一个线程池,利用线程池本身实现隔离效果。
    线程隔离是将不同接口都分配固定的 tomcat 线程资源。使用线程池的方法,tomcat 线程收到了请求,对于堵塞的调用,tomcat 线程自己不处理,而是调用该服务的特定线程池处理。这里的线程池调用是假异步,tomcat 依旧在等待线程池的响应,然后再来写回 response。而关键就是线程池是可以限制数量的,通过限制线程池的数量就可以借此来限制 tomcat 线程的数量,而一旦有超过这个线程池数量的 tomcat 再次请求线程池的服务的时候,线程池会直接 fail,并且释放该 tomcat 线程,达到线程隔离的目的。通过这样控制了一个接口服务只能安排多少个 tomcat 线程来处理,而不至于全部消耗掉。

  2. 信号量隔离
    不创建线程池,而是计数器模式,记录业务使用的线程数量,达到信号量上限时,禁止新的请求

服务熔断

线程隔离虽然避免了雪崩问题,但故障服务(被调用服务)依然会拖慢服务使用方(服务调用方)的接口响应速度。因为可以从以下两点调控:

  • 编写服务降级逻辑:就是服务调用失败后的处理逻辑,根据业务场景,可以抛出异常,也可以返回友好提示或默认数据。
  • 异常统计和熔断:统计服务提供方的异常比例,当比例过高表明该接口会影响到其它服务,应该拒绝调用该接口,而是直接走降级逻辑。

Sentinel

微服务保护技术较多使用 Sentinel

Sentinel 的使用可以分为两个部分:

  • 核心库(Jar包):不依赖任何框架/库,能够运行于 Java 8 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。在项目中引入依赖即可实现服务限流、隔离、熔断等功能。
  • 控制台(Dashboard):Dashboard 主要负责管理推送规则、监控、管理机器信息等。

下载 jar 包启动后,引入依赖:

1
2
3
4
5
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

配置:

1
2
3
4
5
6
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8090 # sentinel 控制台
http-method-specify: true # 开启请求方式前缀 POST/GET/PUT

默认情况下,Sentinel 会监控 SpringMVC 的每一个 Endpoint(接口)

请求限流

在 sentinel 控制台可对指定接口的请求方式作限流配置,可以设置【流控】,使用阈值 QPS ,设置单机阈值,如设为 6 个,意思是该请求接口每秒流量为 6 个上限,如我们测试发送 10 个请求,则会被调控到 6 附近,其余 4 则会被拒绝。

线程隔离

Sentinel 的线程隔离就是基于信号量隔离实现的,而 Hystix 两种都支持,但默认是基于线程池隔离。

限流可以降低服务器压力,尽量减少因并发流量引起的服务故障的概率,但并不能完全避免服务故障。一旦某个服务出现故障,我们必须隔离对这个服务的调用,避免发生雪崩。

默认情况下 SpringBoot 项目的 tomcat 最大线程数是 200,允许的最大连接是 8492,单机测试很难打满。

OpenFeign 配置 Sentinel

application.yml 文件,开启 Feign 的 sentinel 功能:

1
2
3
feign:
sentinel:
enabled: true # 开启feign对sentinel的支持

配置线程隔离

同请求限流的操作一样,在 sentinel 控制台对指定接口的请求方式作限流配置,可以设置【流控】,使用阈值 并发线程数,进行线程数配置,例如配置 5,如果查询商品的接口每秒处理2个请求,则 5 个线程的实际 QPS 在 10 左右,而超出的请求自然会被拒绝。通过测试每秒发送 100 个请求,但实际到达服务时只剩每秒 10 个左右,尽管并发很高,但线程资源被限制了,这样就不会影响到其他接口。

服务熔断

对于超出请求上限的请求,只是不处理、抛出异常对于用户体验很不友好。因此对于这种不太健康的接口,我们应该停止调用,直接走降级逻辑,避免影响到当前服务。

触发限流或熔断后的请求不一定要直接报错,也可以返回一些默认数据或者友好提示,用户体验会更好。
给FeignClient编写失败后的降级逻辑有两种方式:

  • 方式一:FallbackClass,无法对远程调用的异常做处理
  • 方式二:FallbackFactory,可以对远程调用的异常做处理,我们一般选择这种方式。

定义基于 FallbackFactory 处理方法

查询异常时返回空集合,仅展示页面,不展示数据

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
27
28
29
30
31
32
33
package com.hmall.api.client.fallback;

import com.hmall.api.client.ItemClient;
import com.hmall.api.dto.ItemDTO;
import com.hmall.api.dto.OrderDetailDTO;
import com.hmall.common.exception.BizIllegalException;
import com.hmall.common.utils.CollUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;

import java.util.Collection;
import java.util.List;

@Slf4j
public class ItemClientFallback implements FallbackFactory<ItemClient> {
@Override
public ItemClient create(Throwable cause) {
return new ItemClient() {
@Override
public List<ItemDTO> queryItemByIds(Collection<Long> ids) {
log.error("远程调用ItemClient#queryItemByIds方法出现异常,参数:{}", ids, cause);
// 查询购物车允许失败,查询失败,返回空集合
return CollUtils.emptyList();
}

@Override
public void deductStock(List<OrderDetailDTO> items) {
// 库存扣减业务需要触发事务回滚,查询失败,抛出异常
throw new BizIllegalException(cause);
}
};
}
}

在 openFeign 配置里注册 bean

在 hm-api 模块中的 com.hmall.api.config.DefaultFeignConfig 类中将 ItemClientFallback 注册为一个 Bean:

1
2
3
4
5
6
7
@slf4j
public class DefaultFeignConfig {
@Bean
public ItemClientFallback itemClientFallback() {
return new ItemClientFallback();
}
}

在使用到 openFeign 的服务里应用

在 hm-api 模块中的 ItemClient (需调用 item-service)接口中使用ItemClientFallbackFactory

1
2
3
4
5
6
7
8
9
@FeignClient(value = "item-service",
configuration = DefaultFeignConfig.class,
fallbackFactory = ItemClientFallback.class)
public interface ItemClient {
@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);

// ...
}

服务熔断

通过 Sentinel 控制台,对某服务接口进行熔断配置。例如策略选慢调用比例,最大 RT (reponse time) 200ms,比例阈值 0.5,熔断时间 20s,最小请求数 5.

  • RT 超过 200 毫秒的请求调用就是慢调用
  • 统计最近 1000ms 内的最少 5 次请求,如果慢调用比例不低于 0.5,则触发熔断
  • 熔断持续时长 20s

在实际请求时,如触发熔断,调用该接口的请求将会降为 0,但其他服务不受影响。

总结

综上,请求限流是避免流量激增而导致故障,线程隔离是避免因为出现问题的接口影响到其他 ok 的接口,服务熔断则是,这虽然是问题接口,但是总不能给人家直接毙掉,出现服务访问不了时直接停掉访问,展示基础的框架页面,这样就不会太难看。