- 引入sentinel的作用
由于近期商城需要做推广活动,为了防止流量激增导致压垮服务,因此需要对请求进行限流。经过一番搜索,最终锁定了sentinel,因为它是阿里的产品,经过多次双十一的检验,而且还有图形界面供用户设置、观察指标等。 - spring cloud集成sentinel
由于我们使用的框架是spring cloud而不是cloud alibaba,所以在集成时遇到了不少的坑,很多从网上搜索的案例都模糊不清,最终还是通过官方文档(版本说明) 进行配置才搞定的。我使用的是Spring Cloud Finchley版本,因此配置文件如下在这里插入代码片
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.0.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
- 网关限流
我们使用的网关是zuul,同样参考官方文档网关限流,增加配置,然后在启动服务是在VM中增加参数-Dcsp.sentinel.app.type=1(告诉sentinel这是一个网关服务,网关服务和普通服务的功能不同),网关服务多出一个“api管理”,该选型功能很强大也很实用,能够根据请求参数、请求路径、请求ip,请求header等进行限流、降级操作。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
- 规则持久化
sentinel默认是保存在内存中的,因此每次重启服务就会导致规则丢失(重启sentinel dashboard会导致规则丢失,但是限流不会失效;重启sentinel客户端会导致限流失效,但dashboard规则不会丢失,通过dashboard源码可以知道,sentinel是想规则保存在内存,同时通过rpc将规则发送到客户端,由客户端自行限流)。
sentinel官方提供了多种持久化扩展(ZooKeeper,Nacos,File,Apollo,Redis…),这里我选择使用redis进行持久化,网上很多针对redis持久化的案例,但是大多出自一处,都是通过修改dashboard源码将规则持久化到redis。但是这种方式只是解决了保存的问题,没有解决读取的问题。上面我说过真正的限流是在各个客户端自行实现的,dashboard只是提供一个设置的入口而已,每个客户端在重启时需要将redis中涉及到自己的规则读取到内存中,因此这里还需要针对客户端做修改,增加如下配置
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-redis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-api-gateway-adapter-common</artifactId>
</dependency>
- 将持久化从dashboard移到客户端
网上很多很的方案都是在dashboard修改redis持久化配置,我按照网上的方案对dashboard源码进行了修改,确实可以把规则保存到redis中,但是网上的方案都没有涉及客户端服务启动读入规则。当我想要在客户端重启时读入dashboard写入的规则时,发现dashboard和客户端的实体类字段进行有差别,从而导致规则无法读入。因此我转变思维,将持久化操作也放在客户端,由客户端进行持久化、由客户端进行读入,这样就不会出现不兼容的问题。实现WritableDataSurce接口,从而让规则能够持久化到redis
public class RedisWritableDataSource<T> implements WritableDataSource<T> {
private final RedisClient redisClient;
private String key;
public RedisWritableDataSource(RedisConnectionConfig config, String key){
AssertUtil.notNull(config, "Redis connection config can not be null");
AssertUtil.notEmpty(key, "Redis ruleKey can not be empty");
this.redisClient = this.getRedisClient(config);
this.key = key;
}
private RedisClient getRedisClient(RedisConnectionConfig connectionConfig) {
if (connectionConfig.getRedisSentinels().size() == 0) {
RecordLog.info("[RedisDataSource] Creating stand-alone mode Redis client", new Object[0]);
return this.getRedisStandaloneClient(connectionConfig);
} else {
RecordLog.info("[RedisDataSource] Creating Redis Sentinel mode Redis client", new Object[0]);
return this.getRedisSentinelClient(connectionConfig);
}
}
private RedisClient getRedisStandaloneClient(RedisConnectionConfig connectionConfig) {
char[] password = connectionConfig.getPassword();
String clientName = connectionConfig.getClientName();
RedisURI.Builder redisUriBuilder = RedisURI.builder();
redisUriBuilder.withHost(connectionConfig.getHost()).withPort(connectionConfig.getPort()).withDatabase(connectionConfig.getDatabase()).withTimeout(Duration.ofMillis(connectionConfig.getTimeout()));
if (password != null) {
redisUriBuilder.withPassword(connectionConfig.getPassword());
}
if (StringUtil.isNotEmpty(connectionConfig.getClientName())) {
redisUriBuilder.withClientName(clientName);
}
return RedisClient.create(redisUriBuilder.build());
}
private RedisClient getRedisSentinelClient(RedisConnectionConfig connectionConfig) {
char[] password = connectionConfig.getPassword();
String clientName = connectionConfig.getClientName();
RedisURI.Builder sentinelRedisUriBuilder = RedisURI.builder();
Iterator var5 = connectionConfig.getRedisSentinels().iterator();
while(var5.hasNext()) {
RedisConnectionConfig config = (RedisConnectionConfig)var5.next();
sentinelRedisUriBuilder.withSentinel(config.getHost(), config.getPort());
}
if (password != null) {
sentinelRedisUriBuilder.withPassword(connectionConfig.getPassword());
}
if (StringUtil.isNotEmpty(connectionConfig.getClientName())) {
sentinelRedisUriBuilder.withClientName(clientName);
}
sentinelRedisUriBuilder.withSentinelMasterId(connectionConfig.getRedisSentinelMasterId()).withTimeout(connectionConfig.getTimeout(), TimeUnit.MILLISECONDS);
return RedisClient.create(sentinelRedisUriBuilder.build());
}
@Override
public void write(T t) throws Exception {
RedisCommands<String, String> stringRedisCommands = this.redisClient.connect().sync();
stringRedisCommands.set(key, JSON.toJSONString(t, SerializerFeature.WriteClassName));
}
@Override
public void close() throws Exception {
this.redisClient.shutdown();
}
}
- 客户单启动时读取规则配置
@Configuration
public class GatewayRuleConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private Integer port;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.cloud.sentinel.transport.port}")
private String sentinelPort;
//限流规则key前缀
public final String RULE_FLOW = "sentinel_rule_flow_";
public final String RULE_FLOW_CHANNEL = "sentinel_rule_flow_channel";
//降级规则key前缀
public final String RULE_DEGRADE = "sentinel_rule_degrade_";
public final String RULE_DEGRADE_CHANNEL = "sentinel_rule_degrade_channel";
//系统规则key前缀
public final String RULE_SYSTEM = "sentinel_rule_system_";
public final String RULE_SYSTEM_CHANNEL = "sentinel_rule_system_channel";
//API分组
public final String RULE_API = "sentinel_rule_api_";
public final String RULE_API_CHANNEL = "sentinel_rule_api_channel";
//gateway规则
public final String RULE_GATEWAY_FLOW = "sentinel_rule_gateway_flow_";
public final String RULE_GATEWAY_FLOW_CHANNEL = "sentinel_rule_gateway_flow_channel";
@PostConstruct
public void doInit() throws Exception {
RedisConnectionConfig config = RedisConnectionConfig.builder()
.withHost(host)
.withPort(port)
.withPassword(password)
.build();
InetAddress host = InetAddress.getLocalHost();
initFlowRule(config, host);
initDegradeRule(config, host);
initSystemRule(config, host);
initApiDefinition(config, host);
initGatewayFlowRule(config, host);
}
private void initFlowRule(RedisConnectionConfig config, InetAddress host){
Converter<String, List<FlowRule>> parser = source -> JSON.parseObject(source,new TypeReference<List<FlowRule>>() {});
ReadableDataSource<String, List<FlowRule>> redisDataSource = new RedisDataSource<>(config, RULE_FLOW+ SentinelConfig.getAppName()+":"+host.getHostAddress()+":"+sentinelPort, RULE_FLOW_CHANNEL, parser);
FlowRuleManager.register2Property(redisDataSource.getProperty());
WritableDataSource<List<FlowRule>> writableDataSource = new RedisWritableDataSource<>(config, RULE_FLOW+ SentinelConfig.getAppName()+":"+host.getHostAddress()+":"+sentinelPort);
WritableDataSourceRegistry.registerFlowDataSource(writableDataSource);
}
private void initDegradeRule(RedisConnectionConfig config, InetAddress host){
Converter<String, List<DegradeRule>> parser1 = source -> JSON.parseObject(source,new TypeReference<List<DegradeRule>>() {});
ReadableDataSource<String, List<DegradeRule>> redisDataSource1 = new RedisDataSource<>(config, RULE_DEGRADE+SentinelConfig.getAppName()+":"+host.getHostAddress()+":"+sentinelPort, RULE_DEGRADE_CHANNEL, parser1);
DegradeRuleManager.register2Property(redisDataSource1.getProperty());
WritableDataSource<List<DegradeRule>> writableDataSource = new RedisWritableDataSource<>(config, RULE_DEGRADE+ SentinelConfig.getAppName()+":"+host.getHostAddress()+":"+sentinelPort);
WritableDataSourceRegistry.registerDegradeDataSource(writableDataSource);
}
private void initSystemRule(RedisConnectionConfig config, InetAddress host){
Converter<String, List<SystemRule>> parser2 = source -> JSON.parseObject(source,new TypeReference<List<SystemRule>>() {});
ReadableDataSource<String, List<SystemRule>> redisDataSource2 = new RedisDataSource<>(config, RULE_SYSTEM+SentinelConfig.getAppName()+":"+host.getHostAddress()+":"+sentinelPort, RULE_SYSTEM_CHANNEL, parser2);
SystemRuleManager.register2Property(redisDataSource2.getProperty());
WritableDataSource<List<SystemRule>> writableDataSource = new RedisWritableDataSource<>(config, RULE_SYSTEM+ SentinelConfig.getAppName()+":"+host.getHostAddress()+":"+sentinelPort);
WritableDataSourceRegistry.registerSystemDataSource(writableDataSource);
}
private void initApiDefinition(RedisConnectionConfig config, InetAddress host) throws Exception {
Converter<String, Set<ApiDefinition>> parser3 = source -> JSON.parseObject(source,new TypeReference<Set<ApiDefinition>>() {});
ReadableDataSource<String, Set<ApiDefinition>> redisDataSource3 = new RedisDataSource<>(config, RULE_API+SentinelConfig.getAppName()+":"+host.getHostAddress()+":"+sentinelPort, RULE_API_CHANNEL, parser3);
GatewayApiDefinitionManager.register2Property(redisDataSource3.getProperty());
WritableDataSource<Set<ApiDefinition>> writableDataSource = new RedisWritableDataSource<>(config, RULE_API+SentinelConfig.getAppName()+":"+host.getHostAddress()+":"+sentinelPort);
UpdateGatewayApiDefinitionGroupCommandHandler.setWritableDataSource(writableDataSource);
}
private void initGatewayFlowRule(RedisConnectionConfig config, InetAddress host) throws Exception {
Converter<String, Set<GatewayFlowRule>> parser4 = source -> JSON.parseObject(source,new TypeReference<Set<GatewayFlowRule>>() {});
ReadableDataSource<String, Set<GatewayFlowRule>> redisDataSource4 = new RedisDataSource<>(config, RULE_GATEWAY_FLOW+SentinelConfig.getAppName()+":"+host.getHostAddress()+":"+sentinelPort, RULE_GATEWAY_FLOW_CHANNEL, parser4);
GatewayRuleManager.register2Property(redisDataSource4.getProperty());
WritableDataSource<Set<GatewayFlowRule>> writableDataSource = new RedisWritableDataSource<>(config, RULE_GATEWAY_FLOW+SentinelConfig.getAppName()+":"+host.getHostAddress()+":"+sentinelPort);
UpdateGatewayRuleCommandHandler.setWritableDataSource(writableDataSource);
}
}
这样就形成了一个闭环,既能够持久化,又能读入内存,修改即时生效。