上一篇:SpringCloudAlibaba之Integrated Example示例工程源码解读二 - 掘金 (juejin.cn)
综述
本文分析SpringCloudAlibaba源码示例工程Integrated Example中的商品点赞场景。
该示例演示了高并发突发高峰流量场景下,如何进行流量控制,削峰填谷。
架构
- HTTP请求由基于Spring Cloud Gateway组件的API网关统一接入。
- Spring Cloud Gateway集成Nacos实现动态路由配置。
- Spring Cloud Gateway集成Sentinel实现流量控制(Sentinel流量控制配置没有集成Nacos)。
- 点赞服务分生产端服务和消费端服务,使用RocketMQ连接实现削峰填谷的作用。
此示例中对于突发流量的处理,有两个层面: 1.在流量入口(API网关)进行限流,防止服务被突发流量打死。 2.对核心业务(点赞数据落库)异步处理,快速返回客户端响应和释放系统资源,提高用户体验和系统可用性。
由于是简单的示例,业务处理过程并不复杂,处理速度很快,所以为了更好的演示RocketMQ削峰填谷的功能,系统开辟了一条单独的通道,在网关处不进行限流,直接将流量打到后端的点赞生产者服务上。
查看Nacos中的网关路由配置(integrated-gateway.yaml),对于点赞服务(integrated-provider)其配置了两个API入口“/praise/sentinel”和“/praise/rocketmq”:
spring:
cloud:
gateway:
routes:
#为商品点赞(不限流)
- id: praiseItemRocketMQ
uri: lb://integrated-provider
predicates:
- Path=/praise/rocketmq
#为商品点赞(限流)
- id: praiseItemSentinel
uri: lb://integrated-provider
predicates:
- Path=/praise/sentinel
- “/praise/sentinel” 演示基于Sentinel组件的并发限流
- “/praise/rocketmq” 演示基于RocketMQ组件的削峰填谷
这两API对应integrated-provider服务的同一个接口。
@RestController
@RequestMapping("/praise")
public class PraiseController {
...
@GetMapping({ "/rocketmq", "/sentinel" })
public boolean praise(@RequestParam Integer itemId) {
...
}
}
它们之间的区别在于从网关进入时,“/praise/sentinel”接口被sentinel限制了流量速度。
基于Sentinel组件的并发限流
在高并发的场景中,系统的流量可能会突然迅速暴增,导致系统过载瘫痪。使用Sentinel可以对流量进行控制,防止系统雪崩。
下面来看看示例工程中网关是如何利用sentinel组件限流的。
首先需要在Pom.xml中引入sentinel依赖组件:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
然后注入对应的 SentinelGatewayFilter
实例以及 SentinelGatewayBlockExceptionHandler
实例即可。(若使用了 Spring Cloud Alibaba Sentinel,则只需按照文档进行配置,无需自己添加 Configuration).
@Configuration
public class GatewayConfig {
...
// 注册Sentinel网关过虑器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new SentinelGatewayBlockExceptionHandler(viewResolvers,
serverCodecConfigurer);
}
...
}
在java配置类(@Configuration)中设置限流规则(实际应用建议在配置中心保存配置数据),针对id为praiseItemSentinel的路由规则进行限流,每秒5次。
@Configuration
public class GatewayConfig {
...
// 初始化Sentinel限流规则
// 针对id为praiseItemSentinel的路由规则进行限流,每秒5次。
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(
new GatewayFlowRule("praiseItemSentinel").setCount(5).setIntervalSec(1));
GatewayRuleManager.loadRules(rules);
}
...
}
流量控制规则通过GatewayFlowRule类来配置,主要配置参数说明如下:
public class GatewayFlowRule {
//资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称。
private String resource;
//规则是针对 API Gateway 的 route(`RESOURCE_MODE_ROUTE_ID`)还是用户在 Sentinel 中定义的 API 分组(`RESOURCE_MODE_CUSTOM_API_NAME`),默认是 route
private int resourceMode = 0;
//限流指标维度,同限流规则的 `grade` 字段
private int grade = 1;
//限流阈值
private double count;
//统计时间窗口,单位是秒,默认是 1 秒。
private long intervalSec = 1L;
//流量整形的控制效果,同限流规则的 `controlBehavior` 字段,目前支持快速失败和匀速排队两种模式,默认是快速失败。
private int controlBehavior = 0;
//应对突发请求时额外允许的请求数目。
private int burst;
//匀速排队模式下的最长排队时间,单位是毫秒,仅在匀速排队模式下生效。
private int maxQueueingTimeoutMs = 500;
...
}
最后示例代码中添加了一个接口因限流而阻塞时的公共处理类,返回统一的响应消息。
// 设置请求被限流阻塞时的处理类
// 此处代码实现:请求限流时,直接返回“此接口被限流了”的消息给客户端。
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange,
Throwable throwable) {
return ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject("此接口被限流了"));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
基于RocketMQ组件的削峰填谷
削峰填谷是一种常见的系统优化策略,主要是通过平滑系统的负载,防止系统过载,保证服务的可用性,提高系统性能和用户体验,以达到优化系统的目的。
前面说到的限流就是一种削峰的手段,它可以让突发的瞬时流量匀速的进入系统。而消息队列则是另一种常见的削峰手段,通过消息队列可以将大量并发请求进行缓冲,然后由消费端按照指定的速率慢慢处理,这样既能保证服务的可用性,又能有效的避免过载流量拖死核心服务。
该示例程序使用RocketMQ做为消息中间件实现削峰填谷。RocketMQ集群能承载数十万甚至上百万的TPS,可以轻松应对各种大促秒杀场景。
点赞服务生产者
Maven引入spring-cloud-starter-stream-rocketmq依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rocketmq</artifactId>
</dependency>
Nacos配置中心的配置(integrated-provider.yaml):
spring:
cloud:
stream:
bindings:
praise-output:
destination: PRAISE-TOPIC-01
content-type: application/json
rocketmq:
binder:
name-server: rocketmq:9876
bindings:
praise-output:
producer:
group: test
示例配置中采用了Spring Cloud Stream框架。
Spring Cloud Stream是一个用于构建基于消息的微服务应用框架。
Spring Cloud Stream 内部有两个概念:Binder 和 Binding。
- Binder: 跟外部消息中间件集成的组件,用来创建 Binding,各消息中间件都有自己的 Binder 实现。
比如 Kafka
的实现 KafkaMessageChannelBinder
,RabbitMQ
的实现 RabbitMessageChannelBinder
以及 RocketMQ
的实现 RocketMQMessageChannelBinder
。
- Binding: 包括 Input Binding 和 Output Binding。
Binding 在消息中间件与应用程序提供的 Provider 和 Consumer 之间提供了一个桥梁,实现了开发者只需使用应用程序的 Provider 或 Consumer 生产或消费数据即可,屏蔽了开发者与底层消息中间件的接触。
下图是 Spring Cloud Stream 的架构设计。
生产者使用StreamBridge发送消息。StreamBridge
是 Spring Cloud Stream 3.0 之后引入的一个新特性,用于动态地创建并发送消息到绑定器。其主要目标是使应用程序能够动态地(即运行时)向任何可能的绑定器发送消息,而无需在启动时预先创建这些绑定器。
@RestController
@RequestMapping("/praise")
public class PraiseController {
private static final String BINDING_NAME = "praise-output";
@Autowired
private StreamBridge streamBridge;
@GetMapping({ "/rocketmq", "/sentinel" })
public boolean praise(@RequestParam Integer itemId) {
PraiseMessage message = new PraiseMessage();
message.setItemId(itemId);
Message<PraiseMessage> praiseMessage = MessageBuilder.withPayload(message)
.build();
return streamBridge.send(BINDING_NAME, praiseMessage);
}
}
点赞服务消费者
Maven引入spring-cloud-starter-stream-rocketmq依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rocketmq</artifactId>
</dependency>
Nacos配置中心的配置(integrated-consumer.yaml):
spring:
cloud:
stream:
bindings:
praise-input:
destination: PRAISE-TOPIC-01
content-type: application/json
group: praise-consumer-group-PRAISE-TOPIC-01
rocketmq:
binder:
name-server: rocketmq:9876
bindings:
praise-input:
consumer:
# 每4秒消费4条消息
pullInterval: 4000
pullBatchSize: 4
消费端消息代码:
@Configuration
public class ListenerAutoConfiguration {
@Bean
public Consumer<Message<PraiseMessage>> consumer(PraiseService praiseService) {
return msg -> {
praiseService.praiseItem(msg.getPayload().getItemId());
};
}
}
此代码定义了一个 Consumer<Message<PraiseMessage>>
类型的 Bean,这个 Bean 是一个函数,接收一个 Message<PraiseMessage>
对象,并对其进行处理。
Message
接口是Spring提供的一个消息封装接口,其中的getPayload
方法用于获取消息的内容,这个内容就是PraiseMessage
对象。
在这个函数式模型中,Spring Cloud Stream会自动将这个函数注册为消息处理器,监听指定的输入绑定,并在接收到消息时调用这个函数。
这个模型的优点是它更简洁,不需要使用 @StreamListener
或 @ServiceActivator
注解,也不需要定义接口和使用 @EnableBinding
注解。
在这种情况下,Spring Cloud Stream 会尝试将函数与所有配置的输入绑定进行匹配。如果函数的参数类型与某个绑定的消息类型相匹配,那么这个函数就会被注册为这个绑定的消息处理器。显然Message与praise-input绑定的消息类型匹配。
示例代码所使用的函数式编程来收发消息的技术,详细介绍可以查看官网说明