增加rebalance / testing / license能力

This commit is contained in:
zengqiao
2023-02-23 11:56:46 +08:00
parent c27786a257
commit c56d8cfb0f
137 changed files with 10772 additions and 3 deletions

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.xiaojukeji.kafka</groupId>
<artifactId>km-license</artifactId>
<version>${km.revision}</version>
<packaging>jar</packaging>
<parent>
<artifactId>km</artifactId>
<groupId>com.xiaojukeji.kafka</groupId>
<version>${km.revision}</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>com.xiaojukeji.kafka</groupId>
<artifactId>km-common</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>com.xiaojukeji.kafka</groupId>
<artifactId>km-persistence</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>com.xiaojukeji.kafka</groupId>
<artifactId>km-core</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,69 @@
package com.xiaojukeji.know.streaming.km.license;
import com.didiglobal.logi.log.ILog;
import com.didiglobal.logi.log.LogFactory;
import com.xiaojukeji.know.streaming.km.common.bean.entity.result.Result;
import com.xiaojukeji.know.streaming.km.common.utils.ConvertUtil;
import com.xiaojukeji.know.streaming.km.license.service.LicenseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import static com.xiaojukeji.know.streaming.km.common.constant.ApiPrefix.API_V3_PREFIX;
@Component
public class LicenseInterceptor implements HandlerInterceptor {
private static final ILog LOGGER = LogFactory.getLog(LicenseInterceptor.class);
private static final String PHYSICAL_CLUSTER_URL = API_V3_PREFIX + "physical-clusters";
private static final String UTF_8 = "utf-8";
@Autowired
private LicenseService licenseService;
/**
* 拦截预处理
* @return boolean false:拦截, 不向下执行, true:放行
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (PHYSICAL_CLUSTER_URL.equals( request.getRequestURI() ) &&
"POST".equals( request.getMethod() )) {
Result<Void> result = licenseService.addClusterLimit();
if (result.failed()) {
// 如果出错,构造错误信息
OutputStream out = null;
try {
response.setCharacterEncoding(UTF_8);
response.setContentType("text/json");
out = response.getOutputStream();
out.write(ConvertUtil.obj2Json(result).getBytes(UTF_8));
out.flush();
} catch (IOException e) {
LOGGER.error( "method=preHandle||msg=physical-clusters add exception! ", e);
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
LOGGER.error( "method=preHandle||msg=outputStream close exception! ", e);
}
}
// 拒绝向下执行
return false;
}
}
// 未达到限制,继续后续的执行
return true;
}
}

View File

@@ -0,0 +1,24 @@
package com.xiaojukeji.know.streaming.km.license;
import com.xiaojukeji.know.streaming.km.common.constant.ApiPrefix;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author didi
*/
@Configuration
public class LicenseWebConfig implements WebMvcConfigurer {
@Autowired
private LicenseInterceptor licenseInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 会进行拦截的接口
registry.addInterceptor(licenseInterceptor).addPathPatterns(ApiPrefix.API_PREFIX + "**");
}
}

View File

@@ -0,0 +1,11 @@
package com.xiaojukeji.know.streaming.km.license.bean;
import lombok.Data;
/**
* @author didi
*/
@Data
public class KmLicense {
private int clusters;
}

View File

@@ -0,0 +1,24 @@
package com.xiaojukeji.know.streaming.km.license.bean;
import lombok.Data;
import java.util.List;
/**
* @author didi
*/
@Data
public class KmLicenseUsageDetail {
/**
* 上报的 ks 的节点
*/
private String host;
/**
* 上报的 ks 的集群的所有的节点
*/
private List<String> hosts;
/**
* 上报的 ks 集群中 kafka 集群数量
*/
private int clusters;
}

View File

@@ -0,0 +1,34 @@
package com.xiaojukeji.know.streaming.km.license.bean;
import lombok.Data;
/**
* @author didi
*/
@Data
public class LicenseInfo<T> {
/**
*
*/
private int status;
/**
* license 过期时间,单位秒
*/
private Long expiredDate;
/**
*
*/
private String app;
/**
*
*/
private String type;
/**
*
*/
private T info;
}

View File

@@ -0,0 +1,12 @@
package com.xiaojukeji.know.streaming.km.license.bean;
import lombok.Data;
/**
* @author didi
*/
@Data
public class LicenseResult<T> {
String err;
T reply;
}

View File

@@ -0,0 +1,24 @@
package com.xiaojukeji.know.streaming.km.license.bean;
import lombok.Data;
/**
* @author didi
*/
@Data
public class LicenseUsage {
/**
* 上报时间戳
*/
private Long timeStamp;
/**
* uuid
*/
private String uuid;
/**
* 业务数据
*/
private String data;
}

View File

