mirror of
https://github.com/didi/KnowStreaming.git
synced 2026-01-10 17:12:11 +08:00
增加rebalance / testing / license能力
This commit is contained in:
40
km-extends/km-license/pom.xml
Normal file
40
km-extends/km-license/pom.xml
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 + "**");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.xiaojukeji.know.streaming.km.license.bean;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author didi
|
||||
*/
|
||||
@Data
|
||||
public class KmLicense {
|
||||
private int clusters;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user