sentinel集成网关支持restful接口进行url粒度的流量治理
- 前言
- 使用网关进行总体流量治理(sentinel版本:1.8.6)
- 1、cloud gateway添加依赖:
- 2、sentinel配置
- 3、网关类型项目配置
- 4、通过zk事件监听刷新上报api分组信息
- 1、非网关项目上报api分组信息
- 2、网关添加监听事件
- 3、网关监听事件处理
- 5、sentinel控制台启动
前言
sentinel作为开源的微服务、流量治理组件,在对restful接口的支持上,在1.7之后才开始友好起来,对于带有@PathVariable的restful接口未作支持,在sentinel中/api/{id}这样的接口,其中/api/1与/api/2会被当做两个不同的接口处理,因此很难去做类似接口的流量治理,但在之后,sentinel团队已经提供了响应的csp扩展依赖,下文将会逐步讲述如何通过sentinel扩展来支持相应的服务流量治理
使用网关进行总体流量治理(sentinel版本:1.8.6)
这里选型为spring cloud gateway,而sentinel也对spring cloud gateway做了特殊照顾
1、cloud gateway添加依赖:
<!-- alibaba封装的sentinel的starter -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2021.1</version>
</dependency>
<!-- alibaba封装的sentinel支持网关的starter -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
<version>2021.1</version>
</dependency>
<!-- 此包即为sentinel提供的扩展支持restful接口的依赖 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-webmvc-adapter</artifactId>
<version>1.8.0</version>
</dependency>
上述需要重点关注的是sentinel-spring-webmvc-adapter包,此依赖是支持restful接口的关键,不需要我们自己改造。
2、sentinel配置
spring:
cloud:
sentinel:
transport:
#sentinel控制台地址
dashboard: 1.1.1.1:8600
#sentinel通信端口,默认为8179,被占用会继续扫描,一般固定起来
port: 8700
#项目所在服务器ip
client-ip: 2.2.2.2
#心跳启动
eager: true
client-ip在某些情况下不配置会出现sentinl控制台页面只有首页,服务一直注册不上去的情况,如果出现这种情况一定要配置上,如果没有这种情况,client-IP可以不配置,同时上述配置的这些ip端口都需要连通。
3、网关类型项目配置
/**
* @classDesc:
* @author: cyjer
* @date: 2023/1/30 9:53
*/
@SpringBootApplication
@EnableCaching
@Slf4j
public class SiriusApplication {
public static void main(String[] args) {
System.getProperties().setProperty("csp.sentinel.app.type", "1");
SpringApplication.run(SiriusApplication.class, args);
log.info("<<<<<<<<<<启动成功>>>>>>>>>>");
}
}
如果是网关类型的项目,需要配置csp.sentinel.app.type= 1,普通项目与网关项目,在控制台上所展示和可使用的功能是不同的
4、通过zk事件监听刷新上报api分组信息
通过将接口分组按照不同粒度,如controller粒度,和具体api接口粒度,通过zookeeper修改数据监听的方式,通过网关监听该事件,实现将api分组信息写入到sentinel中。
1、非网关项目上报api分组信息
/**
* @classDesc: 扫描项目接口上报api
* @author: cyjer
* @date: 2023/2/10 13:46
*/
@Configuration
@Slf4j
@Order(1)
@RequiredArgsConstructor
public class ApiDefinitionReporter implements BeanPostProcessor, CommandLineRunner, Constraint {
private final List<ApiSiriusDefinition> apiSiriusDefinitionList = new ArrayList<>();
private final GatewayServiceProperties gatewayServiceProperties;
private final Environment environment;
private final static char JTR = '/';
private final static String PASS = "/**";
private final static String APPLICATION_NAME = "spring.application.name";
private final static String CONTEXT_PATH = "server.servlet.context-path";
private final static List<String> PASS_LIST = Arrays.asList("swaggerWelcome", "basicErrorController", "swaggerConfigResource", "openApiResource");
private final ZookeeperService zookeeperService;
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// url访问路径为:访问基地址basePath+classMappingPath+methodPath
if (!gatewayServiceProperties.isAutoReportAndRegister() || PASS_LIST.contains(beanName)) {
return bean;
}
Class<?> beanClass = bean.getClass();
Class<?> targetClass = AopUtils.getTargetClass(bean);
//判断类上有无controller注解 spring代理类需用spring的注解扫描工具类查找
RestController restController = AnnotationUtils.findAnnotation(beanClass, RestController.class);
Controller controller = AnnotationUtils.findAnnotation(beanClass, Controller.class);
//没有注解直接跳过扫描
if (null == controller && null == restController) {
return bean;
}
String applicationName = this.getApplicationName();
//项目访问基地址
String basePath = this.getBasePath();
//如果类上有controller注解再查找requestMapping注解
RequestMapping requestMapping = AnnotationUtils.findAnnotation(beanClass, RequestMapping.class);
String classMappingPath = this.getClassMappingPath(requestMapping);
//按照controller分组上报
if (StringUtils.isNotBlank(classMappingPath)) {
String controllerGroupPath = basePath + classMappingPath + PASS;
ApiSiriusDefinition controllerGroup = new ApiSiriusDefinition();
controllerGroup.setGatewayId(gatewayServiceProperties.getGatewayId());
controllerGroup.setResource("服务:" + applicationName + ",控制器:" + targetClass.getSimpleName() + ",路径:" + controllerGroupPath);
controllerGroup.setUrlPath(controllerGroupPath);
apiSiriusDefinitionList.add(controllerGroup);
}
//查找类中所有方法,进行遍历
Method[] methods = targetClass.getMethods();
for (Method method : methods) {
//查找方法上RequestMapping注解
String methodPath = "";
String requestType = "";
RequestMapping methodRequestMapping = AnnotationUtils.findAnnotation(method, RequestMapping.class);
if (methodRequestMapping != null) {
String[] value = methodRequestMapping.value();
RequestMethod[] requestMethods = methodRequestMapping.method();
if (value.length == 0) {
if (requestMethods.length == 0) {
return bean;
}
RequestMethod requestMethod = requestMethods[0];
requestType = requestMethod.name();
if (requestMethod.equals(RequestMethod.POST)) {
PostMapping postMapping = AnnotationUtils.findAnnotation(method, PostMapping.class);
methodPath = this.joinMethodPath(postMapping.value());
} else if (requestMethod.equals(RequestMethod.GET)) {
GetMapping getMapping = AnnotationUtils.findAnnotation(method, GetMapping.class);
methodPath = this.joinMethodPath(getMapping.value());
} else if (requestMethod.equals(RequestMethod.DELETE)) {
DeleteMapping deleteMapping = AnnotationUtils.findAnnotation(method, DeleteMapping.class);
methodPath = this.joinMethodPath(deleteMapping.value());
} else if (requestMethod.equals(RequestMethod.PATCH)) {
PatchMapping patchMapping = AnnotationUtils.findAnnotation(method, PatchMapping.class);
methodPath = this.joinMethodPath(patchMapping.value());
} else if (requestMethod.equals(RequestMethod.PUT)) {
PutMapping putMapping = AnnotationUtils.findAnnotation(method, PutMapping.class);
methodPath = this.joinMethodPath(putMapping.value());
}
}
ApiSiriusDefinition apiSiriusDefinition = new ApiSiriusDefinition();
String urlPath = basePath + classMappingPath + methodPath;
apiSiriusDefinition.setUrlPath(urlPath);
apiSiriusDefinition.setRequestType(requestType);
apiSiriusDefinition.setGatewayId(gatewayServiceProperties.getGatewayId());
apiSiriusDefinition.setResource("服务:" + applicationName + ",请求类型:" + requestType + ",路径:" + urlPath);
apiSiriusDefinitionList.add(apiSiriusDefinition);
}
}
return bean;
}
private String joinMethodPath(String[] value) {
if (value.length != 0) {
String str = this.trimStrWith(value[0], JTR);
return JTR + str;
}
return "";
}
private String getContextPath() {
String contextPath = environment.getProperty(CONTEXT_PATH);
contextPath = this.trimStrWith(contextPath, JTR);
return StringUtils.isBlank(contextPath) ? "" : contextPath;
}
public String getApplicationName() {
String applicationName = environment.getProperty(APPLICATION_NAME);
applicationName = this.trimStrWith(applicationName, JTR);
return StringUtils.isBlank(applicationName) ? "" : applicationName;
}
private String getBasePath() {
String contextPath = this.getContextPath();
String applicationName = this.getApplicationName();
if (StringUtils.isBlank(contextPath)) {
return JTR + applicationName;
}
return JTR + applicationName + JTR + contextPath;
}
private String getClassMappingPath(RequestMapping requestMapping) {
if (null != requestMapping) {
String requestMappingUrl = requestMapping.value().length == 0 ? "" : requestMapping.value()[0];
requestMappingUrl = this.trimStrWith(requestMappingUrl, JTR);
return JTR + requestMappingUrl;
}
return "";
}
public String trimStrWith(String str, char trimStr) {
if (StringUtils.isBlank(str)) {
return str;
}
int st = 0;
int len = str.length();
char[] val = str.toCharArray();
while ((st < len) && (val[st] <= trimStr)) {
st++;
}
while ((st < len) && (val[len - 1] <= trimStr)) {
len--;
}
return ((st > 0) || (len < str.length())) ? str.substring(st, len) : str;
}
@Override
public void run(String... args) {
if (StringUtils.isBlank(this.getApplicationName())) {
throw new RuntimeException(APPLICATION_NAME + " should not be null");
}
if (!apiSiriusDefinitionList.isEmpty()) {
log.info("<<<<< start to report api information to api governance platform >>>>>");
try {
zookeeperService.create(API_DEFINITION + SPLIT + getApplicationName(), JSONArray.toJSONString(apiSiriusDefinitionList));
zookeeperService.update(API_DEFINITION + SPLIT + getApplicationName(), JSONArray.toJSONString(apiSiriusDefinitionList));
} catch (Exception e) {
log.error("reported api information failed,stack info:", e);
}
log.info("<<<<< successfully reported api information >>>>>");
}
}
}
通过扫描项目下的controller和相应的mapping注解中的属性拼接出url来,通过zk来更新节点数据
2、网关添加监听事件
zk操作查看另一篇文章zookeeper相关操作
/**
* @classDesc: 网关核心应用
* @author: cyjer
* @date: 2023/1/30 9:53
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class GatewayApplication implements ApplicationListener<ContextRefreshedEvent> {
private final GatewayServiceProperties properties;
private final ApiDefinitionService apiDefinitionService;
private final ZookeeperService zookeeperService;
private final ApiGroupProcesser apiGroupProcesser;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
//拉取api governance platform 信息
apiDefinitionService.refreshApiGovernanceInfo(properties.getGatewayId());
log.info("<<<<<<<<<<刷新api分组信息完成>>>>>>>>>>");
zookeeperService.create(Constraint.API_DEFINITION, "init");
zookeeperService.addWatchChildListener(Constraint.API_DEFINITION, apiGroupProcesser);
log.info("<<<<<<<<<<api上报监听器配置完成>>>>>>>>>>");
}
}
通过事件处理,首先启动时刷新api信息,同时尝试初始化zk节点,然后注册监听watch。
3、网关监听事件处理
/**
* @classDesc: api分组上报
* @author: cyjer
* @date: 2023/2/10 11:13
*/
@Slf4j
@Component
public class ApiGroupProcesser extends AbstractChildListenerProcess implements ApiDefinitionConstraint {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private GatewayServiceProperties gatewayServiceProperties;
@Override
public void process(CuratorFramework curatorFramework, PathChildrenCacheEvent cacheEvent) {
ChildData data = cacheEvent.getData();
if (Objects.nonNull(data) && cacheEvent.getType().equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)) {
log.info("<<<<<<<<<<上报api分组到sentinel>>>>>>>>>>");
String path = data.getPath();
String content = new String(data.getData(), StandardCharsets.UTF_8);
Set<ApiDefinition> definitions = GatewayApiDefinitionManager.getApiDefinitions();
List<ApiSiriusDefinition> list = JSONArray.parseArray(content, ApiSiriusDefinition.class);
for (ApiSiriusDefinition apiGroup : list) {
ApiDefinition api = new ApiDefinition(apiGroup.getResource())
.setPredicateItems(new HashSet<ApiPredicateItem>() {
{
add(new ApiPathPredicateItem().setPattern(apiGroup.getUrlPath())
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}
});
definitions.add(api);
}
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
redisTemplate.opsForHash().put(API_INFO_REDIS_PREFIX + gatewayServiceProperties.getGatewayId(), path, JSONArray.toJSONString(list));
log.info("<<<<<<<<<<上报api分组到sentinel成功>>>>>>>>>>");
}
}
}
5、sentinel控制台启动
java -Dserver.port=8600 -Dcsp.sentinel.dashboard.server=localhost:8600 -Dproject.name=sentinel-dashboard -Xms512m -Xmx512m -Xmn256m -XX:MaxMetaspaceSize=100m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/oom/log -Dfile.encoding=UTF-8 -XX:+UseG1GC -jar sentinel-dashboard-1.8.6.jar
打开sentinel控制台,请求几次接口后
可以看到相应的api分组信息和url路径匹配都已加载,在进行流量治理的时候就可以支持restful接口和controller粒度的治理了