@@ -0,0 +1,27 @@
package com.xiaojukeji.know.streaming.km.license.controller;
import com.xiaojukeji.know.streaming.km.common.bean.entity.result.Result;
import com.xiaojukeji.know.streaming.km.common.constant.ApiPrefix;
import com.xiaojukeji.know.streaming.km.license.service.LicenseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
/**
* @author didi
*/
@RestController
@RequestMapping(ApiPrefix.API_V3_PREFIX)
public class LicenseController {
@Autowired
private LicenseService licenseService;
@GetMapping(value = "license")
@ResponseBody
public Result<Void> check() {
return licenseService.check();
}
}

View File

@@ -0,0 +1,18 @@
package com.xiaojukeji.know.streaming.km.license.service;
import com.xiaojukeji.know.streaming.km.common.bean.entity.result.Result;
public interface LicenseService {
/**
* 是否达到了 license 现在的集群数量
* @return
*/
Result<Void> addClusterLimit();
/**
* 校验 license 是否通过
* @return
*/
Result<Void> check();
}

View File

@@ -0,0 +1,253 @@
package com.xiaojukeji.know.streaming.km.license.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.didiglobal.logi.log.ILog;
import com.didiglobal.logi.log.LogFactory;
import com.xiaojukeji.know.streaming.km.common.bean.entity.result.Result;
import com.xiaojukeji.know.streaming.km.common.component.RestTool;
import com.xiaojukeji.know.streaming.km.common.utils.CommonUtils;
import com.xiaojukeji.know.streaming.km.common.utils.NetUtils;
import com.xiaojukeji.know.streaming.km.common.utils.Tuple;
import com.xiaojukeji.know.streaming.km.core.service.cluster.ClusterPhyService;
import com.xiaojukeji.know.streaming.km.core.service.km.KmNodeService;
import com.xiaojukeji.know.streaming.km.license.service.LicenseService;
import com.xiaojukeji.know.streaming.km.license.bean.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class LicenseServiceImpl implements LicenseService {
private static final ILog LOGGER = LogFactory.getLog(LicenseServiceImpl.class);
private static final String LICENSE_INFO_URL = "/api/license/info";
private static final String LICENSE_USAGE_URL = "/api/license/usage";
private static final String LICENSE_HEADER_TOKEN = "x-l-token";
private static final String LICENSE_HEADER_APP = "x-l-app-name";
private static final String LICENSE_HEADER_SIGNATURE = "x-l-signature";
private static final int FAILED_NO_LICENSE = 1000000000;
private static final int FAILED_LICENSE_EXPIRE = 1000000001;
private static final int FAILED_LICENSE_CLUSTER_LIMIT = 1000000002;
private static final int ONE_HOUR = 60 * 60 * 1000;
@Value("${license.server}")
private String licenseSrvUrl;
@Value("${license.signature}")
private String licenseSignature;
@Value("${license.token}")
private String licenseToken;
@Value("${license.app-name}")
private String appName;
@Autowired
private KmNodeService kmNodeService;
@Autowired
private ClusterPhyService clusterPhyService;
@Autowired
private RestTool restTool;
private LicenseInfo<KmLicense> kmLicense;
private List<LicenseUsage> licenseUsages = new ArrayList<>();
@Override
public Result<Void> addClusterLimit() {
//对 LicenseUsage 按照时间挫,从小到大排序,即最新的在最后面
licenseUsages.sort((o1, o2) -> o1.getTimeStamp() < o2.getTimeStamp() ? 1 : -1);
List<KmLicenseUsageDetail> details = licenseUsages.stream()
.map(l -> JSON.parseObject(l.getData(), KmLicenseUsageDetail.class))
.collect(Collectors.toList());
if(CollectionUtils.isEmpty(details)){return Result.buildSuc();}
//Tuple.v1 : ks cluster hosts
//Tuple.v2 : ks 集群管理的 kafka 集群个数
List<Tuple<List<String>, Integer>> ksClusterHostsList = new ArrayList<>();
ksClusterHostsList.add(new Tuple<>(details.get(0).getHosts(), details.get(0).getClusters()));
//根据 hosts 是否有交集,来获取 ks 的集群列表
for(KmLicenseUsageDetail detail : details){
for(Tuple<List<String>, Integer> tuple : ksClusterHostsList){
if(isListIntersection(tuple.getV1(), detail.getHosts())){
tuple.setV1(detail.getHosts());
tuple.setV2(detail.getClusters());
}else {
ksClusterHostsList.add(new Tuple<>(detail.getHosts(), detail.getClusters()));
}
}
}
LOGGER.debug("method=addClusterLimit||details={}||ksClusterHostsList={}",
JSON.toJSONString(details), JSON.toJSONString(ksClusterHostsList));
//计算索引 ks 集群管理的 kafka 集群总个数
final int[] totalKafkaClusterNus = {0};
ksClusterHostsList.stream().forEach(l -> totalKafkaClusterNus[0] += l.getV2() );
if(null == kmLicense) {
return Result.buildFailure(FAILED_NO_LICENSE, "无法获取KS的License信息");
}
if(kmLicense.getInfo().getClusters() < totalKafkaClusterNus[0]) {
return Result.buildFailure(FAILED_LICENSE_CLUSTER_LIMIT, String.format("KS管理的Kafka集群已达到License限制的%d个集群", kmLicense.getInfo().getClusters()));
}
return Result.buildSuc();
}
/**
* 当前这个接口只做最小限度的校验,即 km-license 模块和 license 信息存在,
* 其他异常情况license-srv 临时挂掉不考虑
* check 接口返回的异常 code、msg就在该模块定义不要放到 ResultStatus 中
*/
@Override
public Result<Void> check() {
if(null == kmLicense){
return Result.buildFailure(FAILED_NO_LICENSE, "无法获取KS的license信息");
}
if(System.currentTimeMillis() > kmLicense.getExpiredDate() * 1000){
return Result.buildFailure(FAILED_LICENSE_EXPIRE, "当前KS的license已过期");
}
return Result.buildSuc();
}
@PostConstruct
public void init(){
syncLicenseInfo();
}
/**
* 每10分钟同步一次
*/
@Scheduled(cron="0 0/10 * * * ?")
public void syncLicenseInfo(){
try {
saveLicenseUsageInfo();
List<LicenseUsage> licenseUsages = listLicenseUsageInfo();
if(!CollectionUtils.isEmpty(licenseUsages)){
this.licenseUsages.clear();
this.licenseUsages.addAll(licenseUsages);
}
LicenseInfo<KmLicense> kmLicense = this.getLicenseInfo();
if(null != kmLicense){
this.kmLicense = kmLicense;
}
} catch (Exception e){
LOGGER.error("method=syncLicenseInfo||msg=exception!", e);
}
}
/**************************************************** private method ****************************************************/
private LicenseInfo<KmLicense> getLicenseInfo(){
String url = licenseSrvUrl + LICENSE_INFO_URL;
LicenseResult<String> ret = restTool.getForObject(
url, genHeaders(), new TypeReference<LicenseResult<String>>(){});
LOGGER.debug("method=getLicenseInfo||url={}||ret={}", url, JSON.toJSONString(ret));
if(!StringUtils.isEmpty(ret.getErr())){
return null;
}
byte[] encrypted = Base64.getDecoder().decode(ret.getReply().getBytes(StandardCharsets.UTF_8));
LicenseInfo<KmLicense> info = JSON.parseObject(
new String(encrypted),
new TypeReference<LicenseInfo<KmLicense>>(){}
);
return info;
}
private List<LicenseUsage> listLicenseUsageInfo(){
String url = licenseSrvUrl + LICENSE_USAGE_URL;
LicenseResult<List<LicenseUsage>> ret = restTool.getForObject(
url, genHeaders(), new TypeReference<LicenseResult<List<LicenseUsage>>>(){});
LOGGER.debug("method=listLicenseUsageInfo||url={}||ret={}", url, JSON.toJSONString(ret));
if(!StringUtils.isEmpty(ret.getErr())){
return new ArrayList<>();
}
List<LicenseUsage> licenseUsages = ret.getReply();
if(!CollectionUtils.isEmpty(licenseUsages)){
long now = System.currentTimeMillis();
return licenseUsages.stream()
.filter(l -> l.getTimeStamp() + 6 * ONE_HOUR > now)
.collect(Collectors.toList());
}
return new ArrayList<>();
}
private boolean saveLicenseUsageInfo(){
String host = NetUtils.localHost();
KmLicenseUsageDetail detail = new KmLicenseUsageDetail();
detail.setHost(host);
detail.setHosts(kmNodeService.listKmHosts());
detail.setClusters(clusterPhyService.listAllClusters().size());
LicenseUsage licenseUsage = new LicenseUsage();
licenseUsage.setTimeStamp(System.currentTimeMillis());
licenseUsage.setUuid(CommonUtils.getMD5(host));
licenseUsage.setData(JSON.toJSONString(detail));
Map<String, String> param = new HashMap<>();
param.put("usageSecret", Base64.getEncoder().encodeToString(JSON.toJSONString(licenseUsage).getBytes(StandardCharsets.UTF_8)));
String url = licenseSrvUrl + LICENSE_USAGE_URL;
LicenseResult<Void> ret = restTool.putForObject(url, genHeaders(), JSON.toJSONString(param), LicenseResult.class);
LOGGER.debug("method=saveLicenseUsageInfo||url={}||ret={}", url, JSON.toJSONString(ret));
if(!StringUtils.isEmpty(ret.getErr())){
return false;
}
return true;
}
private HttpHeaders genHeaders(){
HttpHeaders headers = new HttpHeaders();
headers.add(LICENSE_HEADER_TOKEN, licenseToken);
headers.add(LICENSE_HEADER_APP, appName);
headers.add(LICENSE_HEADER_SIGNATURE, licenseSignature);
headers.add("content-type", "application/json");
return headers;
}
/**
* 两个 list 是否相交,是否有相同的内容
* @return
*/
private boolean isListIntersection(List<String> l, List<String> r){
l.retainAll(r);
return !CollectionUtils.isEmpty(l);
}
}