随着微服务的流行,服务与服务之间的稳定性变得越来越重要。Sentinel以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel具有如下特性:
Sentinel控制台是一个轻量级的控制台应用,它可用于实时查看单机资源监控及集群资源汇总,并提供了一系列的规则管理功能,如流控规则、降级规则、热点规则等。
从官网下载Sentinel,这里使用的是sentinel-dashboard-1.7.2.jar文件,下载地址:
https://github.com/alibaba/Sentinel/releases
下载完成后再命令行输入以下命令运行Sentinel控制台:
java -jar sentinel-dashboard-1.7.2.jar
Sentinel控制台默认运行在8080端口上,登录账号和密码均为sentinel,通过如下地址进行访问:http://localhost:8080
Sentinel控制台可以查看单台机器的实时监控数据。
这里我们创建一个springboot-sentinel模块,用于演示Sentinel的熔断与限流功能。
在pom.xml中添加相关依赖,这里我们使用Nacos作为注册中心,所以需要同时引入Nacos的依赖。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/>
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.RELEASE</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.1.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
在application.yml中添加相关配置,主要是配置了Nacos和Sentinel控制台地址:
server:
port: 8401
spring:
application:
name: springboot-sentinel
cloud:
nacos:
discovery:
namespace: dev
server-addr: 192.168.0.130:8848
sentinel:
transport:
# 配置Sentinel dashborad地址
dashboard: http://localhost:8080
port: 8719
datasource:
- nacos:
server-addr: 192.168.0.130:8848
data-id: ${spring.application.name}-flow-rules
group-id: DEFAULT_GROUP
data-type: json
rule-type: flow
service-url:
user-service: http://192.168.0.130:8501
management:
endpoints:
web:
exposure:
include: '*'
feign:
sentinel:
enabled: true
Sentinel Starter 默认为所有的 HTTP 服务提供了限流埋点,我们也可以通过使用@SentinelResource来自定义一些限流行为。
用于测试熔断和限流功能。
package com.lee.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.lee.handle.CustomBlockHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/rateLimit")
public class RateLimitController {
/**
* 按资源名称限流,需要指定限流处理逻辑
*
* @return
*/
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandler = "handleException")
public Map<String,Object> byResource() {
Map<String,Object> result = new HashMap<>();
result.put("name","按资源名称限流");
result.put("code",200);
return result ;
}
/**
* 按url限流,有默认的限流处理逻辑
*
* @return
*/
@GetMapping("byUrl")
@SentinelResource(value = "byUrl", blockHandler = "handleException")
public Map<String,Object> byUrl() {
Map<String,Object> result = new HashMap<>();
result.put("name","按url限流");
result.put("code",200);
return result ;
}
public Map<String,Object> handleException(BlockException exception) {
Map<String,Object> result = new HashMap<>();
result.put("name",exception.getClass().getCanonicalName());
result.put("code",200);
return result ;
}
@GetMapping("/customBlockHandler")
@SentinelResource(value = "customBlockHandler", blockHandler = "handleException", blockHandlerClass = CustomBlockHandler.class)
public Map<String,Object> blockHandler() {
Map<String,Object> result = new HashMap<>();
result.put("name","限流成功");
result.put("code",200);
return result ;
}
}
我们可以根据@SentinelResource注解中定义的value(资源名称)来进行限流操作,但是需要指定限流处理逻辑。
流控规则可以在Sentinel控制台进行配置,由于我们使用了Nacos注册中心,我们先启动Nacos和sentinel-service;
由于Sentinel采用的懒加载规则,需要我们先访问下接口,Sentinel控制台中才会有对应服务信息,我们先访问下该接口:
http://localhost:8401/rateLimit/byUrl
在Sentinel控制台配置流控规则,根据@SentinelResource注解的value值:
快速访问上面的接口,可以发现返回了自己定义的限流处理信息:
我们还可以通过访问的URL来限流,会返回默认的限流处理信息。
多次访问该接口,会返回默认的限流处理结果:http://localhost:8401/rateLimit/byUrl
我们可以自定义通用的限流处理逻辑,然后在@SentinelResource中指定。
创建 CustomBlockHandler 类用于自定义限流处理逻辑:
package com.lee.handle;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import java.util.HashMap;
import java.util.Map;
public class CustomBlockHandler {
public static Map<String,Object> handleException(BlockException exception) {
Map<String,Object> result = new HashMap<>();
result.put("name","自定义限流信息");
result.put("code",200);
return result ;
}
}
在RateLimitController中自定义限流处理逻辑:
package com.lee.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.lee.handle.CustomBlockHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/rateLimit")
public class RateLimitController {
@GetMapping("/customBlockHandler")
@SentinelResource(value = "customBlockHandler", blockHandler = "handleException", blockHandlerClass = CustomBlockHandler.class)
public Map<String,Object> blockHandler() {
Map<String,Object> result = new HashMap<>();
result.put("name","限流成功");
result.put("code",200);
return result ;
}
}
Sentinel 支持对服务间调用进行保护,对故障应用进行熔断操作,这里我们使用RestTemplate来调用springboot-provider服务所提供的接口来演示下该功能。
springboot-provider服务提供的接口非常简单:
package com.lee.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class TestController {
@GetMapping("/user/{id}")
public Map<String,Object> getInfo(@PathVariable(value = "id") String id) {
if("1".equals(id)) {
throw new RuntimeException("remote func is fail");
}
Map<String,Object> result = new HashMap<>();
result.put("reqData",id);
result.put("code","200");
return result ;
}
}
创建RestTemplate
package com.lee.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class CusConfig {
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
添加 CircleBreakerController 类,定义对springboot-provider提供接口的调用:
package com.lee.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/breaker")
public class CircleBreakerController {
private static final Logger LOGGER = LoggerFactory.getLogger(CircleBreakerController.class);
@Autowired
private RestTemplate restTemplate;
@Value("${service-url.user-service}")
private String userServiceUrl;
@GetMapping("/fallback/{id}")
@SentinelResource(value = "fallback", fallback = "handleFallback")
public Map<String,Object> fallback(@PathVariable Long id) {
Map<String,Object> forObject = restTemplate.getForObject(userServiceUrl + "/user/{1}", Map.class, id);
System.out.println(forObject);
return forObject;
}
@GetMapping("/fallbackException/{id}")
@SentinelResource(value = "fallbackException", fallback = "handleFallback2", exceptionsToIgnore = {NullPointerException.class})
public Map<String,Object> fallbackException(@PathVariable Long id) {
if (id == 1) {
throw new IndexOutOfBoundsException();
} else if (id == 2) {
throw new NullPointerException();
}
return restTemplate.getForObject(userServiceUrl + "/user/{1}", Map.class, id);
}
public Map<String,Object> handleFallback(Long id) {
Map<String,Object> result = new HashMap<>();
result.put("name","service degradation");
result.put("code",200);
return result ;
}
public Map<String,Object> handleFallback2(Long id, Throwable e) {
LOGGER.error("handleFallback2 id:{},throwable class:{}", id, e.getClass());
Map<String,Object> result = new HashMap<>();
result.put("name","service degradation");
result.put("code",200);
return result ;
}
}
启动springboot-provider和springboot-sentinel服务:
如果id为1 的情况会返回服务降级结果:
http://localhost:8401/breaker/fallback/1
id为2时正常返回:
由于我们使用了 exceptionsToIgnore 参数忽略了 NullPointerException ,所以我们访问接口报空指针时不会发生服务降级:
http://localhost:8401/breaker/fallbackException/2
Sentinel也适配了Feign组件,我们使用Feign来进行服务间调用时,也可以使用它来进行熔断。
首先我们需要在pom.xml中添加Feign相关依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
在application.yml文件中打开Sentinel对Feign的支持:
# 打开sentinel对feign的支持
feign:
sentinel:
enabled: true
在应用启动类上添加@EnableFeignClients启动Feign的功能;
创建一个UserService接口,用于定义对springboot-provider服务的调用:
package com.lee.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.Map;
@FeignClient(value = "springboot-provider", fallback = UserFallbackService.class)
public interface UserService {
@GetMapping("/user/{id}")
Map<String,Object> getInfo(@PathVariable Long id);
}
创建UserFallbackService类实现UserService接口,用于处理服务降级逻辑:
package com.lee.service;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
public class UserFallbackService implements UserService {
@Override
public Map<String,Object> getInfo(Long id){
Map<String,Object> result = new HashMap<>();
result.put("name","service lower level");
result.put("code",200);
return result ;
}
}
在UserFeignController中使用UserService通过Feign调用springboot-provider服务中的接口:
package com.lee.controller;
import com.lee.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/user")
public class UserFeignController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public Map<String,Object> getUser(@PathVariable Long id) {
return userService.getInfo(id);
}
}
在启动类SentinelServiceApplication添加@EnableFeignClients注解
package com.lee;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
调用如下接口会发生服务降级,返回服务降级处理信息:http://localhost:8401/user/1
默认情况下,当我们在Sentinel控制台中配置规则时,控制台推送规则方式是通过API将规则推送至客户端并直接更新到内存中。一旦我们重启应用,规则将消失。下面我们介绍下如何将配置规则进行持久化,以存储到Nacos为例。
首先我们直接在配置中心创建规则,配置中心将规则推送到客户端;
Sentinel控制台也从配置中心去获取配置信息。
先在pom.xml中添加相关依赖:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
修改application.yml配置文件,添加Nacos数据源配置:
spring:
application:
name: springboot-sentinel
cloud:
nacos:
discovery:
namespace: dev
server-addr: 192.168.0.130:8848
sentinel:
transport:
# 配置Sentinel dashborad地址
dashboard: http://localhost:8080
port: 8719
datasource:
- nacos:
server-addr: 192.168.0.130:8848
data-id: ${spring.application.name}-flow-rules
group-id: DEFAULT_GROUP
data-type: json
rule-type: flow
在Nacos中添加配置:
[
{
"app":"springboot-sentinel",
"clusterConfig":{
"fallbackToLocalWhenFail":true,
"sampleCount":10,
"strategy":0,
"thresholdType":0,
"windowIntervalMs":1000
},
"clusterMode":false,
"controlBehavior":0,
"count":1,
"gmtCreate":1617343597193,
"gmtModified":1617343597193,
"grade":1,
"id":1,
"ip":"192.168.0.130",
"limitApp":"default",
"port":8720,
"resource":"byUrl",
"strategy":0
}
]
常规参数解释:
到这已基本完成对sentinel的基本使用。现在从nacos修改的配置信息能够同步到sentinel,但是从sentinel控制台修改的数据却无法同步到nacos中,这样两边都修改数据信息的话,对导致非常严重的后果。
Sentinel Dashboard的流控规则下的所有操作,都会调用Sentinel-Dashboard源码中的FlowControllerV1类,这个类中包含流控规则本地化 的CRUD操作。
另外,在com.alibaba.csp.sentinel.dashboard.controller.v2包下存在一个FlowControllerV2类,这个类同样提供流控规则的CRUD,和V1版本不同的是,它可以实现指定数据源的规则拉取和发布。
@RestController
@RequestMapping(value = "/v2/flow")
public class FlowControllerV2 {
private final Logger logger = LoggerFactory.getLogger(FlowControllerV2.class);
@Autowired
private InMemoryRuleRepositoryAdapter<FlowRuleEntity> repository;
@Autowired
@Qualifier("flowRuleDefaultProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleDefaultPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
FlowControllerV2依赖以下两个非常重要的类:
我们可以扩展这两个类,然后集成Nacos来实现Sentinel Dashboard规则的同步。
源码下载地址:https://github.com/alibaba/Sentinel/releases
修改源码,具体步骤如下:
package com.alibaba.csp.sentinel.dashboard.rule.nacos;
public final class NacosConfigUtil {
public static final String GROUP_ID = "SENTINEL_GROUP";
public static final String FLOW_DATA_ID_POSTFIX = "-flow-rules";
public static final String PARAM_FLOW_DATA_ID_POSTFIX = "-param-rules";
public static final String CLUSTER_MAP_DATA_ID_POSTFIX = "-cluster-map";
/**
* cc for `cluster-client`
*/
public static final String CLIENT_CONFIG_DATA_ID_POSTFIX = "-cc-config";
/**
* cs for `cluster-server`
*/
public static final String SERVER_TRANSPORT_CONFIG_DATA_ID_POSTFIX = "-cs-transport-config";
public static final String SERVER_FLOW_CONFIG_DATA_ID_POSTFIX = "-cs-flow-config";
public static final String SERVER_NAMESPACE_SET_DATA_ID_POSTFIX = "-cs-namespace-set";
private NacosConfigUtil() {}
}
package com.alibaba.csp.sentinel.dashboard.rule.nacos;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.config.ConfigFactory;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
import java.util.Properties;
/**
* @author Eric Zhao
* @since 1.4.0
*/
@Configuration
public class NacosConfig {
@Value("${sentinel.nacos.serverAddr}")
private String nacosAddr ;
@Bean
public Converter<List<FlowRuleEntity>, String> flowRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter<String, List<FlowRuleEntity>> flowRuleEntityDecoder() {
return s -> JSON.parseArray(s, FlowRuleEntity.class);
}
@Bean
public ConfigService nacosConfigService() throws Exception {
Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR,nacosAddr);
return NacosFactory.createConfigService(properties);
}
}
@Component("flowRuleNacosProvider")
public class FlowRuleNacosProvider implements DynamicRuleProvider<List<FlowRuleEntity>> {
@Autowired
private Converter<String, List<FlowRuleEntity>> converter;
@Value("${sentinel.nacos.group-id}")
private String groupId ;
@Autowired
private ConfigService configService ;
@Override
public List<FlowRuleEntity> getRules(String appName) throws Exception {
// Properties properties = new Properties();
// properties.put(PropertyKeyConst.SERVER_ADDR, nacosAddr);
// ConfigService configService = NacosFactory.createConfigService(properties);
// String rules = configService.getConfig(appName + NacosConfigUtil.FLOW_DATA_ID_POSTFIX, groupId, 5000);
// String serverAddr = "192.168.0.130:8848";
// String dataId = "springboot-sentinel-flow-rules";
// String group = "DEFAULT_GROUP";
// Properties properties = new Properties();
// properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
// ConfigService configService = NacosFactory.createConfigService(properties);
// String rules = configService.getConfig(dataId, group, 5000);
System.out.println(appName+NacosConfigUtil.FLOW_DATA_ID_POSTFIX);
String rules = configService.getConfig(appName + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,
groupId, 5000);
if (StringUtil.isEmpty(rules)) {
return new ArrayList<>();
}
return converter.convert(rules);
}
}
@Component("flowRuleNacosPublisher")
public class FlowRuleNacosPublisher implements DynamicRulePublisher<List<FlowRuleEntity>> {
@Autowired
private Converter<List<FlowRuleEntity>, String> converter;
@Value("${sentinel.nacos.group-id}")
private String groupId ;
@Autowired
private ConfigService configService ;
@Override
public void publish(String app, List<FlowRuleEntity> rules) throws Exception {
AssertUtil.notEmpty(app, "app name cannot be empty");
if (rules == null) {
return;
}
// String serverAddr = "192.168.0.241:8848";
// String dataId = "springboot-docker-flow-rules";
// String group = "DEFAULT_GROUP";
// Properties properties = new Properties();
// properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
// ConfigService configService = NacosFactory.createConfigService(properties);
//
// configService.publishConfig(dataId,group,converter.convert(rules));
configService.publishConfig(app + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,
groupId, converter.convert(rules));
}
}
@RestController
@RequestMapping(value = "/v2/flow")
public class FlowControllerV2 {
private final Logger logger = LoggerFactory.getLogger(FlowControllerV2.class);
@Autowired
private InMemoryRuleRepositoryAdapter<FlowRuleEntity> repository;
@Autowired
@Qualifier("flowRuleNacosProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleNacosPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
sentinel.nacos.serverAddr=192.168.0.241:8848
sentinel.nacos.namespace=
sentinel.nacos.group-id=DEFAULT_GROUP
mvn clean package
项目地址(包含编译好的sentinel1.7.2版本的dashboard):https://gitee.com/enthusiasts/sentinel-nacos.git
参考文章:
https://blog.csdn.net/ThinkWon/article/details/103770879
https://www.jianshu.com/p/deb7daa715af