微服务系统架构可能存在的问题
系统动态扩容
大型的分布式系统,业务一般会有高峰和低谷。就好比居民用电,全年峰谷时段按每日24小时分为 高峰
、 平段
、 低谷
三段各8小时。在系统架构中,设置集群的大小会有成本考虑,一般不会一直按照 高峰
时的规模运行,大多时间里集群规模都不如 高峰时段
大(规模越大,成本越高),那么当 高峰
来临时 ,就会有服务的 动态扩容
。
那么问题来了,扩容一般会带来 滞后性 ,即不能保证瞬时高流量处理的很好,而且可能存在某个业务流量到来时,因为这个业务导致其他业务也无法正常工作。
怎么办呢?限流 啊!通过限制 请求的总量 或者 某段时间内请求的总量 来符合系统的承受能力。
服务雪崩
下面这段
服务雪崩
,在另一篇文章 系统架构演进与Spring Cloud Alibaba简介 也有提及,有兴趣的老铁们可以看一下。
当一个依赖的服务宕机,导致整个应用系统都无法访问的现象就是服务雪崩。
举个例子,服务A和B分别依赖服务C和D,而C和D均依赖服务E:
服务依赖
当这几个服务都正常的时候,调用没有任何问题,当 服务E
出现问题无法正常给 服务C
和 服务D
提供正常的服务时,C和D执行超时重试机制,但是当A和B不断新增请求的时候,C和D对于E的调用请求会 大量积压 ,最终它也会耗尽资源扛不住而倒下的!
由于服务依赖导致部分服务不可用
C和D倒下了,A和B就会不断消耗资源,最终也会宕机下线!直至最后整个应用系统不可访问,服务雪崩。
服务雪崩
而要解决这种微服务架构中可能存在的 雪崩
问题,就需要 熔断 !即当发现 服务C
要去调用有问题的 服务E
时,就直接返回一个给定的默认值,或者直接返回一个有礼貌的错误结果,不去给 服务E
发请求了。
整体的负载超出预设的上限阈值
在高并发的秒杀场景中,抢到秒杀货物的人需要付钱,而系统此时还运行着其他一些比如搜索商品、商品详情、定时任务、评论等等服务,这些服务也是占用着系统资源的,但是他们相对于 支付 服务在此时显得更不重要一些,此时就可以考虑关闭一些不重要的服务。
这种场景就其实就是用到了 服务降级 策略。为了保证重要或基本的服务能正常运行,我们可以将一些 不重要 或 不紧急 的服务或任务进行服务的 延迟使用 或 暂停使用 。
Sentinel 服务限流的利器
前面说了微服务架构体系中可能出现的一些问题,Sentinel 是一种切实可行的解决方案。
Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。
关于 Sentinel 的一些介绍请戳:https://github.com/alibaba/Sentinel/wiki/介绍。这里我不做过多介绍了,下面直接开干上手使用。
本文主要针对 服务限流
进行操作,服务熔断降级
日后再说。
Sentinel 控制台
和 Nacos 一样,Sentinel
也有控制台,而且 Sentinel
的控制台启动相当方便,有两种方式:
- 下载官方提供的
jar
包,使用java -jar
命令启动; - 使用源码构建,需要先下载 Sentinel 源码,然后
mvn clean package
打包。
第二种方式最终也是打成 jar
包,那就直接第一种方式下载 jar
包,然后使用就行了。下载地址:https://github.com/alibaba/Sentinel/releases
将下载下来的 sentinel-dashboard-1.8.3.jar
放到 D:java
(根据实际情况而定)目录下,然后使用如下命令启动:
java -Dserver.port=8080 ^
-Dcsp.sentinel.dashboard.server=localhost:8080 ^
-Dproject.name=sentinel-dashboard ^
-Dsentinel.dashboard.auth.username=sentinel ^
-Dsentinel.dashboard.auth.password=123456 ^
-jar D:javasentinel-dashboard-1.8.3.jar
Tip: 如果在 Linux 环境下启动,则将换行符
^
替换为。
这几个参数的含义:
- server.port :指定
Sentinel
控制台服务的端口号; - csp.sentinel.dashboard.server :向
Sentinel
接入端指定控制台的地址; - project.name :向
Sentinel
指定应用名称; - sentinel.dashboard.auth.username :控制台的登录用户名;
- sentinel.dashboard.auth.password :控制台的登录密码。
如果不加用户名密码参数,那么默认的用户名密码均是 sentinel
。
启动成功后,访问 http://localhost:8080/
,输入用户名:sentinel
,密码:123456
,登录后:
可以看到,控制台已经将 sentinel-dashboard
控制台项目本身监控起来了。
Sentinel 监控注册到 Nacos 中的服务
现在,我们可以把 Sentinel 监控应用到微服务中了。完成如下事情:
- 启动 Nacos 集群 ;
- 启动 Sentinel 控制台;
- 创建
cloud-sentinel-service
服务并注册到 Nacos 集群 中。
Nacos 集群 已经启动:
接下来创建一个 Spring Boot
项目,主要看一下配置文件 application.yml
:
server:
port: 7072
spring:
application:
name: cloud-sentinel-service
cloud:
nacos:
discovery:
server-addr: 192.168.242.112:81
sentinel:
transport:
dashboard: localhost:8080
port: 8719
management:
endpoints:
web:
exposure:
include: '*'
这里的 spring.cloud.sentinel.transport.port
端口配置会在应用对应的机器上启动一个 Http Server ,该 Server 会与 Sentinel 控制台 做交互。
比如 Sentinel 控制台 添加了一个限流规则,会把规则数据 push 给这个 Http Server 接收,Http Server 再将规则注册到 Sentinel 中。
Sentinel 的流控、熔断等策略可以直接在
控制台
进行配置,不需要在代码里进行设计,这也是和 Hystrix 不同的地方。
然后启动该项目,启动后,看一下 Nacos 控制台:
服务已经注册到 Nacos 中。此时,我们来看一下 Sentinel Dashboard 上有没有什么变化:
还是只有 sentinel-dashboard
这一个项目。这是为啥?
这是因为 Sentinel 本身采用的是 懒加载机制 ,所以我们需要首先访问服务对应的接口,Sentinel才能工作。
写一个接口:
代码语言:javascript复制@RestController
public class TestController {
@GetMapping("/test-a")
public String testSentinelA() {
return "hello, sentinel A!";
}
@GetMapping("/test-b")
public String testSentinelB() {
return "hello, sentinel B!";
}
}
下面我们用 JMeter 来访问接口,1秒内开10个线程访问 /test-a
接口:
再来观测 sentinel-dashboard
,发现 cloud-sentinel-service
服务已经被监控到了,如图所示:
我们同时再访问一下 /test-b
接口,链路里面就会显示多了一个 /test-b
:
此时我们频繁快速的访问 /test-a
或者 test-b
,再来查看实时监控的时候,就会出现波动,这也能体现 Sentinel 正在监控 cloud-sentinel-service
这个服务。
那么怎样操作才能实现 Sentinel 对服务进行控流、熔断呢?我们再来看一下控制台的其他菜单功能:
除了实时监控接口的访问情况外,还有 簇点链路
、流控规则
、熔断规则
、热点规则
等功能,下面我们就 流控 、熔断 和 热点 这几个主要功能进行实战演练,搞清楚 Sentinel
是怎样完成服务容错和限流的。
Sentinel 流控规则
除了在 流控规则
菜单下添加规则外,我们还可以直接在 簇点链路
的接口列表中直接对接口添加相应的规则:
现在我们尝试对 /test-a
添加一下 流控
规则:
这个添加 流控规则
的字段中有几个名词,这里是重点,后面将围绕这些东西进行 Demo 实战:
资源名
:唯一名称,默认请求路径。资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。这里的/test-a
接口url就是资源。针对来源
:就是调用者。Sentinel 可以针对调用者进行限流,这里填写微服务名,默认default就是不区分来源。阈值类型
、单机阈值
:这里区分QPS
和并发线程数
。- QPS :Query Per Second,每秒请求的数量,当调用该请求API每秒请求的数量达到配置的
阈值
的时候,进行限流。 - 并发线程数 :当调用该请求API的线程数量达到配置的
阈值
的时候,进行限流。
- QPS :Query Per Second,每秒请求的数量,当调用该请求API每秒请求的数量达到配置的
流控模式
:资源的调用关系,流控模式分为直连
、关联
、链路
。- 直连模式 :请求的API(比如此时的
/test-a
)达到限流条件(配置的阈值)时,直接进行限流; - 关联模式 :当关联的资源达到配置的
阈值
时,就限流自己,比如/test-a
需要读取数据c
,/test-b
则是更改数据c
,他们之间有个争抢资源
的关系,如果放任这两个操作 争抢资源,则争抢
本身带来的开销会降低整体的 吞吐量 。此时对/test-a
设置限流规则流控模式为关联,关联资源为/test-b
, 那么当/test-b
写数据c
的操作过于频繁时,则限制/test-a
读取c
的操作。 - 链路模式 :Sentinel 记录着资源之间的调用链路,这些资源通过调用关系,相互之间构成一棵调用树。
链路模式
就是只关心这颗树上 指定的一条链路 上是否达到阈值而进行限流,不关心其他调用路径上的调用。
- 直连模式 :请求的API(比如此时的
流控效果
:控制的效果,有快速失败
、冷启动
(或称预热
、Warm Up
)、排队等待
。流控效果只在阈值类型为QPS时才有效,阈值类型为线程数的流控效果是如果超出阈值,新的请求会被立即拒绝。- 快速失败 :默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出
FlowException
。 - Warm Up :预热/冷启动方式。根据
codeFactor
(冷加载因子,默认3)的值,请求QPS
从
开始,经
预热时长
逐渐升至设定的QPS
阈值。- 排队等待 :这种方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过。
- 快速失败 :默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出
针对 流控模式
和 流控效果
,下面我们通过 Demo 演示一下。
流控模式——直连
基于 QPS
配置规则如下:
如图我配置了 QPS 的单机阈值为1,意思就是每秒只能请求1次 /test-a
,超过1次就会限流。这里 流控效果
就默认为 快速失败
。下面我们用 Postman
来调用一下,看看效果。
正常访问 /test-a
没有问题:
手速快一点,1秒内多刷新几次,就会出现如下效果:
这个 Demo 演示了 QPS 直接失败的流控效果。
基于并发线程数
再来看一下 并发线程数控制 ,这里需要稍微修改一下 /test-a
接口,让程序睡 0.8 秒,模拟线程做事,这样防止一下运行速度过快无法到线程数控制的效果。
@GetMapping("/test-a")
public String testSentinelA() {
try {
// 睡 0.8s,模拟线程做事
TimeUnit.MILLISECONDS.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hello, sentinel A!";
}
重启服务,然后来配置一下限流规则,设置并发线程数阈值为2:
下面我们用 JMeter
来进行并发测试,测试步骤:
**1. ** 创建线程组,设置并发线程数为5
2. 创建请求连接
**3. ** 添加观察结果树
**4. ** 点击运行,查看结果
从这里可以看出,有3个请求失败了,返回 Blocked by Sentinel (flow limiting)
,只有2个请求成功,这是因为我们设置的并发线程数的阈值为2,而测试同时发出了5个线程进行请求,所以根据限流规则,其余3个线程的请求直接失败。
流控模式——关联
还是先配置流控规则,这次我们就直接按照 并发线程数 来测试,设置阈值为2,关联的资源是 /test-b
。
测试方法是,使用 JMeter
循环并发访问 /test-b
,/test-b
在运行的同时再访问 /test-a
看下效果。
最后看到,频繁访问 /test-b
的同时再访问 /test-a
接口会出现 Blocked by Sentinel (flow limiting)
请求失败,这就是 Sentinel 关联模式
限流起作用的结果。
流控模式—链路
Sentinel 记录了资源之间的调用链路,这些资源通过调用关系,相互之间构成一棵调用树。比如下图的调用关系,请求的 /test-a
和 /test-b
都调用了一个 /common
的资源:
这种请求链路就形成了一颗简单的树。
流控模式先设置为 链路
时, Sentinel 允许只根据某个入口的统计信息对资源限流。
下面代码演示一下怎么 根据调用链路入口限流
。先写一个 /test-a
和 /test-b
都调用的 Sentinel 资源 getUser
:
@Service
public class UserService {
/**
* 标记为 Sentinel 资源
*/
@SentinelResource(value = "getUser")
public String getUser() {
return "行百里者";
}
}
@SentinelResource
注解用来标识资源是否被限流、降级。上述例子上该注解的属性 getUser
表示资源名。
然后稍微修改一下 /test-a
和 /test-b
:
@Autowired
private UserService userService;
@GetMapping("/test-a")
public String testSentinelA() {
// 调用 userService 的 getUser 方法,该方法被标记为 Sentinel 资源
String user = userService.getUser();
return "hello test-a user:" user "!";
}
@GetMapping("/test-b")
public String testSentinelB() {
// 调用 userService 的 getUser 方法,该方法被标记为 Sentinel 资源
String user = userService.getUser();
return "hello test-b user:" user "!";
}
这样一个链路调用树就形成了。
具体如何进行限流呢?这里要注意不要对 /test-a
或者 /test-b
进行限流规则的配置,要给用 @SentinelResource
注解标注的资源 getUser
进行配置限流规则,即当我们用入口资源访问被 SentinelResource
注解标注的资源方法时,当超过阈值就会被限流。
另外,还有一个重要提醒,application.yml
一定要配置如下内容 spring.cloud.sentinel.web-context-unify=false
:
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080
port: 8719
# https://github.com/alibaba/Sentinel/issues/1213
web-context-unify: false
否则通过 链路
控流将不生效,详见 Sentinel GitHub issue:https://github.com/alibaba/Sentinel/issues/1213 。
配置如下:
如上配置应实现的效果为,当每秒请求 /test-b
(调用了资源 getUser
)大于1次时,将进行限流,因为配置的入口资源是 /test-b
,所以不关心 /test-a
的请求情况不对其进行流控。
验证一下,1秒内执行多个请求:
而 /test-a
不受影响:
流控效果——Warm Up(预热)
前面我们演示的流控模式:直连
、关联
和 链路
,其流控效果都是选择默认的 快速失败
(直接失败)。在某些场景下,可能更适合选择其他的流控效果,比如秒杀系统中会有 预热
这样的流控设置,为了防止秒杀瞬间造成系统崩溃。
前面提到,Warm Up
就是:根据 codeFactor
(冷加载因子,默认3)的值,请求 QPS
从
开始,经 预热时长
逐渐升至设定的 QPS
阈值。
比如设置如下:
设置了单机阈值为10,预热时长5,这个流控效果就是:请求资源 /test-c
的 QPS
从单机阈值 3 (10/3)开始,经过 预热时长
5 秒达到阈值 10 。也就是说,最开始请求 /test-c
的最大 QPS 是 3 ,单机阈值开始增长,经过 5 秒的时间请求的最大 QPS 是10 。
演示一下:
保持快速刷新调用 /test-c
,可以发现开始有通过和拒绝的 QPS ,最后就没有拒绝的了(5秒后到达阈值10后)。
这样,流控的 预热
效果就达到了。
为什么要这样做呢?举个栗子,冬天开车启动车子之前,一般先把发动机打开,先暖暖车,而不是一脚油门把车子速度提起来,这样容易坏车。同理,系统虽然能够承受最大阈值,但是如果突然之间有非常大的访问量,也有可能在那一个瞬时扛不住,所以需要 预热
。
以上,本地导航结束,下次水文:Sentinel 的熔断降级策略。