本篇博客主要包括:限流方式、微服务容灾技术选型、Sentinel的使用、Sentinel动态规则持久化Zookeeper 等。
Sentinel动态规则持久化到Zookeeper的代码实现我已经共享到本人的github,有需要的可以下载使用。
github地址:https://github.com/Force-King/sentinel-dashboard-zk
持久化zk的sentinel-dashboard项目下载地址:https://github.com/Force-King/sentinel-dashboard-zk.git
一、限流方式
服务端限流
如果在服务端做限流,无论有多少个客户端,总的提供能力是固定的,所以不会因为客户端数量过多而导致资源不足,因为处理不过来的请求会被阻塞等待获取资源。
缺点
缺点也比较明显,由于服务提供者整体设置了最大限流数,此时所有的客户端共享同一份限流数据,那么有可能导致有的服务能分配到资源有些服务请求分配不到资源导致无法请求的情况。
客户端限流
客户端限流解决上服务端限流提到的问题,它能保证每个客户端都能得到响应。但是从其它方面考虑,必须针对不同的客户端做不同的限流策略:
- 请求量大,但时效性不高,此时将限流数控制小一些会比较合适
- 请求量大,但时效性高,此时将限流数适当调高
- 响应时间长,即慢接口,适当降低
- 主流业务,核心业务,适当调高
- 非主流业务,适当降低
缺点
- 如果客户端的数量不固定,那么有可能导致客户端数量过多造成大量请求打到服务端导致处理不了的结果,所以需要严格监控客户端的调用情况。
- 配置复杂,需要针对每个客户端做相对精准的判断
二、选型
1. 目前开源容灾框架有Hystrix、Sentinel、Resilience4j
Hystrix:常用于Spring Cloud的一个熔断降级组件。主要功能是不同服务之间的资源隔离、失败降级。底层实现是Rxjava。它提供两种资源隔离的模式:信号量隔离和线程池隔离。一般使用线程池隔离。耗费一定资源,但相比之下支持超时和异步执行。听起来可以覆盖大部分场景,但它不支持更高要求的流控,如qps的控制。所以需要单独采用令牌漏桶来做流量控制。
Sentinel:阿里开源的分布式流量控制组件。支持流控、熔断降级、系统保护等。所有的资源都对应一个资源名称以及一个Entry。每一个Entry创建的时候,同时也会创建一系列插件(系统保护插件:SystemSlot、流控插件:FlowSlot、熔断降级插件LDegradeSlot等)。每个插件会监控自己职责范围内的指标。NodeSelectorSlot将各个资源的调用路径以树状存储,用于限流降级。调用者通过创建上下文、请求token来执行方法。若没有抛出BlockException,表示请求成功。它支持并发数/qps的流量控制、也支持熔断降级。
Resilience4j:一个比较轻量的熔断降级库。模块化做的比较好,将每个功能点(如熔断、限速器、自动重试)都拆成了单独的模块,这样整体结构很清晰,用户也只需要引入相应功能的依赖即可;另外resilience4j 是针对 Java 8 和函数式编程设计的,API 比较简洁优雅。同时与 Hystrix 相比,Resilience4j 增加了简单的限速器和自动重试特性,使用场景更加丰富。Resilience4j 属于一个新兴项目,社区也在蓬勃发展。总的来说,Resilience4j 是比较轻量的库,在较小较新的项目中使用还是比较方便的,但是 Resilience4j 只包含限流降级的基本场景,对于非常复杂的企业级服务架构可能无法很好地 cover 住;同时 Resilience4j 缺乏生产级别的配套设施(如提供规则管理和实时监控能力的控制台)。
2. Sentinel 的优势和特性
- 轻量级,核心库无多余依赖,性能损耗小。
- 方便接入,开源生态广泛。
Sentinel 对 Dubbo、Spring Cloud、Web Servlet、gRPC 等常用框架提供适配模块,只需引入相应依赖并简单配置即可快 速接入;同时针对自定 义的场景 Sentinel 还提供低侵入性的注解资源定义方式,方便自定义接入。
- 丰富的流量控制场景。
Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,流控维度包括流控指标、流控效果(塑形)、调用关系、热点、集群等各种维度,针对系统维度也提供自适应的保护机制。
- 易用的控制台,提供实时监控、机器发现、规则管理等能力。
- 完善的扩展性设计,提供多样化的 SPI 接口,方便用户根据需求给 Sentinel 添加自定义的逻辑。
Sentinel 的主要特性
Sentinel 的开源生态
目前使用Sentinel 的公司
阿里巴巴、顺丰、爱奇艺、vivo、每日优鲜、拼多多、易企秀、360金融、麻袋财富、喜马拉雅FM 等。
Sentinel的性能
官方测试,单机25w qps以下基本没啥影响,超过了这个值会有一个比较明显的5%-10%的下降。
三、Sentinel 使用
1. maven 配置pom.xml
<properties>
<sentinel.version>1.6.2</sentinel.version>
</properties>
<!-- 限流降级 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-zookeeper</artifactId>
<version>${sentinel.version}</version>
</dependency>
<!-- 热点规则 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-parameter-flow-control</artifactId>
<version>${sentinel.version}</version>
</dependency>
2. 添加AOP配置类
spring boot 项目:
@SpringBootConfiguration
public class AopConfiguration {
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
}
spring 项目:
@Configuration
public class SentinelAspectConfiguration {
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
}
3. 初始化
jdk 1.8:
jdk 1.7:
4. 使用注解在客户端项目中进行埋点
@RequestMapping(value = "/query", method = RequestMethod.GET)
@SentinelResource(value = "/query", entryType= EntryType.IN, blockHandler = "queryExceptionHandler", blockHandlerClass = {ControllerExceptionUtil.class})
public String query (@RequestParam(value = "appid", required = true) Integer appid,
@RequestParam(value = "idfa", required = true) String idfa){
JSONObject obj = new JSONObject();
try {
int ret = 0;
if(null != repeatService.findIdfa(appid,idfa)){
ret = 1;
}
obj.put(idfa,ret);
} catch (Exception e){
logger.error(e);
return JsonResult.buildEnum(RequestResultEnum.SERVER_EXP).toString();
}
return JsonResult.success(obj).toString();
}
降级、限流处理类,如下:
注解中blockHandlerClass的值就是上面的处理类,其中的方法就是blockHandler对应的值,名称要一致。
service层使用:
5. 启动配置
客户端启动参数:-Dcsp.sentinel.dashboard.server=localhost:8080 -Dcsp.sentinel.log.use.pid=true -Dproject.name=market
控制台启动:java -Dserver.port=8080 -Dcsp.sentinel.log.use.pid=true -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
查看控制台访问: http://localhost:8080/#/dashboard/home
用户:sentinel
密码:sentinel
控制台显示效果:
在控制台可以手动配置限流和降级规则:
如果不需要对特点请求来源做限流,来源应用就用默认即可,默认是指所有的应用。
四、Sentinel动态规则持久化Zookeeper
因为目前阿里的Sentinel控制台默认没有持久化,是存在JVM内存,一旦控制台重启,之前配置的规则就会丢失。
不过控制台提供了可扩展的接口,只要实现DynamicRulePublisher<T>接口的publish() 和 DynamicRuleProvider<T>接口getRules() 即可。
使用Zookeeper做配置中心,将限流规则、降级规则持久化到配置中心,配置中心统一push到集群的每一台机器。实现配置的一致性。
zookeeper配置:
#Zookeeper
zk.address=127.0.0.1:2181
zk.sentinel.path=/sentinel_rules
zk.sentinel.appName=/market
项目初始化加载配置中心的规则:
@Component
public class ZookeeperSentinelConfig {
private final Logger logger = LoggerFactory.getLogger(ZookeeperSentinelConfig.class);
@Value("${zk.address}")
private String zkAddress;
@Value("${zk.sentinel.path}")
private String zkPath;
@Value("${zk.sentinel.appName}")
private String appName;
private final String FLOW_PATH = "/flow";
private final String DEGRADE_PATH = "/degrade";
@PostConstruct
public void loadRules() {
//限流Flow
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new ZookeeperDataSource<>(zkAddress, zkPath + appName,
source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
}));
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
//降级Degrade
String degradePath = zkPath + appName + DEGRADE_PATH;
Converter<String, List<DegradeRule>> degradeRules = source -> JSON.parseObject(source, new TypeReference<List<DegradeRule>>() {
});
ReadableDataSource<String, List<DegradeRule>> zkDataSourceDegrade = new ZookeeperDataSource<>(zkAddress, degradePath, degradeRules);
DegradeRuleManager.register2Property(zkDataSourceDegrade.getProperty());
logger.info("----------------- Sentinel DataSource Zookeeper Init Success -------------------");
}
public String getFlowRulePath(String appName) {
if (appName.startsWith("/")) {
return zkPath+appName+FLOW_PATH;
} else {
return zkPath + "/" + appName + FLOW_PATH;
}
}
public String getDegradeRulePath(String appName) {
if (appName.startsWith("/")) {
return zkPath+appName+DEGRADE_PATH;
} else {
return zkPath + "/" + appName + DEGRADE_PATH;
}
}
public String getZkAddress() {
return zkAddress;
}
public String getZkPath() {
return zkPath;
}
}
getRules实现:
@Component("flowRuleZookeeperProvider")
public class FlowRuleZookeeperProvider implements DynamicRuleProvider<List<FlowRuleEntity>> {
@Autowired
private CuratorFramework zkClient;
@Autowired
private Converter<String, List<FlowRuleEntity>> converter;
@Autowired
private ZookeeperSentinelConfig zkConfig;
@Override
public List<FlowRuleEntity> getRules(String appName) throws Exception {
String zkPath = zkConfig.getFlowRulePath(appName);
byte[] bytes = zkClient.getData().forPath(zkPath);
if (null == bytes || bytes.length == 0) {
return new ArrayList<>();
}
String s = new String(bytes);
return converter.convert(s);
}
}
publish实现:
@Component("flowRuleZookeeperPublisher")
public class FlowRuleZookeeperPublisher implements DynamicRulePublisher<List<FlowRuleEntity>> {
@Autowired
private CuratorFramework zkClient;
@Autowired
private Converter<List<FlowRuleEntity>, String> converter;
@Autowired
private ZookeeperSentinelConfig zkConfig;
@Override
public void publish(String app, List<FlowRuleEntity> rules) throws Exception {
AssertUtil.notEmpty(app, "app name cannot be empty");
String path = zkConfig.getFlowRulePath(app);
Stat stat = zkClient.checkExists().forPath(path);
if (stat == null) {
zkClient.create().creatingParentContainersIfNeeded().withMode(CreateMode.PERSISTENT).forPath(path, null);
}
byte[] data = CollectionUtils.isEmpty(rules) ? "[]".getBytes() : converter.convert(rules).getBytes();
zkClient.setData().forPath(path, data);
}
}
如果使用持久化,Sentinel-dashboard需要修改代码,限流Flow 使用FlowControllerV2,具体使用方法Sentinel的github上有,这里就不再介绍了。需要注意的是,页面的跳转路径需要修改。使用/v2,降级是Degrade, 也需要修改。
前端页面需要修改 sidebar.html 的流控规则跳转方法,identity.js 中的流控规则添加后跳转路径改为/dashboard/v2/flow/,否则在簇点链路添加流控规则会有问题。