sentinel是阿里推出的流控防护组件,随着hystrix不在维护,新的项目一般会选用 resilience4j 或者 Sentinel 进行代替,由于国内很多公司使用的就是SpringCloudAlibaba作为微服务体系.
项目现在需要使用到流控组件,先对 Sentinel 进行学习
学习路线大概分了三个步骤
目的就是在基于SpringCloud下需要引入Sentinel,因此直接引入starter项目
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
@Test
public void test() throws InterruptedException {
var rule = new FlowRule();
rule.setResource("HelloWorld");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(10);
FlowRuleManager.loadRules(List.of(rule));
DegradeRule degradeRule = new DegradeRule();
degradeRule.setResource("HelloWorld");
DegradeRuleManager.loadRules(List.of(degradeRule));
int counter = 0;
while (true) {
try (Entry entry = SphU.entry("HelloWorld")) {
if (++counter % 9 == 0) {
throw new RuntimeException("抛出一个异常");
}
System.out.println("hello world!");
} catch (BlockException e) {
TimeUnit.MILLISECONDS.sleep(200);
Tracer.trace(e);
System.out.println("block!");
} catch (Exception e) {
System.out.println("fallback");
}
}
}
对一个接口如何进行压测是比较熟悉的, 通过jmeter等工具对接口进行压测, 但是通过Sentinel的官方Demo中,我学到了新姿势
private void initDegradeRule() {
DegradeRule degradeRule = new DegradeRule(DEMO_RESOURCE); //资源
degradeRule.setGrade(CircuitBreakerStrategy.ERROR_RATIO.getType()) //采用异常率作为熔断规则
.setCount(0.5d) //异常率达到 50% 进行熔断
.setStatIntervalMs(30000) //三百毫秒作为一次bucket
.setMinRequestAmount(50) //请求达到50个并且才会判断异常率
.setTimeWindow(5); //发生熔断后多久会变为半开状态进行尝试
DegradeRuleManager.loadRules(List.of(degradeRule));
}
static AtomicInteger total = new AtomicInteger();
static AtomicInteger pass = new AtomicInteger();
static AtomicInteger block = new AtomicInteger();
static AtomicInteger bizException = new AtomicInteger();
static volatile boolean isRunning = true;
static final String DEMO_RESOURCE = "HelloWorld";
static int runningDuration = 60;
private void tick() {
new Thread(() -> {
int beforeTotal = 0, beforePass = 0, beforeBlock = 0, beforeBizException = 0;
while (isRunning) {
ThrowableRunnable.execute(() -> TimeUnit.SECONDS.sleep(1));
int currentTotal = total.get();
int currentPass = pass.get();
int currentBlock = block.get();
int currentBizException = bizException.get();
String format = "倒计时第 %d 秒, 总请求数-> %d 个,通过 -> %d 个 , 阻塞 -> %d 个,异常 %d 个 \n";
System.out.printf(format, runningDuration, currentTotal - beforeTotal,
currentPass - beforePass,
currentBlock - beforeBlock,
currentBizException - beforeBizException);
beforeTotal = currentTotal;
beforeBizException = currentBizException;
beforePass = currentPass;
beforeBlock = currentBlock;
if (runningDuration-- <= 0) {
isRunning = false;
}
}
}).start();
}
断路器的规则基本都是一样的,简单描述一下:
关闭时流量通过 → 触发限流时关闭 → 达到一定时间进入半开,放一个请求进行请求的尝试 → 尝试的请求正常则断路器关闭,请求还失败会继续开启
private void registerStateChangeObserver() {
EventObserverRegistry.getInstance()
.addStateChangeObserver("logging", ((prevState, newState, rule, snapshotValue) -> {
// snapshotValue就是当前快照点的异常率
if (newState == CircuitBreaker.State.OPEN) {
System.err.printf("资源: %s 断路器开启, at %s , snapshotValue: %.2f \n",
rule.getResource(), TimeUtil.currentTimeMillis(), snapshotValue);
}else{
System.err.printf("资源: %s -> %s at %d \n", prevState.name(), newState.name(),
TimeUtil.currentTimeMillis());
}
}));
}
final int concurrentThread = 10;
for (int i = 0; i < concurrentThread; i++) {
new Thread(() -> {
while (true) {
Entry entry = null;
try {
entry = SphU.entry(DEMO_RESOURCE);
int nextInt = ThreadLocalRandom.current().nextInt(5, 10);
ThrowableRunnable.execute(() -> TimeUnit.MICROSECONDS.sleep(nextInt));
pass.incrementAndGet();
int errInt = ThreadLocalRandom.current().nextInt(0, 100);
//大概50%出现异常情况
if (errInt > 50) {
throw new RuntimeException("randomException");
}
} catch (BlockException e) {
block.incrementAndGet();
int nextInt = ThreadLocalRandom.current().nextInt(5, 10);
ThrowableRunnable.execute(() -> TimeUnit.MICROSECONDS.sleep(nextInt));
} catch (Throwable e) {
bizException.incrementAndGet();
//当发生异常时,这段代码为必须执行 ; 在后面close执行的时候,会从entry中获取异常,不为空则异常累加一次
Tracer.traceEntry(e, entry);
}finally {
total.incrementAndGet();
if (entry != null) {
entry.exit();
}
}
}
}).start();
}
ThrowableRunnable.execute(() -> Thread.currentThread().join());
程序每秒都会滴滴答答的展示当前的检测结果,挑选部分结果进行展示
资源: HelloWorld 断路器开启, at 1644550765275 , snapshotValue: 0.50
倒计时第 60 秒, 总请求数-> 2771 个,通过 -> 742 个 , 阻塞 -> 2039 个,异常 368 个
倒计时第 59 秒, 总请求数-> 4223 个,通过 -> 0 个 , 阻塞 -> 4222 个,异常 0 个
倒计时第 58 秒, 总请求数-> 4577 个,通过 -> 0 个 , 阻塞 -> 4578 个,异常 0 个
倒计时第 57 秒, 总请求数-> 4757 个,通过 -> 0 个 , 阻塞 -> 4756 个,异常 0 个
倒计时第 56 秒, 总请求数-> 4739 个,通过 -> 0 个 , 阻塞 -> 4740 个,异常 0 个
资源: OPEN -> HALF_OPEN at 1644550770276
资源: HelloWorld 断路器开启, at 1644550770278 , snapshotValue: 1.00
倒计时第 55 秒, 总请求数-> 4634 个,通过 -> 1 个 , 阻塞 -> 4633 个,异常 1 个
倒计时第 54 秒, 总请求数-> 3603 个,通过 -> 0 个 , 阻塞 -> 3603 个,异常 0 个
倒计时第 53 秒, 总请求数-> 4629 个,通过 -> 0 个 , 阻塞 -> 4628 个,异常 0 个
倒计时第 52 秒, 总请求数-> 4829 个,通过 -> 0 个 , 阻塞 -> 4829 个,异常 0 个
倒计时第 51 秒, 总请求数-> 4572 个,通过 -> 0 个 , 阻塞 -> 4573 个,异常 0 个
资源: OPEN -> HALF_OPEN at 1644550775278
资源: HelloWorld 断路器开启, at 1644550775280 , snapshotValue: 1.00
倒计时第 50 秒, 总请求数-> 4731 个,通过 -> 1 个 , 阻塞 -> 4729 个,异常 1 个
倒计时第 49 秒, 总请求数-> 4892 个,通过 -> 0 个 , 阻塞 -> 4892 个,异常 0 个
sentinel有自己的控制台, 下载页面
电脑的默认JDK环境是17启动失败了,这时候跑到JDK8的目录下执行命令即可 :
java -jar 目录\sentinel-dashboard-1.8.3.jar --server.port=9000
界面还是很不错的,可以通过界面直接设置一些流控规则,但是很明显这是给应急用的,没有发现持久化的规则方式当服务启动时规则肯定会丢失的; 远程配置中心和Sentinel继承肯定是要的
引入web /openfeign /nacos依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba.csp/sentinel-datasource-nacos -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
spring:
application:
name: consumer-application
sentinel:
transport:
port: 8719
dashboard: localhost:9000
server:
port: 10002
上一个专门写日常测试的仓库模块太多太难受,这里只是用一个模块进行测试,因此采用ApplicationBuilder来启动项目, 对应不同的配置文件
@EnableAutoConfiguration
@ComponentScan
@RestController
public class ProviderApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ProviderApplication.class)
.run("--spring.profiles.active=provider");
}
@GetMapping("/echo")
public ResponseEntity<String> echo(@RequestParam Integer sleepTime) throws InterruptedException {
TimeUnit.SECONDS.sleep(sleepTime);
return ResponseEntity.ok("成功");
}
@GetMapping("/isOk")
public ResponseEntity<String> isOk(@RequestParam Boolean isOk) {
if (isOk) {
return ResponseEntity.ok("ok");
}
return ResponseEntity.badRequest().body("not ok");
}
}
当发生请求后,通过控制台可以看到资源请求的情况,也可以通过控制台对资源进行流控管理
盲猜也是通过aop对方法进行横切,然后切面中进行增强. Sentinel目前切入的方式大致分三种:
- 比如demo的方式,手动通过SphU.entry进入,entry.close进行关闭注解形式:
- @SentinelResource,比如需要对业务代码某一块进行管理,那么可以在代码上加入注解产生一个Resource. 然后对方法进行拦截,拦截过程中增加流控逻辑 ; AOP类: `SentinelResourceAspect` 类三方集成
- 集成一般都在\*-starter 包, 逼格高的有一个autoconfig 包, 里面会有自动装配的配置类我们前面引入的starter-alibaba-sentinel是在一块的,这里在 META-INF/spring.factories下 看下默认引入装配类有哪些:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.cloud.sentinel.SentinelWebAutoConfiguration,\
com.alibaba.cloud.sentinel.SentinelWebFluxAutoConfiguration,\
com.alibaba.cloud.sentinel.endpoint.SentinelEndpointAutoConfiguration,\
com.alibaba.cloud.sentinel.custom.SentinelAutoConfiguration,\
com.alibaba.cloud.sentinel.feign.SentinelFeignAutoConfiguration
org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker=\
com.alibaba.cloud.sentinel.custom.SentinelCircuitBreakerConfiguration
核心在 SentinelWebAutoConfiguration 中
核心在 SentinelFeignAutoConfiguration 中
控制台上也可以增加流控规则,但是因为只能针对某一个节点并且没有持久化,一般只能简单用用,生产上还是需要将配置进行统一的管理
我们通过配置文件对Sentinel的行为进行调整,对应的Java类是: SentinelProperties
装配入口在 SentinelAutoConfiguration 中, SentinelDataSourceHandler 则是将配置文件进行解析产生对应的资源对象
SentinelProperties#datasource变量明显是进行数据源管理的,
DataSourcePropertiesConfiguration则是数据源的配置项,该类代码如下:
public class DataSourcePropertiesConfiguration {
private FileDataSourceProperties file;
private NacosDataSourceProperties nacos;
private ZookeeperDataSourceProperties zk;
private ApolloDataSourceProperties apollo;
private RedisDataSourceProperties redis;
private ConsulDataSourceProperties consul;
}
也就是上面六种形式都是支持通过配置文件形式进行配置的. 我在官网上看到还支持etcd, 可能暂时不支持直接使用配置文件,需要手动指定一下
以file形式为例, 不使用配置文件而使用Java代码的形式配置一个基于文件的配置管理
@Configuration
public class SentinelConfig {
@Bean
public SentinelDataSourceHandler sentinelDataSourceHandler(
DefaultListableBeanFactory beanFactory, SentinelProperties sentinelProperties,
Environment env) {
FileDataSourceProperties fileDataSourceProperties = new FileDataSourceProperties();
fileDataSourceProperties.setFile("classpath:sentinel/degraderule.json");
fileDataSourceProperties.setRuleType(RuleType.DEGRADE);
DataSourcePropertiesConfiguration custom = new DataSourcePropertiesConfiguration();
custom.setFile(fileDataSourceProperties);
Map<String, DataSourcePropertiesConfiguration> datasource = sentinelProperties.getDatasource();
datasource.put("file", custom);
return new SentinelDataSourceHandler(beanFactory, sentinelProperties, env);
}
}
因为本地有nacos的exe文件,那么就以nacos为例进行了测试,
配置文件也是按照Java类的成员变量来的,对着 SentinelProperties来配置一个nacos的配置,
spring:
application:
name: invoke-application
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
sentinel:
transport:
port: 8719
dashboard: localhost:9000
datasource:
nacosRule:
nacos:
serverAddr: 127.0.0.1:8848
dataId: demo-sentinel
ruleType: flow
# 启用SentinelFeign
feign:
sentinel:
enabled: true
之前demo中学到了一个tick滴滴答答的进行数据展示, 这里也搞一个tick进行资源的打印
@Bean
CommandLineRunner runner() {
return arg -> {
tickRule();
};
}
private void tickRule() {
new Thread(() -> {
while (true) {
System.err.println("======== ");
List<FlowRule> rules = FlowRuleManager.getRules();
for (FlowRule rule : rules) {
String resource = rule.getResource();
int strategy = rule.getStrategy();
double count = rule.getCount();
String format = String.format("资源:%s , 策略: %d , count: %s", resource, strategy, count);
System.err.println("format = " + format);
}
ThrowableRunnable.execute(() -> TimeUnit.SECONDS.sleep(60));
}
}).start();
}
nacos上配置规则
[
{
"resource": "GET:http:://localhost:10000/echo",
"count": 1,
"grade": 1,
"timeWindow": 1
}
]
项目启动后,对规则调整一次,规则进行了正确的打印
========
format = 资源:GET:http:://localhost:10000/echo , 策略: 0 , count: 1.0
========
format = 资源:GET:http:://localhost:10000/echo , 策略: 0 , count: 1.0
format = 资源:GET:http:://localhost:10000/echo2 , 策略: 0 , count: 2.0
========
format = 资源:GET:http:://localhost:10000/echo , 策略: 0 , count: 1.0
format = 资源:GET:http:://localhost:10000/echo2 , 策略: 0 , count: 2.0
over
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。