From c71865f6238d146c80e389a962d8c1dd9833419c Mon Sep 17 00:00:00 2001 From: ZQKC Date: Fri, 17 Mar 2023 20:15:05 +0800 Subject: [PATCH 01/37] =?UTF-8?q?[Bugfix]=E4=BF=AE=E5=A4=8DZK=E5=9B=9B?= =?UTF-8?q?=E5=AD=97=E5=91=BD=E4=BB=A4=E8=A7=A3=E6=9E=90=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1、四字命令结果为Float类型的字符串时,使用Long.valueOf()会抛出格式转换失败异常。因此为了方便处理,将使用ConvertUtil.string2Float()方法进行转换。 2、规范调整过程中,涉及到的代码。 --- .../bean/entity/metrics/BaseMetrics.java | 4 +++ .../fourletterword/MonitorCmdData.java | 20 ++++++------ .../fourletterword/ServerCmdData.java | 10 +++--- .../parser/MonitorCmdDataParser.java | 22 ++++++------- .../parser/ServerCmdDataParser.java | 12 +++---- .../impl/ZookeeperMetricServiceImpl.java | 32 +++++++++---------- 6 files changed, 52 insertions(+), 48 deletions(-) diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/metrics/BaseMetrics.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/metrics/BaseMetrics.java index c6ce63512..890a539da 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/metrics/BaseMetrics.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/metrics/BaseMetrics.java @@ -27,6 +27,10 @@ public abstract class BaseMetrics implements Serializable { protected Map metrics = new ConcurrentHashMap<>(); public void putMetric(String key, Float value){ + if (value == null || key == null) { + return; + } + metrics.put(key, value); } diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/zookeeper/fourletterword/MonitorCmdData.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/zookeeper/fourletterword/MonitorCmdData.java index 7e2a10f41..3c862cece 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/zookeeper/fourletterword/MonitorCmdData.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/zookeeper/fourletterword/MonitorCmdData.java @@ -25,15 +25,15 @@ public class MonitorCmdData extends BaseFourLetterWordCmdData { private Float zkAvgLatency; private Float zkMaxLatency; private Float zkMinLatency; - private Long zkPacketsReceived; - private Long zkPacketsSent; - private Long zkNumAliveConnections; - private Long zkOutstandingRequests; + private Float zkPacketsReceived; + private Float zkPacketsSent; + private Float zkNumAliveConnections; + private Float zkOutstandingRequests; private String zkServerState; - private Long zkZnodeCount; - private Long zkWatchCount; - private Long zkEphemeralsCount; - private Long zkApproximateDataSize; - private Long zkOpenFileDescriptorCount; - private Long zkMaxFileDescriptorCount; + private Float zkZnodeCount; + private Float zkWatchCount; + private Float zkEphemeralsCount; + private Float zkApproximateDataSize; + private Float zkOpenFileDescriptorCount; + private Float zkMaxFileDescriptorCount; } diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/zookeeper/fourletterword/ServerCmdData.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/zookeeper/fourletterword/ServerCmdData.java index 0bd9e0a48..350885d33 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/zookeeper/fourletterword/ServerCmdData.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/zookeeper/fourletterword/ServerCmdData.java @@ -20,11 +20,11 @@ public class ServerCmdData extends BaseFourLetterWordCmdData { private Float zkAvgLatency; private Float zkMaxLatency; private Float zkMinLatency; - private Long zkPacketsReceived; - private Long zkPacketsSent; - private Long zkNumAliveConnections; - private Long zkOutstandingRequests; + private Float zkPacketsReceived; + private Float zkPacketsSent; + private Float zkNumAliveConnections; + private Float zkOutstandingRequests; private String zkServerState; - private Long zkZnodeCount; + private Float zkZnodeCount; private Long zkZxid; } diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/zookeeper/fourletterword/parser/MonitorCmdDataParser.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/zookeeper/fourletterword/parser/MonitorCmdDataParser.java index 8c3e6958b..888f0beab 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/zookeeper/fourletterword/parser/MonitorCmdDataParser.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/zookeeper/fourletterword/parser/MonitorCmdDataParser.java @@ -51,7 +51,7 @@ public MonitorCmdData parseAndInitData(Long clusterPhyId, String host, int port, } MonitorCmdData monitorCmdData = new MonitorCmdData(); - dataMap.entrySet().stream().forEach(elem -> { + dataMap.entrySet().forEach(elem -> { try { switch (elem.getKey()) { case "zk_version": @@ -67,37 +67,37 @@ public MonitorCmdData parseAndInitData(Long clusterPhyId, String host, int port, monitorCmdData.setZkMinLatency(ConvertUtil.string2Float(elem.getValue())); break; case "zk_packets_received": - monitorCmdData.setZkPacketsReceived(Long.valueOf(elem.getValue())); + monitorCmdData.setZkPacketsReceived(ConvertUtil.string2Float(elem.getValue())); break; case "zk_packets_sent": - monitorCmdData.setZkPacketsSent(Long.valueOf(elem.getValue())); + monitorCmdData.setZkPacketsSent(ConvertUtil.string2Float(elem.getValue())); break; case "zk_num_alive_connections": - monitorCmdData.setZkNumAliveConnections(Long.valueOf(elem.getValue())); + monitorCmdData.setZkNumAliveConnections(ConvertUtil.string2Float(elem.getValue())); break; case "zk_outstanding_requests": - monitorCmdData.setZkOutstandingRequests(Long.valueOf(elem.getValue())); + monitorCmdData.setZkOutstandingRequests(ConvertUtil.string2Float(elem.getValue())); break; case "zk_server_state": monitorCmdData.setZkServerState(elem.getValue()); break; case "zk_znode_count": - monitorCmdData.setZkZnodeCount(Long.valueOf(elem.getValue())); + monitorCmdData.setZkZnodeCount(ConvertUtil.string2Float(elem.getValue())); break; case "zk_watch_count": - monitorCmdData.setZkWatchCount(Long.valueOf(elem.getValue())); + monitorCmdData.setZkWatchCount(ConvertUtil.string2Float(elem.getValue())); break; case "zk_ephemerals_count": - monitorCmdData.setZkEphemeralsCount(Long.valueOf(elem.getValue())); + monitorCmdData.setZkEphemeralsCount(ConvertUtil.string2Float(elem.getValue())); break; case "zk_approximate_data_size": - monitorCmdData.setZkApproximateDataSize(Long.valueOf(elem.getValue())); + monitorCmdData.setZkApproximateDataSize(ConvertUtil.string2Float(elem.getValue())); break; case "zk_open_file_descriptor_count": - monitorCmdData.setZkOpenFileDescriptorCount(Long.valueOf(elem.getValue())); + monitorCmdData.setZkOpenFileDescriptorCount(ConvertUtil.string2Float(elem.getValue())); break; case "zk_max_file_descriptor_count": - monitorCmdData.setZkMaxFileDescriptorCount(Long.valueOf(elem.getValue())); + monitorCmdData.setZkMaxFileDescriptorCount(ConvertUtil.string2Float(elem.getValue())); break; case "Proposal sizes last/min/max": case "zk_fsync_threshold_exceed_count": diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/zookeeper/fourletterword/parser/ServerCmdDataParser.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/zookeeper/fourletterword/parser/ServerCmdDataParser.java index e16fbdb0d..0355272d8 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/zookeeper/fourletterword/parser/ServerCmdDataParser.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/zookeeper/fourletterword/parser/ServerCmdDataParser.java @@ -46,7 +46,7 @@ public ServerCmdData parseAndInitData(Long clusterPhyId, String host, int port, } ServerCmdData serverCmdData = new ServerCmdData(); - dataMap.entrySet().stream().forEach(elem -> { + dataMap.entrySet().forEach(elem -> { try { switch (elem.getKey()) { case "Zookeeper version": @@ -59,22 +59,22 @@ public ServerCmdData parseAndInitData(Long clusterPhyId, String host, int port, serverCmdData.setZkMaxLatency(ConvertUtil.string2Float(data[2])); break; case "Received": - serverCmdData.setZkPacketsReceived(Long.valueOf(elem.getValue())); + serverCmdData.setZkPacketsReceived(ConvertUtil.string2Float(elem.getValue())); break; case "Sent": - serverCmdData.setZkPacketsSent(Long.valueOf(elem.getValue())); + serverCmdData.setZkPacketsSent(ConvertUtil.string2Float(elem.getValue())); break; case "Connections": - serverCmdData.setZkNumAliveConnections(Long.valueOf(elem.getValue())); + serverCmdData.setZkNumAliveConnections(ConvertUtil.string2Float(elem.getValue())); break; case "Outstanding": - serverCmdData.setZkOutstandingRequests(Long.valueOf(elem.getValue())); + serverCmdData.setZkOutstandingRequests(ConvertUtil.string2Float(elem.getValue())); break; case "Mode": serverCmdData.setZkServerState(elem.getValue()); break; case "Node count": - serverCmdData.setZkZnodeCount(Long.valueOf(elem.getValue())); + serverCmdData.setZkZnodeCount(ConvertUtil.string2Float(elem.getValue())); break; case "Zxid": serverCmdData.setZkZxid(Long.parseUnsignedLong(elem.getValue().trim().substring(2), 16)); diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/zookeeper/impl/ZookeeperMetricServiceImpl.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/zookeeper/impl/ZookeeperMetricServiceImpl.java index bd41f43bc..07844c6b1 100644 --- a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/zookeeper/impl/ZookeeperMetricServiceImpl.java +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/zookeeper/impl/ZookeeperMetricServiceImpl.java @@ -161,7 +161,7 @@ public Result> listMetricsFromES(Long clusterPhyId, MetricDTO // 格式转化 List voList = new ArrayList<>(); - pointVOMap.entrySet().stream().forEach(entry -> + pointVOMap.entrySet().forEach(entry -> voList.add(new MetricLineVO(String.valueOf(clusterPhyId), entry.getKey(), entry.getValue())) ); return Result.buildSuc(voList); @@ -208,11 +208,11 @@ private Result getMetricFromServerCmd(VersionItemParam metricP metrics.putMetric(ZOOKEEPER_METRIC_AVG_REQUEST_LATENCY, cmdData.getZkAvgLatency()); metrics.putMetric(ZOOKEEPER_METRIC_MIN_REQUEST_LATENCY, cmdData.getZkMinLatency()); metrics.putMetric(ZOOKEEPER_METRIC_MAX_REQUEST_LATENCY, cmdData.getZkMaxLatency()); - metrics.putMetric(ZOOKEEPER_METRIC_OUTSTANDING_REQUESTS, cmdData.getZkOutstandingRequests().floatValue()); - metrics.putMetric(ZOOKEEPER_METRIC_NODE_COUNT, cmdData.getZkZnodeCount().floatValue()); - metrics.putMetric(ZOOKEEPER_METRIC_NUM_ALIVE_CONNECTIONS, cmdData.getZkNumAliveConnections().floatValue()); - metrics.putMetric(ZOOKEEPER_METRIC_PACKETS_RECEIVED, cmdData.getZkPacketsReceived().floatValue()); - metrics.putMetric(ZOOKEEPER_METRIC_PACKETS_SENT, cmdData.getZkPacketsSent().floatValue()); + metrics.putMetric(ZOOKEEPER_METRIC_OUTSTANDING_REQUESTS, cmdData.getZkOutstandingRequests()); + metrics.putMetric(ZOOKEEPER_METRIC_NODE_COUNT, cmdData.getZkZnodeCount()); + metrics.putMetric(ZOOKEEPER_METRIC_NUM_ALIVE_CONNECTIONS, cmdData.getZkNumAliveConnections()); + metrics.putMetric(ZOOKEEPER_METRIC_PACKETS_RECEIVED, cmdData.getZkPacketsReceived()); + metrics.putMetric(ZOOKEEPER_METRIC_PACKETS_SENT, cmdData.getZkPacketsSent()); return Result.buildSuc(metrics); } @@ -257,16 +257,16 @@ private Result getMetricFromMonitorCmd(VersionItemParam metric metrics.putMetric(ZOOKEEPER_METRIC_AVG_REQUEST_LATENCY, cmdData.getZkAvgLatency()); metrics.putMetric(ZOOKEEPER_METRIC_MIN_REQUEST_LATENCY, cmdData.getZkMinLatency()); metrics.putMetric(ZOOKEEPER_METRIC_MAX_REQUEST_LATENCY, cmdData.getZkMaxLatency()); - metrics.putMetric(ZOOKEEPER_METRIC_OUTSTANDING_REQUESTS, cmdData.getZkOutstandingRequests().floatValue()); - metrics.putMetric(ZOOKEEPER_METRIC_NODE_COUNT, cmdData.getZkZnodeCount().floatValue()); - metrics.putMetric(ZOOKEEPER_METRIC_WATCH_COUNT, cmdData.getZkWatchCount().floatValue()); - metrics.putMetric(ZOOKEEPER_METRIC_NUM_ALIVE_CONNECTIONS, cmdData.getZkNumAliveConnections().floatValue()); - metrics.putMetric(ZOOKEEPER_METRIC_PACKETS_RECEIVED, cmdData.getZkPacketsReceived().floatValue()); - metrics.putMetric(ZOOKEEPER_METRIC_PACKETS_SENT, cmdData.getZkPacketsSent().floatValue()); - metrics.putMetric(ZOOKEEPER_METRIC_EPHEMERALS_COUNT, cmdData.getZkEphemeralsCount().floatValue()); - metrics.putMetric(ZOOKEEPER_METRIC_APPROXIMATE_DATA_SIZE, cmdData.getZkApproximateDataSize().floatValue()); - metrics.putMetric(ZOOKEEPER_METRIC_OPEN_FILE_DESCRIPTOR_COUNT, cmdData.getZkOpenFileDescriptorCount().floatValue()); - metrics.putMetric(ZOOKEEPER_METRIC_MAX_FILE_DESCRIPTOR_COUNT, cmdData.getZkMaxFileDescriptorCount().floatValue()); + metrics.putMetric(ZOOKEEPER_METRIC_OUTSTANDING_REQUESTS, cmdData.getZkOutstandingRequests()); + metrics.putMetric(ZOOKEEPER_METRIC_NODE_COUNT, cmdData.getZkZnodeCount()); + metrics.putMetric(ZOOKEEPER_METRIC_WATCH_COUNT, cmdData.getZkWatchCount()); + metrics.putMetric(ZOOKEEPER_METRIC_NUM_ALIVE_CONNECTIONS, cmdData.getZkNumAliveConnections()); + metrics.putMetric(ZOOKEEPER_METRIC_PACKETS_RECEIVED, cmdData.getZkPacketsReceived()); + metrics.putMetric(ZOOKEEPER_METRIC_PACKETS_SENT, cmdData.getZkPacketsSent()); + metrics.putMetric(ZOOKEEPER_METRIC_EPHEMERALS_COUNT, cmdData.getZkEphemeralsCount()); + metrics.putMetric(ZOOKEEPER_METRIC_APPROXIMATE_DATA_SIZE, cmdData.getZkApproximateDataSize()); + metrics.putMetric(ZOOKEEPER_METRIC_OPEN_FILE_DESCRIPTOR_COUNT, cmdData.getZkOpenFileDescriptorCount()); + metrics.putMetric(ZOOKEEPER_METRIC_MAX_FILE_DESCRIPTOR_COUNT, cmdData.getZkMaxFileDescriptorCount()); return Result.buildSuc(metrics); } From 769c2c0fbc56b259330fcc7a2a00b987b647588c Mon Sep 17 00:00:00 2001 From: ZQKC Date: Fri, 17 Mar 2023 20:34:50 +0800 Subject: [PATCH 02/37] =?UTF-8?q?[Bugfix]=E4=BF=AE=E5=A4=8DConsumerAssignm?= =?UTF-8?q?ent=E7=B1=BB=E5=9E=8B=E8=BD=AC=E6=8D=A2=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1、问题 KSGroupDescription 的 KSMemberBaseAssignment 对象,转 KSMemberConsumerAssignment 时,会出现转换失败的错误。 2、原因 KSPartialKafkaAdminClient 在返回 KSMemberDescription 时,当 ConsumerGroup 的 memberAssignment.length() <= 0 时,遗漏对 memberBaseAssignment 对象进行初始化。 3、解决 发现 memberAssignment.length() <= 0 时,主动将 KSMemberDescription 中的 memberBaseAssignment 赋值为 KSMemberConsumerAssignment 对象。 --- .../know/streaming/km/biz/group/impl/GroupManagerImpl.java | 3 ++- .../km/common/utils/kafka/KSPartialKafkaAdminClient.java | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/impl/GroupManagerImpl.java b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/impl/GroupManagerImpl.java index 17216793d..fd05c2181 100644 --- a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/impl/GroupManagerImpl.java +++ b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/impl/GroupManagerImpl.java @@ -168,9 +168,10 @@ public PaginationResult pagingGroupTopicConsumedMetr // 转换存储格式 Map tpMemberMap = new HashMap<>(); - //如果不是connect集群 + // 如果不是connect集群 if (!groupDescription.protocolType().equals(CONNECT_CLUSTER_PROTOCOL_TYPE)) { for (KSMemberDescription description : groupDescription.members()) { + // 如果是 Consumer 的 Description ,则 Assignment 的类型为 KSMemberConsumerAssignment 的 KSMemberConsumerAssignment assignment = (KSMemberConsumerAssignment) description.assignment(); for (TopicPartition tp : assignment.topicPartitions()) { tpMemberMap.put(tp, description); diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/utils/kafka/KSPartialKafkaAdminClient.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/utils/kafka/KSPartialKafkaAdminClient.java index f985fb01c..db8093058 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/utils/kafka/KSPartialKafkaAdminClient.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/utils/kafka/KSPartialKafkaAdminClient.java @@ -1338,6 +1338,8 @@ void handleResponse(AbstractResponse abstractResponse) { if (groupMember.memberAssignment().length > 0) { final Assignment assignment = ConsumerProtocol.deserializeAssignment(ByteBuffer.wrap(groupMember.memberAssignment())); memberBaseAssignment = new KSMemberConsumerAssignment(new HashSet<>(assignment.partitions())); + } else { + memberBaseAssignment = new KSMemberConsumerAssignment(new HashSet<>()); } } else { ConnectProtocol.Assignment assignment = null; From 13641c00ba5d0b65ecad407a119564752aaa0ae8 Mon Sep 17 00:00:00 2001 From: ZQKC Date: Mon, 3 Apr 2023 11:45:56 +0800 Subject: [PATCH 03/37] =?UTF-8?q?[Bugfix]=E4=BF=AE=E5=A4=8DBroker=E5=85=83?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E8=A7=A3=E6=9E=90=E6=96=B9=E6=B3=95=E6=9C=AA?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E5=AF=BC=E8=87=B4=E6=8E=A5=E5=85=A5=E9=9B=86?= =?UTF-8?q?=E7=BE=A4=E5=A4=B1=E8=B4=A5=E7=9A=84=E9=97=AE=E9=A2=98(#986)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/KafkaZKDAOImpl.java | 5 +- .../znode/brokers/BrokerMetadata.java | 124 ++++++++++++++---- 2 files changed, 102 insertions(+), 27 deletions(-) diff --git a/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/kafka/zookeeper/service/impl/KafkaZKDAOImpl.java b/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/kafka/zookeeper/service/impl/KafkaZKDAOImpl.java index 82cb8130b..b56c53d6f 100644 --- a/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/kafka/zookeeper/service/impl/KafkaZKDAOImpl.java +++ b/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/kafka/zookeeper/service/impl/KafkaZKDAOImpl.java @@ -7,6 +7,7 @@ import com.xiaojukeji.know.streaming.km.common.bean.entity.kafkacontroller.KafkaController; import com.xiaojukeji.know.streaming.km.common.bean.entity.result.ResultStatus; import com.xiaojukeji.know.streaming.km.common.bean.entity.topic.Topic; +import com.xiaojukeji.know.streaming.km.common.constant.Constant; import com.xiaojukeji.know.streaming.km.common.enums.topic.TopicTypeEnum; import com.xiaojukeji.know.streaming.km.common.exception.AdminOperateException; import com.xiaojukeji.know.streaming.km.common.exception.NotExistException; @@ -78,7 +79,7 @@ public Broker getBrokerMetadata(Long clusterPhyId, Integer brokerId) throws NotE try { BrokerMetadata metadata = this.getData(kafkaZkClient.currentZooKeeper(), BrokerIdZNode.path(brokerId), false, BrokerMetadata.class); - BrokerMetadata.parseAndUpdateBrokerMetadata(metadata); + return this.convert2Broker(clusterPhyId, brokerId, metadata); } catch (KeeperException ke) { logger.error("method=getBrokerMetadata||clusterPhyId={}||brokerId={}||errMsg=exception", clusterPhyId, brokerId, ke); @@ -279,7 +280,7 @@ private Broker convert2Broker(Long clusterPhyId, Integer brokerId, BrokerMetadat metadata.setJmxPort(brokerMetadata.getJmxPort()); metadata.setStartTimestamp(brokerMetadata.getTimestamp()); metadata.setRack(brokerMetadata.getRack()); - metadata.setStatus(1); + metadata.setStatus(Constant.ALIVE); metadata.setEndpointMap(brokerMetadata.getEndpointMap()); return metadata; } diff --git a/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/kafka/zookeeper/znode/brokers/BrokerMetadata.java b/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/kafka/zookeeper/znode/brokers/BrokerMetadata.java index 3b252c5f4..a944d4b96 100644 --- a/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/kafka/zookeeper/znode/brokers/BrokerMetadata.java +++ b/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/kafka/zookeeper/znode/brokers/BrokerMetadata.java @@ -1,12 +1,11 @@ package com.xiaojukeji.know.streaming.km.persistence.kafka.zookeeper.znode.brokers; -import com.fasterxml.jackson.annotation.JsonIgnore; +import com.alibaba.fastjson.JSON; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.xiaojukeji.know.streaming.km.common.bean.entity.common.IpPortData; import com.xiaojukeji.know.streaming.km.common.constant.KafkaConstant; import com.xiaojukeji.know.streaming.km.common.utils.ConvertUtil; -import lombok.Data; import java.io.Serializable; import java.util.HashMap; @@ -51,7 +50,6 @@ * } * */ -@Data @JsonIgnoreProperties(ignoreUnknown = true) public class BrokerMetadata implements Serializable { private static final long serialVersionUID = 3918113492423375809L; @@ -74,34 +72,92 @@ public class BrokerMetadata implements Serializable { private String rack; - @JsonIgnore - public String getExternalHost() { - if (!endpointMap.containsKey(KafkaConstant.EXTERNAL_KEY)) { - // external如果不存在,就返回host - return host; + public List getEndpoints() { + return endpoints; + } + + public void setEndpoints(List endpoints) { + this.endpoints = endpoints; + } + + public Map getEndpointMap() { + if (endpointMap == null) { + this.parseBrokerMetadata(); } - return endpointMap.get(KafkaConstant.EXTERNAL_KEY).getIp(); + return endpointMap; + } + + public String getHost() { + if (endpointMap == null) { + this.parseBrokerMetadata(); + } + + return host; + } + + public void setHost(String host) { + this.host = host; } - @JsonIgnore - public String getInternalHost() { - if (!endpointMap.containsKey(KafkaConstant.INTERNAL_KEY)) { - // internal如果不存在,就返回host - return host; + public Integer getPort() { + if (endpointMap == null) { + this.parseBrokerMetadata(); } - return endpointMap.get(KafkaConstant.INTERNAL_KEY).getIp(); + + return port; } - public static void parseAndUpdateBrokerMetadata(BrokerMetadata brokerMetadata) { - brokerMetadata.setEndpointMap(new HashMap<>()); + public void setPort(Integer port) { + this.port = port; + } + + public Integer getJmxPort() { + return jmxPort; + } + + public void setJmxPort(Integer jmxPort) { + this.jmxPort = jmxPort; + } + + public Integer getVersion() { + return version; + } - if (brokerMetadata.getEndpoints().isEmpty()) { + public void setVersion(Integer version) { + this.version = version; + } + + public Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } + + public String getRack() { + return rack; + } + + public void setRack(String rack) { + this.rack = rack; + } + + private synchronized void parseBrokerMetadata() { + if (this.endpointMap != null) { + return; + } + + if (this.endpoints == null || this.endpoints.isEmpty()) { + this.endpointMap = new HashMap<>(0); return; } + Map tempEndpointMap = new HashMap<>(); + // example EXTERNAL://10.179.162.202:7092 - for (String endpoint: brokerMetadata.getEndpoints()) { + for (String endpoint: this.endpoints) { int idx1 = endpoint.indexOf("://"); int idx2 = endpoint.lastIndexOf(":"); if (idx1 == -1 || idx2 == -1 || idx1 == idx2) { @@ -111,19 +167,37 @@ public static void parseAndUpdateBrokerMetadata(BrokerMetadata brokerMetadata) { String brokerHost = endpoint.substring(idx1 + "://".length(), idx2); String brokerPort = endpoint.substring(idx2 + 1); - brokerMetadata.getEndpointMap().put(endpoint.substring(0, idx1), new IpPortData(brokerHost, brokerPort)); + tempEndpointMap.put(endpoint.substring(0, idx1), new IpPortData(brokerHost, brokerPort)); if (KafkaConstant.INTERNAL_KEY.equals(endpoint.substring(0, idx1))) { // 优先使用internal的地址进行展示 - brokerMetadata.setHost(brokerHost); - brokerMetadata.setPort(ConvertUtil.string2Integer(brokerPort)); + this.host = brokerHost; + this.port = ConvertUtil.string2Integer(brokerPort); } - if (null == brokerMetadata.getHost()) { - brokerMetadata.setHost(brokerHost); - brokerMetadata.setPort(ConvertUtil.string2Integer(brokerPort)); + if (null == this.host) { + this.host = brokerHost; + this.port = ConvertUtil.string2Integer(brokerPort); } } + + this.endpointMap = tempEndpointMap; + } + + public static void main(String[] args) { + String str = "{\t\n" + + "\t\"listener_security_protocol_map\":{\"EXTERNAL\":\"SASL_PLAINTEXT\",\"INTERNAL\":\"SASL_PLAINTEXT\"},\n" + + "\t\"endpoints\":[\"EXTERNAL://10.179.162.202:7092\",\"INTERNAL://10.179.162.202:7093\"],\n" + + "\t\"jmx_port\":8099,\n" + + "\t\"host\":null,\n" + + "\t\"timestamp\":\"1627289710439\",\n" + + "\t\"port\":-1,\n" + + "\t\"version\":4\n" + + "}"; + + BrokerMetadata bm = JSON.parseObject(str, BrokerMetadata.class); + System.out.println(bm.getHost()); + System.out.println(JSON.toJSON(bm)); } } From 62f870a3421b973e1f918e2dbf8718c6e125119d Mon Sep 17 00:00:00 2001 From: ZQKC Date: Tue, 11 Apr 2023 10:54:37 +0800 Subject: [PATCH 04/37] =?UTF-8?q?[Optimize]=E4=BC=98=E5=8C=96pom.xml?= =?UTF-8?q?=E4=B8=AD=EF=BC=8CKS=E7=89=88=E6=9C=AC=E7=9A=84=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1、修改后,便于idea识别,否则会一直存在存在错误提示。 --- km-biz/pom.xml | 4 ++-- km-collector/pom.xml | 4 ++-- km-common/pom.xml | 4 ++-- km-console/pom.xml | 4 ++-- km-core/pom.xml | 4 ++-- km-dist/pom.xml | 4 ++-- km-enterprise/km-ha/pom.xml | 4 ++-- km-extends/km-account/pom.xml | 4 ++-- km-extends/km-monitor/pom.xml | 4 ++-- km-persistence/pom.xml | 4 ++-- km-rest/pom.xml | 4 ++-- km-task/pom.xml | 4 ++-- pom.xml | 4 ++-- 13 files changed, 26 insertions(+), 26 deletions(-) diff --git a/km-biz/pom.xml b/km-biz/pom.xml index b8c3457b4..a6db4220c 100644 --- a/km-biz/pom.xml +++ b/km-biz/pom.xml @@ -5,13 +5,13 @@ 4.0.0 com.xiaojukeji.kafka km-biz - ${km.revision} + ${revision} jar km com.xiaojukeji.kafka - ${km.revision} + ${revision} diff --git a/km-collector/pom.xml b/km-collector/pom.xml index e1fc023d4..af1d489b4 100644 --- a/km-collector/pom.xml +++ b/km-collector/pom.xml @@ -5,13 +5,13 @@ 4.0.0 com.xiaojukeji.kafka km-collector - ${km.revision} + ${revision} jar km com.xiaojukeji.kafka - ${km.revision} + ${revision} diff --git a/km-common/pom.xml b/km-common/pom.xml index 63c94a483..17e5619a9 100644 --- a/km-common/pom.xml +++ b/km-common/pom.xml @@ -5,13 +5,13 @@ 4.0.0 com.xiaojukeji.kafka km-common - ${km.revision} + ${revision} jar km com.xiaojukeji.kafka - ${km.revision} + ${revision} diff --git a/km-console/pom.xml b/km-console/pom.xml index 2863160ae..76201e592 100644 --- a/km-console/pom.xml +++ b/km-console/pom.xml @@ -4,13 +4,13 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 km-console - ${km.revision} + ${revision} jar km com.xiaojukeji.kafka - ${km.revision} + ${revision} diff --git a/km-core/pom.xml b/km-core/pom.xml index 896d54d6e..05158736b 100644 --- a/km-core/pom.xml +++ b/km-core/pom.xml @@ -5,13 +5,13 @@ 4.0.0 com.xiaojukeji.kafka km-core - ${km.revision} + ${revision} jar km com.xiaojukeji.kafka - ${km.revision} + ${revision} diff --git a/km-dist/pom.xml b/km-dist/pom.xml index d74390059..d5ed33ab9 100644 --- a/km-dist/pom.xml +++ b/km-dist/pom.xml @@ -4,13 +4,13 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 km-dist - ${km.revision} + ${revision} pom km com.xiaojukeji.kafka - ${km.revision} + ${revision} diff --git a/km-enterprise/km-ha/pom.xml b/km-enterprise/km-ha/pom.xml index 88a2e1e05..3285386a1 100644 --- a/km-enterprise/km-ha/pom.xml +++ b/km-enterprise/km-ha/pom.xml @@ -5,13 +5,13 @@ 4.0.0 com.xiaojukeji.kafka km-ha - ${km.revision} + ${revision} jar km com.xiaojukeji.kafka - ${km.revision} + ${revision} ../../pom.xml diff --git a/km-extends/km-account/pom.xml b/km-extends/km-account/pom.xml index d8f631b45..234e5dd30 100644 --- a/km-extends/km-account/pom.xml +++ b/km-extends/km-account/pom.xml @@ -5,13 +5,13 @@ 4.0.0 com.xiaojukeji.kafka km-account - ${km.revision} + ${revision} jar km com.xiaojukeji.kafka - ${km.revision} + ${revision} ../../pom.xml diff --git a/km-extends/km-monitor/pom.xml b/km-extends/km-monitor/pom.xml index 8db003d70..74f0a3c28 100644 --- a/km-extends/km-monitor/pom.xml +++ b/km-extends/km-monitor/pom.xml @@ -5,13 +5,13 @@ 4.0.0 com.xiaojukeji.kafka km-monitor - ${km.revision} + ${revision} jar km com.xiaojukeji.kafka - ${km.revision} + ${revision} ../../pom.xml diff --git a/km-persistence/pom.xml b/km-persistence/pom.xml index e1b0ff5a5..eeb57bcbb 100644 --- a/km-persistence/pom.xml +++ b/km-persistence/pom.xml @@ -4,13 +4,13 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 km-persistence - ${km.revision} + ${revision} jar km com.xiaojukeji.kafka - ${km.revision} + ${revision} diff --git a/km-rest/pom.xml b/km-rest/pom.xml index b86151e3a..0c69eccc7 100644 --- a/km-rest/pom.xml +++ b/km-rest/pom.xml @@ -4,13 +4,13 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 km-rest - ${km.revision} + ${revision} jar km com.xiaojukeji.kafka - ${km.revision} + ${revision} diff --git a/km-task/pom.xml b/km-task/pom.xml index d07b37b3e..a88442273 100644 --- a/km-task/pom.xml +++ b/km-task/pom.xml @@ -5,13 +5,13 @@ 4.0.0 com.xiaojukeji.kafka km-task - ${km.revision} + ${revision} jar km com.xiaojukeji.kafka - ${km.revision} + ${revision} diff --git a/pom.xml b/pom.xml index 8099b5f2a..d111185b8 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.xiaojukeji.kafka km pom - ${km.revision} + ${revision} org.springframework.boot @@ -15,7 +15,7 @@ - 3.3.0 + 3.3.0 8 8 From 9d0345c9cdc21d4a9359c4ccfc01bef5f7269269 Mon Sep 17 00:00:00 2001 From: ZQKC Date: Tue, 11 Apr 2023 10:57:12 +0800 Subject: [PATCH 05/37] bump jackson version to 2.13.5 --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index d111185b8..4f672e85d 100644 --- a/pom.xml +++ b/pom.xml @@ -38,6 +38,7 @@ 2.3.7.RELEASE 5.3.19 9.0.41 + 2.13.5 1.2.83 From e61c4464104a75a704b651ca76860b6ffe4dda8c Mon Sep 17 00:00:00 2001 From: william <723187978@qq.com> Date: Fri, 14 Apr 2023 18:08:59 +0800 Subject: [PATCH 06/37] =?UTF-8?q?=E6=B5=8B=E8=AF=95=20git=20=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=9D=83=E9=99=90.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8f5262686..de99df791 100644 --- a/README.md +++ b/README.md @@ -155,3 +155,4 @@ PS: 提问请尽量把问题一次性描述清楚,并告知环境信息情况 ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=didi/KnowStreaming&type=Date)](https://star-history.com/#didi/KnowStreaming&Date) + From 3cb1f03668425f47cd20dea699326af97551ca25 Mon Sep 17 00:00:00 2001 From: ZQKC Date: Wed, 19 Apr 2023 15:18:28 +0800 Subject: [PATCH 07/37] =?UTF-8?q?[Optimize]=E8=A1=A5=E5=85=85ES=E9=9B=86?= =?UTF-8?q?=E7=BE=A4Shard=E6=BB=A1=E7=9A=84=E5=BC=82=E5=B8=B8=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1、调整文档的目录结构; 2、补充ES集群Shard满的异常日志; 3、强调说明ES日志所在的位置; --- ...22\346\237\245\346\211\213\345\206\214.md" | 161 ++++++++++++------ 1 file changed, 109 insertions(+), 52 deletions(-) rename "docs/dev_guide/\346\227\240\346\225\260\346\215\256\346\216\222\346\237\245\346\226\207\346\241\243.md" => "docs/dev_guide/\351\241\265\351\235\242\346\227\240\346\225\260\346\215\256\346\216\222\346\237\245\346\211\213\345\206\214.md" (63%) diff --git "a/docs/dev_guide/\346\227\240\346\225\260\346\215\256\346\216\222\346\237\245\346\226\207\346\241\243.md" "b/docs/dev_guide/\351\241\265\351\235\242\346\227\240\346\225\260\346\215\256\346\216\222\346\237\245\346\211\213\345\206\214.md" similarity index 63% rename from "docs/dev_guide/\346\227\240\346\225\260\346\215\256\346\216\222\346\237\245\346\226\207\346\241\243.md" rename to "docs/dev_guide/\351\241\265\351\235\242\346\227\240\346\225\260\346\215\256\346\216\222\346\237\245\346\211\213\345\206\214.md" index fd7886bc8..b8209086c 100644 --- "a/docs/dev_guide/\346\227\240\346\225\260\346\215\256\346\216\222\346\237\245\346\226\207\346\241\243.md" +++ "b/docs/dev_guide/\351\241\265\351\235\242\346\227\240\346\225\260\346\215\256\346\216\222\346\237\245\346\211\213\345\206\214.md" @@ -1,3 +1,10 @@ +![Logo](https://user-images.githubusercontent.com/71620349/185368586-aed82d30-1534-453d-86ff-ecfa9d0f35bd.png) + +# 页面无数据排查手册 + + +--- + ## 1、集群接入错误 ### 1.1、异常现象 @@ -20,52 +27,69 @@ -## 2、JMX连接失败(需使用3.0.1及以上版本) -### 2.1异常现象 -Broker列表的JMX Port列出现红色感叹号,则该Broker的JMX连接异常。 + + +--- + +## 2、JMX连接失败 + +背景:Kafka 通过 JMX 服务进行运行指标的暴露,因此 `KnowStreaming` 会主动连接 Kafka 的 JMX 服务进行指标采集。如果我们发现页面缺少指标,那么可能原因之一是 Kafka 的 JMX 端口配置的有问题导致指标获取失败,进而页面没有数据。 + +### 2.1、正异常现象 + +**1、异常现象** + +Broker 列表的 JMX PORT 列出现红色感叹号,则表示 JMX 连接存在异常。 +**2、正常现象** + +Broker 列表的 JMX PORT 列出现绿色,则表示 JMX 连接正常。 + + + + +--- -#### 2.1.1、原因一:JMX未开启 -##### 2.1.1.1、异常现象 +### 2.2、异因一:JMX未开启 + +#### 2.2.1、异常现象 broker列表的JMX Port值为-1,对应Broker的JMX未开启。 -##### 2.1.1.2、解决方案 +#### 2.2.2、解决方案 开启JMX,开启流程如下: 1、修改kafka的bin目录下面的:`kafka-server-start.sh`文件 -``` +```bash # 在这个下面增加JMX端口的配置 if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then - export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G" - export JMX_PORT=9999 # 增加这个配置, 这里的数值并不一定是要9999 + export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G" + export JMX_PORT=9999 # 增加这个配置, 这里的数值并不一定是要9999 fi ``` - 2、修改kafka的bin目录下面对的:`kafka-run-class.sh`文件 -``` +```bash # JMX settings if [ -z "$KAFKA_JMX_OPTS" ]; then - KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false - -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=${当前机器的IP}" + KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=${当前机器的IP}" fi # JMX port to use if [ $JMX_PORT ]; then - KAFKA_JMX_OPTS="$KAFKA_JMX_OPTS -Dcom.sun.management.jmxremote.port=$JMX_PORT - Dcom.sun.management.jmxremote.rmi.port=$JMX_PORT" + KAFKA_JMX_OPTS="$KAFKA_JMX_OPTS -Dcom.sun.management.jmxremote.port=$JMX_PORT -Dcom.sun.management.jmxremote.rmi.port=$JMX_PORT" fi ``` @@ -74,14 +98,17 @@ fi 3、重启Kafka-Broker。 +--- -#### 2.1.2、原因二:JMX配置错误 -##### 2.1.2.1、异常现象 + +### 2.3、异原二:JMX配置错误 + +#### 2.3.1、异常现象 错误日志: -``` +```log # 错误一: 错误提示的是真实的IP,这样的话基本就是JMX配置的有问题了。 2021-01-27 10:06:20.730 ERROR 50901 --- [ics-Thread-1-62] c.x.k.m.c.utils.jmx.JmxConnectorWrap : JMX connect exception, host:192.168.0.1 port:9999. java.rmi.ConnectException: Connection refused to host: 192.168.0.1; nested exception is: @@ -89,54 +116,59 @@ fi 2021-01-27 10:06:20.730 ERROR 50901 --- [ics-Thread-1-62] c.x.k.m.c.utils.jmx.JmxConnectorWrap : JMX connect exception, host:127.0.0.1 port:9999. java.rmi.ConnectException: Connection refused to host: 127.0.0.1;; nested exception is: ``` - - -##### 2.1.2.2、解决方案 +#### 2.3.2、解决方案 开启JMX,开启流程如下: 1、修改kafka的bin目录下面的:`kafka-server-start.sh`文件 -``` +```bash # 在这个下面增加JMX端口的配置 if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then - export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G" - export JMX_PORT=9999 # 增加这个配置, 这里的数值并不一定是要9999 + export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G" + export JMX_PORT=9999 # 增加这个配置, 这里的数值并不一定是要9999 fi ``` - - 2、修改kafka的bin目录下面对的:`kafka-run-class.sh`文件 -``` +```bash # JMX settings if [ -z "$KAFKA_JMX_OPTS" ]; then - KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false - -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=${当前机器的IP}" + KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=${当前机器的IP}" fi # JMX port to use if [ $JMX_PORT ]; then - KAFKA_JMX_OPTS="$KAFKA_JMX_OPTS -Dcom.sun.management.jmxremote.port=$JMX_PORT - Dcom.sun.management.jmxremote.rmi.port=$JMX_PORT" + KAFKA_JMX_OPTS="$KAFKA_JMX_OPTS -Dcom.sun.management.jmxremote.port=$JMX_PORT -Dcom.sun.management.jmxremote.rmi.port=$JMX_PORT" fi ``` +3、重启Kafka-Broker。 -3、重启Kafka-Broker。 +--- +### 2.4、异因三:JMX开启SSL -#### 2.1.3、原因三:JMX开启SSL +#### 2.4.1、异常现象 -##### 2.1.3.1、解决方案 +```log +# 连接JMX的日志中,出现SSL认证失败的相关日志。TODO:欢迎补充具体日志案例。 +``` + +#### 2.4.2、解决方案 -#### 2.1.4、原因四:连接了错误IP -##### 2.1.4.1、异常现象 +--- + + +### 2.5、异因四:连接了错误IP + +#### 2.5.1、异常现象 Broker 配置了内外网,而JMX在配置时,可能配置了内网IP或者外网IP,此时`KnowStreaming` 需要连接到特定网络的IP才可以进行访问。 @@ -160,7 +192,7 @@ Broker 配置了内外网,而JMX在配置时,可能配置了内网IP或者 } ``` -##### 2.1.4.2、解决方案 +#### 2.5.2、解决方案 可以手动往`ks_km_physical_cluster`表的`jmx_properties`字段增加一个`useWhichEndpoint`字段,从而控制 `KnowStreaming` 连接到特定的JMX IP及PORT。 @@ -184,49 +216,66 @@ SQL例子: UPDATE ks_km_physical_cluster SET jmx_properties='{ "maxConn": 10, "username": "xxxxx", "password": "xxxx", "openSSL": false , "useWhichEndpoint": "xxx"}' where id={xxx}; ``` -### 2.2、正常情况 -修改完成后,如果看到 JMX PORT这一列全部为绿色,则表示JMX已正常。 +--- - -## 3、Elasticsearch问题 -注意:mac系统在执行curl指令时,可能报zsh错误。可参考以下操作。 -``` + + + +## 3、ElasticSearch问题 + +**背景:** +`KnowStreaming` 将从 Kafka 中采集到的指标存储到 ES 中,如果 ES 存在问题,则也可能会导致页面出现无数据的情况。 + +**日志:** +`KnowStreaming` 读写 ES 相关日志,在 `logs/es/es.log` 中! + + +**注意:** +mac系统在执行curl指令时,可能报zsh错误。可参考以下操作。 + +```bash 1 进入.zshrc 文件 vim ~/.zshrc 2.在.zshrc中加入 setopt no_nomatch 3.更新配置 source ~/.zshrc ``` -### 3.1、原因一:缺少索引 +--- + +### 3.1、异因一:缺少索引 #### 3.1.1、异常现象 报错信息 -``` +```log +# 日志位置 logs/es/es.log com.didiglobal.logi.elasticsearch.client.model.exception.ESIndexNotFoundException: method [GET], host[http://127.0.0.1:9200], URI [/ks_kafka_broker_metric_2022-10-21,ks_kafka_broker_metric_2022-10-22/_search], status line [HTTP/1.1 404 Not Found] ``` -curl http://{ES的IP地址}:{ES的端口号}/_cat/indices/ks_kafka* 查看KS索引列表,发现没有索引。 + +`curl http://{ES的IP地址}:{ES的端口号}/_cat/indices/ks_kafka*` 查看KS索引列表,发现没有索引。 #### 3.1.2、解决方案 执行 [ES索引及模版初始化](https://github.com/didi/KnowStreaming/blob/master/bin/init_es_template.sh) 脚本,来创建索引及模版。 +--- + -### 3.2、原因二:索引模板错误 +### 3.2、异因二:索引模板错误 #### 3.2.1、异常现象 多集群列表有数据,集群详情页图标无数据。查询KS索引模板列表,发现不存在。 -``` +```bash curl {ES的IP地址}:{ES的端口号}/_cat/templates/ks_kafka*?v&h=name ``` @@ -240,7 +289,7 @@ curl {ES的IP地址}:{ES的端口号}/_cat/templates/ks_kafka*?v&h=name 删除KS索引模板和索引 -``` +```bash curl -XDELETE {ES的IP地址}:{ES的端口号}/ks_kafka* curl -XDELETE {ES的IP地址}:{ES的端口号}/_template/ks_kafka* ``` @@ -248,23 +297,29 @@ curl -XDELETE {ES的IP地址}:{ES的端口号}/_template/ks_kafka* 执行 [ES索引及模版初始化](https://github.com/didi/KnowStreaming/blob/master/bin/init_es_template.sh) 脚本,来创建索引及模版。 -### 3.3、原因三:集群Shard满 +--- + + +### 3.3、异因三:集群Shard满 #### 3.3.1、异常现象 报错信息 -``` -com.didiglobal.logi.elasticsearch.client.model.exception.ESIndexNotFoundException: method [GET], host[http://127.0.0.1:9200], URI [/ks_kafka_broker_metric_2022-10-21,ks_kafka_broker_metric_2022-10-22/_search], status line [HTTP/1.1 404 Not Found] +```log +# 日志位置 logs/es/es.log + +{"error":{"root_cause":[{"type":"validation_exception","reason":"Validation Failed: 1: this action would add [4] total shards, but this cluster currently has [1000]/[1000] maximum shards open;"}],"type":"validation_exception","reason":"Validation Failed: 1: this action would add [4] total shards, but this cluster currently has [1000]/[1000] maximum shards open;"},"status":400} ``` 尝试手动创建索引失败。 -``` +```bash #创建ks_kafka_cluster_metric_test索引的指令 curl -s -XPUT http://{ES的IP地址}:{ES的端口号}/ks_kafka_cluster_metric_test ``` + #### 3.3.2、解决方案 ES索引的默认分片数量为1000,达到数量以后,索引创建失败。 @@ -276,10 +331,12 @@ curl -XPUT -H"content-type:application/json" http://{ES的IP地址}:{ES的端 { "persistent": { "cluster": { - "max_shards_per_node":{索引上限,默认为1000} + "max_shards_per_node":{索引上限,默认为1000, 测试时可以将其调整为10000} } } }' ``` 执行 [ES索引及模版初始化](https://github.com/didi/KnowStreaming/blob/master/bin/init_es_template.sh) 脚本,来补全索引。 + + From 6aaa4b34b86ba4bd938a64b211c1f4c62368e2d0 Mon Sep 17 00:00:00 2001 From: ZQKC Date: Wed, 19 Apr 2023 15:21:39 +0800 Subject: [PATCH 08/37] =?UTF-8?q?=E8=A1=A5=E5=85=85=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E6=97=A0=E6=95=B0=E6=8D=AE=E6=8E=92=E6=9F=A5=E6=89=8B=E5=86=8C?= =?UTF-8?q?=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index de99df791..d95a05874 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ - [单机部署手册](docs/install_guide/单机部署手册.md) - [版本升级手册](docs/install_guide/版本升级手册.md) - [本地源码启动手册](docs/dev_guide/本地源码启动手册.md) +- [页面无数据排查手册](docs/dev_guide/页面无数据排查手册.md) **`产品相关手册`** From 82fbea4e5f6ab79ba4781b069791016f3d90a41c Mon Sep 17 00:00:00 2001 From: ZQKC Date: Sun, 23 Apr 2023 11:35:44 +0800 Subject: [PATCH 09/37] =?UTF-8?q?[Doc]=E8=A1=A5=E5=85=85zk=5Fproperties?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E7=9A=84=E4=BD=BF=E7=94=A8=E8=AF=B4=E6=98=8E?= =?UTF-8?q?(#995)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1、补充 zk_properties 字段 使用说明; 2、补充 Digest-MD5 认证例子; 3、调整 Kerberos 认证说明; --- ...0\257\201Kafka\351\233\206\347\276\244.md" | 180 ++++++++++++++++++ ...\350\256\244\350\257\201\347\232\204ZK.md" | 69 ------- 2 files changed, 180 insertions(+), 69 deletions(-) create mode 100644 "docs/dev_guide/\346\216\245\345\205\245ZK\345\270\246\350\256\244\350\257\201Kafka\351\233\206\347\276\244.md" delete mode 100644 "docs/dev_guide/\346\224\257\346\214\201Kerberos\350\256\244\350\257\201\347\232\204ZK.md" diff --git "a/docs/dev_guide/\346\216\245\345\205\245ZK\345\270\246\350\256\244\350\257\201Kafka\351\233\206\347\276\244.md" "b/docs/dev_guide/\346\216\245\345\205\245ZK\345\270\246\350\256\244\350\257\201Kafka\351\233\206\347\276\244.md" new file mode 100644 index 000000000..294a4742f --- /dev/null +++ "b/docs/dev_guide/\346\216\245\345\205\245ZK\345\270\246\350\256\244\350\257\201Kafka\351\233\206\347\276\244.md" @@ -0,0 +1,180 @@ + +![Logo](https://user-images.githubusercontent.com/71620349/185368586-aed82d30-1534-453d-86ff-ecfa9d0f35bd.png) + +--- + +# 接入 ZK 带认证的 Kafka 集群 + +- [接入 ZK 带认证的 Kafka 集群](#接入-zk-带认证的-kafka-集群) + - [1、简要说明](#1简要说明) + - [2、支持 Digest-MD5 认证](#2支持-digest-md5-认证) + - [3、支持 Kerberos 认证](#3支持-kerberos-认证) + + + +## 1、简要说明 + +- 1、当前 KnowStreaming 暂无页面可以直接配置 ZK 的认证信息,但是 KnowStreaming 的后端预留了 MySQL 的字段用于存储 ZK 的认证信息,用户可通过将认证信息存储至该字段,从而达到支持接入 ZK 带认证的 Kafka 集群。 +  + +- 2、该字段位于 MySQL 库 ks_km_physical_cluster 表中的 zk_properties 字段,该字段的格式是: +```json +{ + "openSecure": false, # 是否开启认证,开启时配置为true + "sessionTimeoutUnitMs": 15000, # session超时时间 + "requestTimeoutUnitMs": 5000, # request超时时间 + "otherProps": { # 其他配置,认证信息主要配置在该位置 + "zookeeper.sasl.clientconfig": "kafkaClusterZK1" # 例子, + } +} +``` + +- 3、实际生效的代码位置 +```java +// 代码位置:https://github.com/didi/KnowStreaming/blob/master/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/kafka/KafkaAdminZKClient.java + +kafkaZkClient = KafkaZkClient.apply( + clusterPhy.getZookeeper(), + zkConfig.getOpenSecure(), // 是否开启认证,开启时配置为true + zkConfig.getSessionTimeoutUnitMs(), // session超时时间 + zkConfig.getRequestTimeoutUnitMs(), // request超时时间 + 5, + Time.SYSTEM, + "KS-ZK-ClusterPhyId-" + clusterPhyId, + "KS-ZK-SessionExpireListener-clusterPhyId-" + clusterPhyId, + Option.apply("KS-ZK-ClusterPhyId-" + clusterPhyId), + Option.apply(this.getZKConfig(clusterPhyId, zkConfig.getOtherProps())) // 其他配置,认证信息主要配置在该位置 +); +``` + +- 4、SQL例子 +```sql +update ks_km_physical_cluster set zk_properties='{ "openSecure": true, "otherProps": { "zookeeper.sasl.clientconfig": "kafkaClusterZK1" } }' where id=集群1的ID; +``` + + +- 5、zk_properties 字段不能覆盖所有的场景,所以实际使用过程中还可能需要在此基础之上,进行其他的调整。比如,`Digest-MD5 认证` 和 `Kerberos 认证` 都还需要修改启动脚本等。后续看能否通过修改 ZK 客户端的源码,使得 ZK 认证的相关配置能和 Kafka 认证的配置一样方便。 + + +--- + + +## 2、支持 Digest-MD5 认证 + +1. 假设你有两个 Kafka 集群, 对应两个 ZK 集群; +2. 两个 ZK 集群的认证信息如下所示 + +```bash +# ZK1集群的认证信息,这里的 kafkaClusterZK1 可以是随意的名称,只需要和后续数据库的配置对应上即可。 +kafkaClusterZK1 { + org.apache.zookeeper.server.auth.DigestLoginModule required + username="zk1" + password="zk1-passwd"; +}; + +# ZK2集群的认证信息,这里的 kafkaClusterZK2 可以是随意的名称,只需要和后续数据库的配置对应上即可。 +kafkaClusterZK2 { + org.apache.zookeeper.server.auth.DigestLoginModule required + username="zk2" + password="zk2-passwd"; +}; +``` + +3. 将这两个ZK集群的认证信息存储到 `/xxx/zk_client_jaas.conf` 文件中,文件中的内容如下所示: + +```bash +kafkaClusterZK1 { + org.apache.zookeeper.server.auth.DigestLoginModule required + username="zk1" + password="zk1-passwd"; +}; + +kafkaClusterZK2 { + org.apache.zookeeper.server.auth.DigestLoginModule required + username="zk2" + password="zk2-passwd"; +}; + +``` + +4. 修改 KnowStreaming 的启动脚本 + +```bash +# `KnowStreaming/bin/startup.sh` 中的 47 行的 JAVA_OPT 中追加如下设置 + +-Djava.security.auth.login.config=/xxx/zk_client_jaas.conf +``` + +5. 修改 KnowStreaming 的表数据 + +```sql +# 这里的 kafkaClusterZK1 要和 /xxx/zk_client_jaas.conf 中的对应上 +update ks_km_physical_cluster set zk_properties='{ "openSecure": true, "otherProps": { "zookeeper.sasl.clientconfig": "kafkaClusterZK1" } }' where id=集群1的ID; + +update ks_km_physical_cluster set zk_properties='{ "openSecure": true, "otherProps": { "zookeeper.sasl.clientconfig": "kafkaClusterZK2" } }' where id=集群2的ID; +``` + +6. 重启 KnowStreaming + + +--- + + +## 3、支持 Kerberos 认证 + +**第一步:查看用户在ZK的ACL** + +假设我们使用的用户是 `kafka` 这个用户。 + +- 1、查看 server.properties 的配置的 zookeeper.connect 的地址; +- 2、使用 `zkCli.sh -serve zookeeper.connect的地址` 登录到ZK页面; +- 3、ZK页面上,执行命令 `getAcl /kafka` 查看 `kafka` 用户的权限; + +此时,我们可以看到如下信息: +![watch_user_acl.png](assets/support_kerberos_zk/watch_user_acl.png) + +`kafka` 用户需要的权限是 `cdrwa`。如果用户没有 `cdrwa` 权限的话,需要创建用户并授权,授权命令为:`setAcl` + + +**第二步:创建Kerberos的keytab并修改 KnowStreaming 主机** + +- 1、在 Kerberos 的域中创建 `kafka/_HOST` 的 `keytab`,并导出。例如:`kafka/dbs-kafka-test-8-53`; +- 2、导出 keytab 后上传到安装 KS 的机器的 `/etc/keytab` 下; +- 3、在 KS 机器上,执行 `kinit -kt zookeepe.keytab kafka/dbs-kafka-test-8-53` 看是否能进行 `Kerberos` 登录; +- 4、可以登录后,配置 `/opt/zookeeper.jaas` 文件,例子如下: +```bash +Client { + com.sun.security.auth.module.Krb5LoginModule required + useKeyTab=true + storeKey=false + serviceName="zookeeper" + keyTab="/etc/keytab/zookeeper.keytab" + principal="kafka/dbs-kafka-test-8-53@XXX.XXX.XXX"; +}; +``` +- 5、需要配置 `KDC-Server` 对 `KnowStreaming` 的机器开通防火墙,并在KS的机器 `/etc/host/` 配置 `kdc-server` 的 `hostname`。并将 `krb5.conf` 导入到 `/etc` 下; + + +**第三步:修改 KnowStreaming 的配置** + +- 1、修改数据库,开启ZK的认证 +```sql +update ks_km_physical_cluster set zk_properties='{ "openSecure": true }' where id=集群1的ID; +``` + +- 2、在 `KnowStreaming/bin/startup.sh` 中的47行的JAVA_OPT中追加如下设置 +```bash +-Dsun.security.krb5.debug=true -Djava.security.krb5.conf=/etc/krb5.conf -Djava.security.auth.login.config=/opt/zookeeper.jaas +``` + +- 3、重启KS集群后再 start.out 中看到如下信息,则证明Kerberos配置成功; + +![success_1.png](assets/support_kerberos_zk/success_1.png) + +![success_2.png](assets/support_kerberos_zk/success_2.png) + + +**第四步:补充说明** + +- 1、多Kafka集群如果用的是一样的Kerberos域的话,只需在每个`ZK`中给`kafka`用户配置`crdwa`权限即可,这样集群初始化的时候`zkclient`是都可以认证; +- 2、多个Kerberos域暂时未适配; \ No newline at end of file diff --git "a/docs/dev_guide/\346\224\257\346\214\201Kerberos\350\256\244\350\257\201\347\232\204ZK.md" "b/docs/dev_guide/\346\224\257\346\214\201Kerberos\350\256\244\350\257\201\347\232\204ZK.md" deleted file mode 100644 index 116643ba6..000000000 --- "a/docs/dev_guide/\346\224\257\346\214\201Kerberos\350\256\244\350\257\201\347\232\204ZK.md" +++ /dev/null @@ -1,69 +0,0 @@ - -## 支持Kerberos认证的ZK - - -### 1、修改 KnowStreaming 代码 - -代码位置:`src/main/java/com/xiaojukeji/know/streaming/km/persistence/kafka/KafkaAdminZKClient.java` - -将 `createZKClient` 的 `135行 的 false 改为 true -![need_modify_code.png](assets/support_kerberos_zk/need_modify_code.png) - - -修改完后重新进行打包编译,打包编译见:[打包编译](https://github.com/didi/KnowStreaming/blob/master/docs/install_guide/%E6%BA%90%E7%A0%81%E7%BC%96%E8%AF%91%E6%89%93%E5%8C%85%E6%89%8B%E5%86%8C.md -) - - - -### 2、查看用户在ZK的ACL - -假设我们使用的用户是 `kafka` 这个用户。 - -- 1、查看 server.properties 的配置的 zookeeper.connect 的地址; -- 2、使用 `zkCli.sh -serve zookeeper.connect的地址` 登录到ZK页面; -- 3、ZK页面上,执行命令 `getAcl /kafka` 查看 `kafka` 用户的权限; - -此时,我们可以看到如下信息: -![watch_user_acl.png](assets/support_kerberos_zk/watch_user_acl.png) - -`kafka` 用户需要的权限是 `cdrwa`。如果用户没有 `cdrwa` 权限的话,需要创建用户并授权,授权命令为:`setAcl` - - -### 3、创建Kerberos的keytab并修改 KnowStreaming 主机 - -- 1、在 Kerberos 的域中创建 `kafka/_HOST` 的 `keytab`,并导出。例如:`kafka/dbs-kafka-test-8-53`; -- 2、导出 keytab 后上传到安装 KS 的机器的 `/etc/keytab` 下; -- 3、在 KS 机器上,执行 `kinit -kt zookeepe.keytab kafka/dbs-kafka-test-8-53` 看是否能进行 `Kerberos` 登录; -- 4、可以登录后,配置 `/opt/zookeeper.jaas` 文件,例子如下: -```sql -Client { - com.sun.security.auth.module.Krb5LoginModule required - useKeyTab=true - storeKey=false - serviceName="zookeeper" - keyTab="/etc/keytab/zookeeper.keytab" - principal="kafka/dbs-kafka-test-8-53@XXX.XXX.XXX"; -}; -``` -- 5、需要配置 `KDC-Server` 对 `KnowStreaming` 的机器开通防火墙,并在KS的机器 `/etc/host/` 配置 `kdc-server` 的 `hostname`。并将 `krb5.conf` 导入到 `/etc` 下; - - -### 4、修改 KnowStreaming 的配置 - -- 1、在 `/usr/local/KnowStreaming/KnowStreaming/bin/startup.sh` 中的47行的JAVA_OPT中追加如下设置 -```bash --Dsun.security.krb5.debug=true -Djava.security.krb5.conf=/etc/krb5.conf -Djava.security.auth.login.config=/opt/zookeeper.jaas -``` - -- 2、重启KS集群后再 start.out 中看到如下信息,则证明Kerberos配置成功; - -![success_1.png](assets/support_kerberos_zk/success_1.png) - -![success_2.png](assets/support_kerberos_zk/success_2.png) - - -### 5、补充说明 - -- 1、多Kafka集群如果用的是一样的Kerberos域的话,只需在每个`ZK`中给`kafka`用户配置`crdwa`权限即可,这样集群初始化的时候`zkclient`是都可以认证; -- 2、当前需要修改代码重新打包才可以支持,后续考虑通过页面支持Kerberos认证的ZK接入; -- 3、多个Kerberos域暂时未适配; \ No newline at end of file From db044caf8bb0e5ac5ef6cc457bad1b7e174fbb65 Mon Sep 17 00:00:00 2001 From: ZQKC Date: Wed, 26 Apr 2023 16:54:26 +0800 Subject: [PATCH 10/37] =?UTF-8?q?[Optimize]Group=E5=85=83=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E6=9B=B4=E6=96=B0=E4=BC=98=E5=8C=96(#1005)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1、Group元信息未变化时,则不进行updateById操作; 2、失效的Group信息直接删除; --- .../common/bean/po/group/GroupMemberPO.java | 13 ++ .../km/common/bean/po/group/GroupPO.java | 16 +++ .../km/common/converter/GroupConverter.java | 2 + .../km/core/service/group/GroupService.java | 9 +- .../service/group/impl/GroupServiceImpl.java | 122 ++++++++++-------- .../kafka/metadata/SyncKafkaGroupTask.java | 19 +-- 6 files changed, 114 insertions(+), 67 deletions(-) diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/po/group/GroupMemberPO.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/po/group/GroupMemberPO.java index 7992ac17b..432f061c8 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/po/group/GroupMemberPO.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/po/group/GroupMemberPO.java @@ -7,6 +7,7 @@ import lombok.NoArgsConstructor; import java.util.Date; +import java.util.Objects; @Data @NoArgsConstructor @@ -37,4 +38,16 @@ public GroupMemberPO(Long clusterPhyId, String topicName, String groupName, Stri this.memberCount = memberCount; this.updateTime = updateTime; } + + public boolean equal2GroupMemberPO(GroupMemberPO that) { + if (that == null) { + return false; + } + + return Objects.equals(clusterPhyId, that.clusterPhyId) + && Objects.equals(topicName, that.topicName) + && Objects.equals(groupName, that.groupName) + && Objects.equals(state, that.state) + && Objects.equals(memberCount, that.memberCount); + } } \ No newline at end of file diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/po/group/GroupPO.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/po/group/GroupPO.java index 49ac5bf30..53b925d40 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/po/group/GroupPO.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/po/group/GroupPO.java @@ -9,6 +9,8 @@ import lombok.Data; import lombok.NoArgsConstructor; +import java.util.Objects; + @Data @NoArgsConstructor @@ -58,4 +60,18 @@ public class GroupPO extends BasePO { */ private int coordinatorId; + public boolean equal2GroupPO(GroupPO groupPO) { + if (groupPO == null) { + return false; + } + + return coordinatorId == groupPO.coordinatorId + && Objects.equals(clusterPhyId, groupPO.clusterPhyId) + && Objects.equals(type, groupPO.type) + && Objects.equals(name, groupPO.name) + && Objects.equals(state, groupPO.state) + && Objects.equals(memberCount, groupPO.memberCount) + && Objects.equals(topicMembers, groupPO.topicMembers) + && Objects.equals(partitionAssignor, groupPO.partitionAssignor); + } } diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/converter/GroupConverter.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/converter/GroupConverter.java index 131bd2432..c203b3dfe 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/converter/GroupConverter.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/converter/GroupConverter.java @@ -10,6 +10,7 @@ import com.xiaojukeji.know.streaming.km.common.utils.ValidateUtils; import java.util.ArrayList; +import java.util.Date; import java.util.stream.Collectors; /** @@ -57,6 +58,7 @@ public static GroupPO convert2GroupPO(Group group) { po.setTopicMembers(ConvertUtil.obj2Json(group.getTopicMembers())); po.setType(group.getType().getCode()); po.setState(group.getState().getState()); + po.setUpdateTime(new Date()); return po; } } diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/GroupService.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/GroupService.java index 47317c804..c2cb71800 100644 --- a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/GroupService.java +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/GroupService.java @@ -12,9 +12,9 @@ import com.xiaojukeji.know.streaming.km.common.exception.NotExistException; import org.apache.kafka.common.TopicPartition; -import java.util.Date; import java.util.List; import java.util.Map; +import java.util.Set; public interface GroupService { /** @@ -35,10 +35,11 @@ public interface GroupService { /** * 批量更新DB + * @param clusterPhyId 集群ID + * @param newGroupList 新的group列表 + * @param getFailedGroupSet 元信息获取失败的group列表 */ - void batchReplaceGroupsAndMembers(Long clusterPhyId, List newGroupList, long updateTime); - - int deleteByUpdateTimeBeforeInDB(Long clusterPhyId, Date beforeTime); + void batchReplaceGroupsAndMembers(Long clusterPhyId, List newGroupList, Set getFailedGroupSet); /** * DB-Group相关接口 diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/impl/GroupServiceImpl.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/impl/GroupServiceImpl.java index 15dc21081..21511a96b 100644 --- a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/impl/GroupServiceImpl.java +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/impl/GroupServiceImpl.java @@ -49,7 +49,7 @@ @Service public class GroupServiceImpl extends BaseKafkaVersionControlService implements GroupService { - private static final ILog log = LogFactory.getLog(GroupServiceImpl.class); + private static final ILog LOGGER = LogFactory.getLog(GroupServiceImpl.class); @Autowired private GroupDAO groupDAO; @@ -92,7 +92,7 @@ public List listGroupsFromKafka(ClusterPhy clusterPhy) throws AdminOpera return groupNameList; } catch (Exception e) { - log.error("method=listGroupsFromKafka||clusterPhyId={}||errMsg=exception!", clusterPhy.getId(), e); + LOGGER.error("method=listGroupsFromKafka||clusterPhyId={}||errMsg=exception!", clusterPhy.getId(), e); throw new AdminOperateException(e.getMessage(), e, ResultStatus.KAFKA_OPERATE_FAILED); } finally { @@ -142,7 +142,8 @@ public Group getGroupFromKafka(ClusterPhy clusterPhy, String groupName) throws N member.setMemberCount(member.getMemberCount() + 1); } } - group.setTopicMembers(memberMap.values().stream().collect(Collectors.toList())); + + group.setTopicMembers(new ArrayList<>(memberMap.values())); return group; } @@ -161,7 +162,7 @@ public Map getGroupOffsetFromKafka(Long clusterPhyId, Stri return offsetMap; } catch (Exception e) { - log.error("method=getGroupOffset||clusterPhyId={}|groupName={}||errMsg=exception!", clusterPhyId, groupName, e); + LOGGER.error("method=getGroupOffset||clusterPhyId={}|groupName={}||errMsg=exception!", clusterPhyId, groupName, e); throw new AdminOperateException(e.getMessage(), e, ResultStatus.KAFKA_OPERATE_FAILED); } @@ -187,7 +188,7 @@ public KSGroupDescription getGroupDescriptionFromKafka(ClusterPhy clusterPhy, St return describeGroupsResult.all().get().get(groupName); } catch(Exception e){ - log.error("method=getGroupDescription||clusterPhyId={}|groupName={}||errMsg=exception!", clusterPhy.getId(), groupName, e); + LOGGER.error("method=getGroupDescription||clusterPhyId={}|groupName={}||errMsg=exception!", clusterPhy.getId(), groupName, e); throw new AdminOperateException(e.getMessage(), e, ResultStatus.KAFKA_OPERATE_FAILED); } finally { @@ -202,12 +203,12 @@ public KSGroupDescription getGroupDescriptionFromKafka(ClusterPhy clusterPhy, St } @Override - public void batchReplaceGroupsAndMembers(Long clusterPhyId, List newGroupList, long updateTime) { + public void batchReplaceGroupsAndMembers(Long clusterPhyId, List newGroupList, Set getFailedGroupSet) { // 更新Group信息 - this.batchReplaceGroups(clusterPhyId, newGroupList, updateTime); + this.batchReplaceGroups(clusterPhyId, newGroupList, getFailedGroupSet); // 更新Group-Topic信息 - this.batchReplaceGroupMembers(clusterPhyId, newGroupList, updateTime); + this.batchReplaceGroupMembers(clusterPhyId, newGroupList, getFailedGroupSet); } @Override @@ -283,21 +284,6 @@ public List listClusterGroups(Long clusterPhyId) { return groupDAO.selectList(lambdaQueryWrapper).stream().map(elem -> GroupConverter.convert2Group(elem)).collect(Collectors.toList()); } - @Override - public int deleteByUpdateTimeBeforeInDB(Long clusterPhyId, Date beforeTime) { - // 删除过期Group信息 - LambdaQueryWrapper groupPOLambdaQueryWrapper = new LambdaQueryWrapper<>(); - groupPOLambdaQueryWrapper.eq(GroupPO::getClusterPhyId, clusterPhyId); - groupPOLambdaQueryWrapper.le(GroupPO::getUpdateTime, beforeTime); - groupDAO.delete(groupPOLambdaQueryWrapper); - - // 删除过期GroupMember信息 - LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(GroupMemberPO::getClusterPhyId, clusterPhyId); - queryWrapper.le(GroupMemberPO::getUpdateTime, beforeTime); - return groupMemberDAO.delete(queryWrapper); - } - @Override public List getGroupsFromDB(Long clusterPhyId) { LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); @@ -368,7 +354,7 @@ public Result resetGroupOffsets(Long clusterPhyId, return Result.buildSuc(); } catch(Exception e){ - log.error("method=resetGroupOffsets||clusterPhyId={}|groupName={}||errMsg=exception!", clusterPhyId, groupName, e); + LOGGER.error("method=resetGroupOffsets||clusterPhyId={}|groupName={}||errMsg=exception!", clusterPhyId, groupName, e); throw new AdminOperateException(e.getMessage(), e, ResultStatus.KAFKA_OPERATE_FAILED); } @@ -378,62 +364,96 @@ public Result resetGroupOffsets(Long clusterPhyId, /**************************************************** private method ****************************************************/ - private void batchReplaceGroupMembers(Long clusterPhyId, List newGroupList, long updateTime) { - if (ValidateUtils.isEmptyList(newGroupList)) { - return; - } - - List dbPOList = this.listClusterGroupsMemberPO(clusterPhyId); - Map dbPOMap = dbPOList.stream().collect(Collectors.toMap(elem -> elem.getGroupName() + elem.getTopicName(), Function.identity())); + private void batchReplaceGroupMembers(Long clusterPhyId, List newGroupList, Set getFailedGroupSet) { + // DB 中的数据 + Map dbPOMap = this.listClusterGroupsMemberPO(clusterPhyId) + .stream() + .collect(Collectors.toMap(elem -> elem.getGroupName() + elem.getTopicName(), Function.identity())); + // 进行数据的更新 for (Group group: newGroupList) { for (GroupTopicMember member : group.getTopicMembers()) { try { - GroupMemberPO newPO = new GroupMemberPO(clusterPhyId, member.getTopicName(), group.getName(), group.getState().getState(), member.getMemberCount(), new Date(updateTime)); + GroupMemberPO newPO = new GroupMemberPO(clusterPhyId, member.getTopicName(), group.getName(), group.getState().getState(), member.getMemberCount(), new Date()); GroupMemberPO dbPO = dbPOMap.remove(newPO.getGroupName() + newPO.getTopicName()); - if (dbPO != null) { + if (dbPO == null) { + // 数据不存在则直接写入 + groupMemberDAO.insert(newPO); + } else if (!dbPO.equal2GroupMemberPO(newPO)) { + // 数据发生了变化则进行更新 newPO.setId(dbPO.getId()); groupMemberDAO.updateById(newPO); - continue; } - - groupMemberDAO.insert(newPO); } catch (Exception e) { - log.error( + LOGGER.error( "method=batchReplaceGroupMembers||clusterPhyId={}||groupName={}||topicName={}||errMsg=exception", clusterPhyId, group.getName(), member.getTopicName(), e ); } } } - } - private void batchReplaceGroups(Long clusterPhyId, List newGroupList, long updateTime) { - if (ValidateUtils.isEmptyList(newGroupList)) { - return; - } + // 删除剩余不存在的 + dbPOMap.values().forEach(elem -> { + try { + if (getFailedGroupSet.contains(elem.getGroupName())) { + // 该group信息获取失败,所以忽略对该数据的删除 + return; + } + + groupDAO.deleteById(elem.getId()); + } catch (Exception e) { + LOGGER.error( + "method=batchReplaceGroupMembers||clusterPhyId={}||groupName={}||topicName={}||msg=delete expired group data in db failed||errMsg=exception", + clusterPhyId, elem.getGroupName(), elem.getTopicName(), e + ); + } + }); + } - List dbGroupList = this.listClusterGroupsPO(clusterPhyId); - Map dbGroupMap = dbGroupList.stream().collect(Collectors.toMap(elem -> elem.getName(), Function.identity())); + private void batchReplaceGroups(Long clusterPhyId, List newGroupList, Set getFailedGroupSet) { + // 获取 DB 中的数据 + Map dbGroupMap = this.listClusterGroupsPO(clusterPhyId) + .stream() + .collect(Collectors.toMap(elem -> elem.getName(), Function.identity())); + // 进行数据的更新 for (Group newGroup: newGroupList) { try { - GroupPO newPO = GroupConverter.convert2GroupPO(newGroup); - newPO.setUpdateTime(new Date(updateTime)); - GroupPO dbPO = dbGroupMap.remove(newGroup.getName()); - if (dbPO != null) { + if (dbPO == null) { + // 一条新的数据,则直接insert + groupDAO.insert(GroupConverter.convert2GroupPO(newGroup)); + continue; + } + + GroupPO newPO = GroupConverter.convert2GroupPO(newGroup); + if (!newPO.equal2GroupPO(dbPO)) { + // 如果不相等,则直接更新 newPO.setId(dbPO.getId()); groupDAO.updateById(newPO); - continue; } - groupDAO.insert(newPO); + // 其他情况,则不需要进行任何操作 } catch (Exception e) { - log.error("method=batchGroupReplace||clusterPhyId={}||groupName={}||errMsg=exception", clusterPhyId, newGroup.getName(), e); + LOGGER.error("method=batchReplaceGroups||clusterPhyId={}||groupName={}||errMsg=exception", clusterPhyId, newGroup.getName(), e); } } + + // 删除剩余不存在的 + dbGroupMap.values().forEach(elem -> { + try { + if (getFailedGroupSet.contains(elem.getName())) { + // 该group信息获取失败,所以忽略对该数据的删除 + return; + } + + groupDAO.deleteById(elem.getId()); + } catch (Exception e) { + LOGGER.error("method=batchReplaceGroups||clusterPhyId={}||groupName={}||msg=delete expired group data in db failed||errMsg=exception", clusterPhyId, elem.getName(), e); + } + }); } private List listClusterGroupsPO(Long clusterPhyId) { diff --git a/km-task/src/main/java/com/xiaojukeji/know/streaming/km/task/kafka/metadata/SyncKafkaGroupTask.java b/km-task/src/main/java/com/xiaojukeji/know/streaming/km/task/kafka/metadata/SyncKafkaGroupTask.java index 521e1f846..9358993e3 100644 --- a/km-task/src/main/java/com/xiaojukeji/know/streaming/km/task/kafka/metadata/SyncKafkaGroupTask.java +++ b/km-task/src/main/java/com/xiaojukeji/know/streaming/km/task/kafka/metadata/SyncKafkaGroupTask.java @@ -36,7 +36,7 @@ public TaskResult processClusterTask(ClusterPhy clusterPhy, long triggerTimeUnit // 获取集群的Group列表 List groupNameList = groupService.listGroupsFromKafka(clusterPhy); - TaskResult allSuccess = TaskResult.SUCCESS; + Set getFailedGroupSet = new HashSet<>(); // 获取Group详细信息 List groupList = new ArrayList<>(); @@ -44,13 +44,16 @@ public TaskResult processClusterTask(ClusterPhy clusterPhy, long triggerTimeUnit try { Group group = groupService.getGroupFromKafka(clusterPhy, groupName); if (group == null) { + // 获取到为空的 group 信息,直接忽略不要 continue; } groupList.add(group); } catch (Exception e) { log.error("method=processClusterTask||clusterPhyId={}||groupName={}||errMsg=exception", clusterPhy.getId(), groupName, e); - allSuccess = TaskResult.FAIL; + + // 记录获取失败的 group 信息 + getFailedGroupSet.add(groupName); } } @@ -58,17 +61,9 @@ public TaskResult processClusterTask(ClusterPhy clusterPhy, long triggerTimeUnit this.filterTopicIfTopicNotExist(clusterPhy.getId(), groupList); // 更新DB中的Group信息 - groupService.batchReplaceGroupsAndMembers(clusterPhy.getId(), groupList, triggerTimeUnitMs); - - // 如果存在错误,则直接返回 - if (!TaskResult.SUCCESS.equals(allSuccess)) { - return allSuccess; - } - - // 删除历史的Group - groupService.deleteByUpdateTimeBeforeInDB(clusterPhy.getId(), new Date(triggerTimeUnitMs - 5 * 60 * 1000)); + groupService.batchReplaceGroupsAndMembers(clusterPhy.getId(), groupList, getFailedGroupSet); - return allSuccess; + return getFailedGroupSet.isEmpty()? TaskResult.SUCCESS: TaskResult.FAIL; } private void filterTopicIfTopicNotExist(Long clusterPhyId, List groupList) { From e975932d4173c038611717a1efc6f8d6cfe03f00 Mon Sep 17 00:00:00 2001 From: ZQKC Date: Fri, 12 May 2023 13:52:26 +0800 Subject: [PATCH 11/37] =?UTF-8?q?[Bugfix]=E4=BF=AE=E5=A4=8DPrometheus?= =?UTF-8?q?=E5=BC=80=E6=94=BE=E6=8E=A5=E5=8F=A3=E4=B8=AD=EF=BC=8CPartition?= =?UTF-8?q?=E6=8C=87=E6=A0=87tag=E7=BC=BA=E5=A4=B1=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98(#1013)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../km/monitor/component/AbstractMonitorSinkService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/km-extends/km-monitor/src/main/java/com/xiaojukeji/know/streaming/km/monitor/component/AbstractMonitorSinkService.java b/km-extends/km-monitor/src/main/java/com/xiaojukeji/know/streaming/km/monitor/component/AbstractMonitorSinkService.java index f35c5ec6e..bbb316475 100644 --- a/km-extends/km-monitor/src/main/java/com/xiaojukeji/know/streaming/km/monitor/component/AbstractMonitorSinkService.java +++ b/km-extends/km-monitor/src/main/java/com/xiaojukeji/know/streaming/km/monitor/component/AbstractMonitorSinkService.java @@ -123,6 +123,7 @@ private List partitionMetric2SinkPoint(List p tagsMap.put(CLUSTER_ID.getName(), p.getClusterPhyId()); tagsMap.put(BROKER_ID.getName(), p.getBrokerId()); tagsMap.put(PARTITION_ID.getName(), p.getPartitionId()); + tagsMap.put(TOPIC.getName(), p.getTopic()); pointList.addAll(genSinkPoint("Partition", p.getMetrics(), p.getTimestamp(), tagsMap)); } From 2256e8bbdb122ff50634a97a3e5387f9a574bd3d Mon Sep 17 00:00:00 2001 From: ZQKC Date: Fri, 12 May 2023 14:25:20 +0800 Subject: [PATCH 12/37] =?UTF-8?q?[Bugfix]=E4=BF=AE=E5=A4=8DConnect-GroupDe?= =?UTF-8?q?scription=E8=A7=A3=E6=9E=90=E5=A4=B1=E8=B4=A5=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98(#1010)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1、先尝试使用IncrementalCooperativeConnectProtocol协议进行解析; 2、IncrementalCooperativeConnectProtocol协议解析失败后,再维持原先的情况,使用ConnectProtocol协议进行解析; --- .../kafka/KSPartialKafkaAdminClient.java | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/utils/kafka/KSPartialKafkaAdminClient.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/utils/kafka/KSPartialKafkaAdminClient.java index db8093058..8f53d9988 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/utils/kafka/KSPartialKafkaAdminClient.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/utils/kafka/KSPartialKafkaAdminClient.java @@ -78,6 +78,8 @@ import org.apache.kafka.common.utils.Time; import org.apache.kafka.common.utils.Utils; import org.apache.kafka.connect.runtime.distributed.ConnectProtocol; +import org.apache.kafka.connect.runtime.distributed.ExtendedWorkerState; +import org.apache.kafka.connect.runtime.distributed.IncrementalCooperativeConnectProtocol; import org.slf4j.Logger; import java.net.InetSocketAddress; @@ -1342,19 +1344,7 @@ void handleResponse(AbstractResponse abstractResponse) { memberBaseAssignment = new KSMemberConsumerAssignment(new HashSet<>()); } } else { - ConnectProtocol.Assignment assignment = null; - if (groupMember.memberAssignment().length > 0) { - assignment = ConnectProtocol. - deserializeAssignment(ByteBuffer.wrap(groupMember.memberAssignment())); - } - - ConnectProtocol.WorkerState workerState = null; - if (groupMember.memberMetadata().length > 0) { - workerState = ConnectProtocol. - deserializeMetadata(ByteBuffer.wrap(groupMember.memberMetadata())); - } - - memberBaseAssignment = new KSMemberConnectAssignment(assignment, workerState); + memberBaseAssignment = deserializeConnectGroupDataCompatibility(groupMember); } memberDescriptions.add(new KSMemberDescription( @@ -1383,6 +1373,36 @@ void handleFailure(Throwable throwable) { }; } + private KSMemberBaseAssignment deserializeConnectGroupDataCompatibility(DescribedGroupMember groupMember) { + try { + // 高版本的反序列化方式 + ExtendedWorkerState workerState = null; + if (groupMember.memberMetadata().length > 0) { + workerState = IncrementalCooperativeConnectProtocol. + deserializeMetadata(ByteBuffer.wrap(groupMember.memberMetadata())); + + return new KSMemberConnectAssignment(workerState.assignment(), workerState); + } + } catch (Exception e) { + // ignore + } + + // 低版本的反序列化方式 + ConnectProtocol.Assignment assignment = null; + if (groupMember.memberAssignment().length > 0) { + assignment = ConnectProtocol. + deserializeAssignment(ByteBuffer.wrap(groupMember.memberAssignment())); + } + + ConnectProtocol.WorkerState workerState = null; + if (groupMember.memberMetadata().length > 0) { + workerState = ConnectProtocol. + deserializeMetadata(ByteBuffer.wrap(groupMember.memberMetadata())); + } + + return new KSMemberConnectAssignment(assignment, workerState); + } + private Set validAclOperations(final int authorizedOperations) { if (authorizedOperations == MetadataResponse.AUTHORIZED_OPERATIONS_OMITTED) { From 7bfe787e395e8b60a1853abf118e19334b405975 Mon Sep 17 00:00:00 2001 From: ZQKC Date: Fri, 19 May 2023 11:46:33 +0800 Subject: [PATCH 13/37] =?UTF-8?q?[Bugfix]=E4=BF=AE=E5=A4=8Dzk=20standalone?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E4=B8=8D=E5=85=BC=E5=AE=B9=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../km/biz/cluster/impl/ClusterZookeepersManagerImpl.java | 3 ++- .../know/streaming/km/common/enums/zookeeper/ZKRoleEnum.java | 2 ++ .../health/checker/zookeeper/HealthCheckZookeeperService.java | 4 ++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/cluster/impl/ClusterZookeepersManagerImpl.java b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/cluster/impl/ClusterZookeepersManagerImpl.java index aca302693..8aed18ddf 100644 --- a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/cluster/impl/ClusterZookeepersManagerImpl.java +++ b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/cluster/impl/ClusterZookeepersManagerImpl.java @@ -62,7 +62,8 @@ public Result getClusterPhyZookeepersState(Long cluste vo.setTotalObserverCount(0); vo.setAliveServerCount(0); for (ZookeeperInfo info: infoList) { - if (info.getRole().equals(ZKRoleEnum.LEADER.getRole())) { + if (info.getRole().equals(ZKRoleEnum.LEADER.getRole()) || info.getRole().equals(ZKRoleEnum.STANDALONE.getRole())) { + // leader 或者 standalone vo.setLeaderNode(info.getHost()); } diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/enums/zookeeper/ZKRoleEnum.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/enums/zookeeper/ZKRoleEnum.java index fd379dc84..420ccc36b 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/enums/zookeeper/ZKRoleEnum.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/enums/zookeeper/ZKRoleEnum.java @@ -10,6 +10,8 @@ public enum ZKRoleEnum { OBSERVER("observer"), + STANDALONE("standalone"), + UNKNOWN("unknown"), ; diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/health/checker/zookeeper/HealthCheckZookeeperService.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/health/checker/zookeeper/HealthCheckZookeeperService.java index f18f3172c..933310dcb 100644 --- a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/health/checker/zookeeper/HealthCheckZookeeperService.java +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/health/checker/zookeeper/HealthCheckZookeeperService.java @@ -102,6 +102,10 @@ private HealthCheckResult checkBrainSplit(Tuple ZKRoleEnum.LEADER.getRole().equals(elem.getRole())).count(); + if (value == 0) { + // ZK 在单机模式下,leader角色就是standalone + value = infoList.stream().filter(elem -> ZKRoleEnum.STANDALONE.getRole().equals(elem.getRole())).count(); + } checkResult.setPassed(value == 1 ? Constant.YES : Constant.NO); return checkResult; From ffc115cb763f6f04b9d66ea34a44963b59a4ffbe Mon Sep 17 00:00:00 2001 From: Richard <18310231437@163.com> Date: Tue, 30 May 2023 17:03:34 +0800 Subject: [PATCH 14/37] =?UTF-8?q?[Bugfix]=E4=BF=AE=E5=A4=8Des=E7=B4=A2?= =?UTF-8?q?=E5=BC=95create/delete=E6=AD=BB=E5=BE=AA=E7=8E=AF=E9=97=AE?= =?UTF-8?q?=E9=A2=98=20(#1021)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../km/persistence/es/dao/BaseMetricESDAO.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/es/dao/BaseMetricESDAO.java b/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/es/dao/BaseMetricESDAO.java index 48651dd75..77d7bbdf4 100644 --- a/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/es/dao/BaseMetricESDAO.java +++ b/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/es/dao/BaseMetricESDAO.java @@ -24,6 +24,7 @@ import org.springframework.util.CollectionUtils; import java.util.*; +import java.util.stream.IntStream; import static com.xiaojukeji.know.streaming.km.common.constant.ESConstant.*; @@ -68,13 +69,11 @@ public void checkCurrentDayIndexExist(){ String indexTemplate = templateLoaderUtil.getContextByFileName(indexName); esOpClient.createIndexTemplateIfNotExist(indexName, indexTemplate); - //检查最近7天索引存在不存 - for(int i = 0; i < INDEX_DAYS; i++){ - String realIndex = IndexNameUtils.genDailyIndexName(indexName, i); - if(esOpClient.indexExist(realIndex)){continue;} - - esOpClient.createIndex(realIndex); - } + int retainDays = indexExpireDays > INDEX_DAYS ? INDEX_DAYS : indexExpireDays; + // 检查最近【retainDays】天索引存在不存 + IntStream.range(0, retainDays).mapToObj(i -> IndexNameUtils.genDailyIndexName(indexName, i)) + .filter(realIndex -> !esOpClient.indexExist(realIndex)) + .forEach(realIndex -> esOpClient.createIndex(realIndex)); } catch (Exception e) { LOGGER.error("method=checkCurrentDayIndexExist||errMsg=exception!", e); } @@ -94,8 +93,7 @@ public void delExpireIndex(){ indexExpireDays, indexList.subList(indexExpireDays, size)); } - indexList.subList(indexExpireDays, size).stream().forEach( - s -> esOpClient.delIndexByName(s)); + indexList.subList(indexExpireDays, size).forEach(s -> esOpClient.delIndexByName(s)); } } From b3fd494398c3fe929a20950e892a25b67818ba7f Mon Sep 17 00:00:00 2001 From: Fangzhibin <2535030577@qq.com> Date: Tue, 23 May 2023 21:16:10 +0800 Subject: [PATCH 15/37] =?UTF-8?q?[Optimize]=E8=A7=A3=E5=86=B3Connect?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E6=B2=A1=E6=9C=89=E9=BB=98=E8=AE=A4=E5=8B=BE?= =?UTF-8?q?=E9=80=89=E6=8C=87=E6=A0=87=EF=BC=88#926=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/VersionControlManagerImpl.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/version/impl/VersionControlManagerImpl.java b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/version/impl/VersionControlManagerImpl.java index 740974d70..a8b7da8f4 100644 --- a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/version/impl/VersionControlManagerImpl.java +++ b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/version/impl/VersionControlManagerImpl.java @@ -35,6 +35,8 @@ import static com.xiaojukeji.know.streaming.km.core.service.version.metrics.kafka.GroupMetricVersionItems.*; import static com.xiaojukeji.know.streaming.km.core.service.version.metrics.kafka.TopicMetricVersionItems.*; import static com.xiaojukeji.know.streaming.km.core.service.version.metrics.connect.MirrorMakerMetricVersionItems.*; +import static com.xiaojukeji.know.streaming.km.core.service.version.metrics.connect.ConnectClusterMetricVersionItems.*; +import static com.xiaojukeji.know.streaming.km.core.service.version.metrics.connect.ConnectorMetricVersionItems.*; import static com.xiaojukeji.know.streaming.km.core.service.version.metrics.kafka.ZookeeperMetricVersionItems.*; @Service @@ -123,6 +125,42 @@ public void init(){ defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_MIRROR_MAKER.getCode(), MIRROR_MAKER_METRIC_RECORD_COUNT, true)); defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_MIRROR_MAKER.getCode(), MIRROR_MAKER_METRIC_RECORD_RATE, true)); defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_MIRROR_MAKER.getCode(), MIRROR_MAKER_METRIC_REPLICATION_LATENCY_MS_MAX, true)); + + // Connect Cluster + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CLUSTER.getCode(), CONNECT_CLUSTER_METRIC_CONNECTOR_COUNT, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CLUSTER.getCode(), CONNECT_CLUSTER_METRIC_TASK_COUNT, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CLUSTER.getCode(), CONNECT_CLUSTER_METRIC_CONNECTOR_STARTUP_ATTEMPTS_TOTAL, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CLUSTER.getCode(), CONNECT_CLUSTER_METRIC_CONNECTOR_STARTUP_FAILURE_PERCENTAGE, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CLUSTER.getCode(), CONNECT_CLUSTER_METRIC_CONNECTOR_STARTUP_FAILURE_TOTAL, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CLUSTER.getCode(), CONNECT_CLUSTER_METRIC_TASK_STARTUP_ATTEMPTS_TOTAL, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CLUSTER.getCode(), CONNECT_CLUSTER_METRIC_TASK_STARTUP_FAILURE_PERCENTAGE, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CLUSTER.getCode(), CONNECT_CLUSTER_METRIC_TASK_STARTUP_FAILURE_TOTAL, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CLUSTER.getCode(), CONNECT_CLUSTER_METRIC_COLLECT_COST_TIME, true)); + + + // Connect Connector + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CONNECTOR.getCode(), CONNECTOR_METRIC_HEALTH_STATE, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CONNECTOR.getCode(), CONNECTOR_METRIC_HEALTH_CHECK_PASSED, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CONNECTOR.getCode(), CONNECTOR_METRIC_HEALTH_CHECK_TOTAL, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CONNECTOR.getCode(), CONNECTOR_METRIC_COLLECT_COST_TIME, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CONNECTOR.getCode(), CONNECTOR_METRIC_CONNECTOR_TOTAL_TASK_COUNT, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CONNECTOR.getCode(), CONNECTOR_METRIC_CONNECTOR_RUNNING_TASK_COUNT, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CONNECTOR.getCode(), CONNECTOR_METRIC_CONNECTOR_FAILED_TASK_COUNT, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CONNECTOR.getCode(), CONNECTOR_METRIC_SOURCE_RECORD_ACTIVE_COUNT, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CONNECTOR.getCode(), CONNECTOR_METRIC_SOURCE_RECORD_POLL_TOTAL, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CONNECTOR.getCode(), CONNECTOR_METRIC_SOURCE_RECORD_WRITE_TOTAL, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CONNECTOR.getCode(), CONNECTOR_METRIC_SINK_RECORD_ACTIVE_COUNT, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CONNECTOR.getCode(), CONNECTOR_METRIC_SINK_RECORD_READ_TOTAL, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CONNECTOR.getCode(), CONNECTOR_METRIC_SINK_RECORD_SEND_TOTAL, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CONNECTOR.getCode(), CONNECTOR_METRIC_DEADLETTERQUEUE_PRODUCE_FAILURES, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CONNECTOR.getCode(), CONNECTOR_METRIC_DEADLETTERQUEUE_PRODUCE_REQUESTS, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CONNECTOR.getCode(), CONNECTOR_METRIC_TOTAL_ERRORS_LOGGED, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CONNECTOR.getCode(), CONNECTOR_METRIC_SOURCE_RECORD_POLL_RATE, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CONNECTOR.getCode(), CONNECTOR_METRIC_SOURCE_RECORD_WRITE_RATE, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CONNECTOR.getCode(), CONNECTOR_METRIC_SINK_RECORD_READ_RATE, true)); + defaultMetrics.add(new UserMetricConfig(METRIC_CONNECT_CONNECTOR.getCode(), CONNECTOR_METRIC_SINK_RECORD_SEND_RATE, true)); + + } @Autowired From e34e3f3e3d73d49e27ff4bad2541511576c007e9 Mon Sep 17 00:00:00 2001 From: ZQKC Date: Thu, 1 Jun 2023 14:29:07 +0800 Subject: [PATCH 16/37] =?UTF-8?q?[Feature]=E6=94=AF=E6=8C=81=E6=8C=87?= =?UTF-8?q?=E5=AE=9AServer=E7=9A=84=E5=85=B7=E4=BD=93Jmx=E7=AB=AF=E5=8F=A3?= =?UTF-8?q?(#965)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 变更事项: 1、接入集群时,支持按照Broker粒度进行Jmx端口的配置; 2、设置Jmx端口的优先级为:指定Broker端口 》ZK中获取到的Broker端口 》指定Cluster端口; 补充说明: 1、该修改仅为后端修改,产品上暂未进行修改; --- .../impl/ClusterBrokersManagerImpl.java | 2 +- .../km/common/bean/entity/broker/Broker.java | 6 +- .../bean/entity/config/JmxAuthConfig.java | 29 ++++++++ .../common/bean/entity/config/JmxConfig.java | 73 +++++++++++++++---- .../bean/entity/jmx/ServerIdJmxPort.java | 25 +++++++ .../km/common/enums/jmx/JmxEnum.java | 20 +++++ .../km/common/jmx/JmxConnectorWrap.java | 44 ++++++----- .../broker/impl/BrokerServiceImpl.java | 2 +- .../persistence/connect/ConnectJMXClient.java | 2 +- .../km/persistence/kafka/KafkaJMXClient.java | 4 +- 10 files changed, 170 insertions(+), 37 deletions(-) create mode 100644 km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/config/JmxAuthConfig.java create mode 100644 km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/jmx/ServerIdJmxPort.java create mode 100644 km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/enums/jmx/JmxEnum.java diff --git a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/cluster/impl/ClusterBrokersManagerImpl.java b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/cluster/impl/ClusterBrokersManagerImpl.java index ab5d6a6d6..c77724dd8 100644 --- a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/cluster/impl/ClusterBrokersManagerImpl.java +++ b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/cluster/impl/ClusterBrokersManagerImpl.java @@ -202,7 +202,7 @@ private List convert2ClusterBrokersOverviewVOList(Clus //补充非zk模式的JMXPort信息 if (!clusterPhy.getRunState().equals(ClusterRunStateEnum.RUN_ZK.getRunState())) { JmxConfig jmxConfig = ConvertUtil.str2ObjByJson(clusterPhy.getJmxProperties(), JmxConfig.class); - voList.forEach(elem -> elem.setJmxPort(jmxConfig.getJmxPort() == null ? -1 : jmxConfig.getJmxPort())); + voList.forEach(elem -> elem.setJmxPort(jmxConfig.getFinallyJmxPort(String.valueOf(elem.getBrokerId())))); } return voList; diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/broker/Broker.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/broker/Broker.java index 752aade03..35fa1f5a9 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/broker/Broker.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/broker/Broker.java @@ -4,6 +4,8 @@ import com.alibaba.fastjson.TypeReference; import com.xiaojukeji.know.streaming.km.common.bean.entity.common.IpPortData; import com.xiaojukeji.know.streaming.km.common.bean.po.broker.BrokerPO; +import com.xiaojukeji.know.streaming.km.common.constant.Constant; +import com.xiaojukeji.know.streaming.km.common.enums.jmx.JmxEnum; import com.xiaojukeji.know.streaming.km.common.utils.ConvertUtil; import lombok.AllArgsConstructor; import lombok.Data; @@ -71,10 +73,10 @@ public static Broker buildFrom(Long clusterPhyId, Node node, Long startTimestamp metadata.setBrokerId(node.id()); metadata.setHost(node.host()); metadata.setPort(node.port()); - metadata.setJmxPort(-1); + metadata.setJmxPort(JmxEnum.UNKNOWN.getPort()); metadata.setStartTimestamp(startTimestamp); metadata.setRack(node.rack()); - metadata.setStatus(1); + metadata.setStatus(Constant.ALIVE); return metadata; } diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/config/JmxAuthConfig.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/config/JmxAuthConfig.java new file mode 100644 index 000000000..02fec6b4e --- /dev/null +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/config/JmxAuthConfig.java @@ -0,0 +1,29 @@ +package com.xiaojukeji.know.streaming.km.common.bean.entity.config; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; + +/** + * @author zengqiao + * @date 23/05/19 + */ +@Data +@ApiModel(description = "Jmx配置") +public class JmxAuthConfig implements Serializable { + @ApiModelProperty(value="最大连接", example = "100") + protected Integer maxConn; + + @ApiModelProperty(value="是否开启SSL,如果开始则username 与 token 必须非空", example = "false") + protected Boolean openSSL; + + @ApiModelProperty(value="SSL情况下的username", example = "Ks-Km") + protected String username; + + @ApiModelProperty(value="SSL情况下的token", example = "KsKmCCY19") + protected String token; +} + + diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/config/JmxConfig.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/config/JmxConfig.java index 87607c1f9..7620e960e 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/config/JmxConfig.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/config/JmxConfig.java @@ -1,10 +1,12 @@ package com.xiaojukeji.know.streaming.km.common.bean.entity.config; +import com.xiaojukeji.know.streaming.km.common.bean.entity.jmx.ServerIdJmxPort; +import com.xiaojukeji.know.streaming.km.common.enums.jmx.JmxEnum; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import java.io.Serializable; +import java.util.List; /** * @author zengqiao @@ -12,24 +14,69 @@ */ @Data @ApiModel(description = "Jmx配置") -public class JmxConfig implements Serializable { - @ApiModelProperty(value="jmx端口", example = "8099") +public class JmxConfig extends JmxAuthConfig { + @ApiModelProperty(value="jmx端口,最低优先使用的端口", example = "8099") private Integer jmxPort; - @ApiModelProperty(value="最大连接", example = "100") - private Integer maxConn; + @ApiModelProperty(value="使用哪个endpoint网络", example = "EXTERNAL") + private String useWhichEndpoint; - @ApiModelProperty(value="是否开启SSL,如果开始则username 与 token 必须非空", example = "false") - private Boolean openSSL; + @ApiModelProperty(value="指定server的JMX端口, 最高优先使用的端口", example = "") + private List specifiedJmxPortList; - @ApiModelProperty(value="SSL情况下的username", example = "Ks-Km") - private String username; + /** + * 选取最终的jmx端口 + * @param serverId 服务ID + * @param metadataJmxPort ks从元信息中获取到的jmx端口 + */ + public Integer getFinallyJmxPort(String serverId, Integer metadataJmxPort) { + if (specifiedJmxPortList == null || specifiedJmxPortList.isEmpty()) { + // 未进行特殊指定时,zkJMX端口存在则优先使用zkJmxPort,否则使用配置的jmxPort + return this.selectJmxPort(jmxPort, metadataJmxPort); + } - @ApiModelProperty(value="SSL情况下的token", example = "KsKmCCY19") - private String token; + // 进行特殊配置时 + for (ServerIdJmxPort serverIdJmxPort: specifiedJmxPortList) { + if (serverId.equals(serverIdJmxPort.getServerId()) && serverIdJmxPort.getJmxPort() != null) { + // 当前server有指定具体的jmx端口时,则使用具体的端口 + return serverIdJmxPort.getJmxPort(); + } + } - @ApiModelProperty(value="使用哪个endpoint网络", example = "EXTERNAL") - private String useWhichEndpoint; + return this.selectJmxPort(jmxPort, metadataJmxPort); + } + + /** + * 选取最终的jmx端口 + * @param serverId serverId + */ + public Integer getFinallyJmxPort(String serverId) { + return this.getFinallyJmxPort(serverId, null); + } + + /** + * 选取jmx端口 + * @param feJmxPort 前端页面配置的jmx端口 + * @param metadataJmxPort ks从元信息中获取到的jmx端口 + */ + private Integer selectJmxPort(Integer feJmxPort, Integer metadataJmxPort) { + if (metadataJmxPort == null) { + return feJmxPort != null? feJmxPort: JmxEnum.NOT_OPEN.getPort(); + } + + if (JmxEnum.NOT_OPEN.getPort().equals(metadataJmxPort)) { + // 如果元信息提示未开启,则直接返回未开启 + return JmxEnum.NOT_OPEN.getPort(); + } + + if (JmxEnum.UNKNOWN.getPort().equals(metadataJmxPort)) { + // 如果元信息提示未知,则直接返回feJmxPort 或者 未开启 + return feJmxPort != null? feJmxPort: JmxEnum.NOT_OPEN.getPort(); + } + + // 其他情况,返回 metadataJmxPort + return metadataJmxPort; + } } diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/jmx/ServerIdJmxPort.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/jmx/ServerIdJmxPort.java new file mode 100644 index 000000000..df27cb87d --- /dev/null +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/jmx/ServerIdJmxPort.java @@ -0,0 +1,25 @@ +package com.xiaojukeji.know.streaming.km.common.bean.entity.jmx; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * @author didi + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ServerIdJmxPort implements Serializable { + /** + * serverID + */ + private String serverId; + + /** + * JMX端口 + */ + private Integer jmxPort; +} diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/enums/jmx/JmxEnum.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/enums/jmx/JmxEnum.java new file mode 100644 index 000000000..314402e84 --- /dev/null +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/enums/jmx/JmxEnum.java @@ -0,0 +1,20 @@ +package com.xiaojukeji.know.streaming.km.common.enums.jmx; + +import lombok.Getter; + +@Getter +public enum JmxEnum { + NOT_OPEN(-1, "未开启JMX端口"), + + UNKNOWN(-2, "JMX端口未知"), + + ; + + private final Integer port; + private final String message; + + JmxEnum(Integer port, String message) { + this.port = port; + this.message = message; + } +} diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/jmx/JmxConnectorWrap.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/jmx/JmxConnectorWrap.java index d9cfb0821..071072ade 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/jmx/JmxConnectorWrap.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/jmx/JmxConnectorWrap.java @@ -1,6 +1,8 @@ package com.xiaojukeji.know.streaming.km.common.jmx; +import com.xiaojukeji.know.streaming.km.common.bean.entity.config.JmxAuthConfig; import com.xiaojukeji.know.streaming.km.common.bean.entity.config.JmxConfig; +import com.xiaojukeji.know.streaming.km.common.enums.jmx.JmxEnum; import com.xiaojukeji.know.streaming.km.common.utils.BackoffUtils; import com.xiaojukeji.know.streaming.km.common.utils.ValidateUtils; import org.slf4j.Logger; @@ -33,26 +35,21 @@ public class JmxConnectorWrap { private final Long brokerStartupTime; - private final String host; + private final String jmxHost; - private final Integer port; + private final Integer jmxPort; private JMXConnector jmxConnector; private final AtomicInteger atomicInteger; - private JmxConfig jmxConfig; + private JmxAuthConfig jmxConfig; - public JmxConnectorWrap(String clientLogIdent, Long brokerStartupTime, String host, Integer port, JmxConfig jmxConfig) { + public JmxConnectorWrap(String clientLogIdent, Long brokerStartupTime, String jmxHost, Integer jmxPort, JmxAuthConfig jmxConfig) { this.clientLogIdent=clientLogIdent; this.brokerStartupTime = brokerStartupTime; - this.host = host; - - if (port == null || port == -1 && jmxConfig.getJmxPort() != null) { - this.port = jmxConfig.getJmxPort(); - } else { - this.port = port; - } + this.jmxHost = jmxHost; + this.jmxPort = (jmxPort == null? JmxEnum.UNKNOWN.getPort() : jmxPort); this.jmxConfig = jmxConfig; if (ValidateUtils.isNull(this.jmxConfig)) { @@ -61,6 +58,7 @@ public JmxConnectorWrap(String clientLogIdent, Long brokerStartupTime, String ho if (ValidateUtils.isNullOrLessThanZero(this.jmxConfig.getMaxConn())) { this.jmxConfig.setMaxConn(1000); } + this.atomicInteger = new AtomicInteger(this.jmxConfig.getMaxConn()); } @@ -68,7 +66,7 @@ public boolean checkJmxConnectionAndInitIfNeed() { if (jmxConnector != null) { return true; } - if (port == null || port == -1) { + if (jmxPort == null || jmxPort == -1) { return false; } return createJmxConnector(); @@ -91,7 +89,10 @@ public synchronized void close() { jmxConnector = null; } catch (IOException e) { - LOGGER.warn("close JmxConnector exception, clientLogIdent:{} host:{} port:{}.", clientLogIdent, host, port, e); + LOGGER.error( + "method=close||clientLogIdent={}||jmxHost={}||jmxPort={}||msg=close jmx JmxConnector exception.", + clientLogIdent, jmxHost, jmxPort, e + ); } } @@ -159,7 +160,7 @@ private synchronized boolean createJmxConnector() { if (jmxConnector != null) { return true; } - String jmxUrl = String.format("service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi", host, port); + String jmxUrl = String.format("service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi", jmxHost, jmxPort); try { Map environment = new HashMap(); if (!ValidateUtils.isBlank(this.jmxConfig.getUsername()) && !ValidateUtils.isBlank(this.jmxConfig.getToken())) { @@ -174,12 +175,21 @@ private synchronized boolean createJmxConnector() { } jmxConnector = JMXConnectorFactory.connect(new JMXServiceURL(jmxUrl), environment); - LOGGER.info("JMX connect success, clientLogIdent:{} host:{} port:{}.", clientLogIdent, host, port); + LOGGER.info( + "method=createJmxConnector||clientLogIdent={}||jmxHost={}||jmxPort={}||msg=jmx connect success.", + clientLogIdent, jmxHost, jmxPort + ); return true; } catch (MalformedURLException e) { - LOGGER.error("JMX url exception, clientLogIdent:{} host:{} port:{} jmxUrl:{}", clientLogIdent, host, port, jmxUrl, e); + LOGGER.error( + "method=createJmxConnector||clientLogIdent={}||jmxHost={}||jmxPort={}||jmxUrl={}||msg=jmx url exception.", + clientLogIdent, jmxHost, jmxPort, jmxUrl, e + ); } catch (Exception e) { - LOGGER.error("JMX connect exception, clientLogIdent:{} host:{} port:{}.", clientLogIdent, host, port, e); + LOGGER.error( + "method=createJmxConnector||clientLogIdent={}||jmxHost={}||jmxPort={}||msg=jmx connect exception.", + clientLogIdent, jmxHost, jmxPort, e + ); } return false; } diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/broker/impl/BrokerServiceImpl.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/broker/impl/BrokerServiceImpl.java index 97dc00c83..e34c035c5 100644 --- a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/broker/impl/BrokerServiceImpl.java +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/broker/impl/BrokerServiceImpl.java @@ -360,7 +360,7 @@ private Result> getBrokersFromAdminClient(ClusterPhy clusterPhy) { private Broker getStartTimeAndBuildBroker(Long clusterPhyId, Node newNode, JmxConfig jmxConfig) { try { - Long startTime = jmxDAO.getServerStartTime(clusterPhyId, newNode.host(), jmxConfig.getJmxPort(), jmxConfig); + Long startTime = jmxDAO.getServerStartTime(clusterPhyId, newNode.host(), jmxConfig.getFinallyJmxPort(String.valueOf(newNode.id())), jmxConfig); return Broker.buildFrom(clusterPhyId, newNode, startTime); } catch (Exception e) { diff --git a/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/connect/ConnectJMXClient.java b/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/connect/ConnectJMXClient.java index 727ad7f6e..300243b7c 100644 --- a/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/connect/ConnectJMXClient.java +++ b/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/connect/ConnectJMXClient.java @@ -90,7 +90,7 @@ private JmxConnectorWrap createJmxConnectorWrap(ConnectCluster connectCluster, S "connectClusterId: " + connectCluster.getId() + " workerId: " + workerId, null, connectWorker.getHost(), - connectWorker.getJmxPort() != null ? connectWorker.getJmxPort() : jmxConfig.getJmxPort(), + jmxConfig.getFinallyJmxPort(workerId, connectWorker.getJmxPort()), jmxConfig ); diff --git a/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/kafka/KafkaJMXClient.java b/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/kafka/KafkaJMXClient.java index 1ace6742e..e759da609 100644 --- a/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/kafka/KafkaJMXClient.java +++ b/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/kafka/KafkaJMXClient.java @@ -161,8 +161,8 @@ private JmxConnectorWrap createJmxConnectorWrap(ClusterPhy clusterPhy, Integer b JmxConnectorWrap jmxConnectorWrap = new JmxConnectorWrap( "clusterPhyId: " + clusterPhy.getId() + " brokerId: " + brokerId, broker.getStartTimestamp(), - jmxConfig != null ? broker.getJmxHost(jmxConfig.getUseWhichEndpoint()) : broker.getHost(), - broker.getJmxPort() != null ? broker.getJmxPort() : jmxConfig.getJmxPort(), + broker.getJmxHost(jmxConfig.getUseWhichEndpoint()), + jmxConfig.getFinallyJmxPort(String.valueOf(brokerId), broker.getJmxPort()), jmxConfig ); From c497e4cb2d56c5a67720e0cb9c9e9a00ab4ff23a Mon Sep 17 00:00:00 2001 From: ZQKC Date: Thu, 15 Jun 2023 17:37:46 +0800 Subject: [PATCH 17/37] =?UTF-8?q?[Doc]=E8=A1=A5=E5=85=85=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E7=89=B9=E5=AE=9AJmx=E7=AB=AF=E5=8F=A3=E7=9A=84=E8=AF=B4?= =?UTF-8?q?=E6=98=8E(#965)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1、补充连接特定Jmx端口的说明; 2、统一JMX问题排查文档; 3、删除无效图片; --- .../connect_jmx_failed/check_jmx_opened.jpg | Bin 390896 -> 0 bytes ...346\216\245JMX\345\244\261\350\264\245.md" | 272 ++++++++++++++---- ...22\346\237\245\346\211\213\345\206\214.md" | 195 ++----------- 3 files changed, 229 insertions(+), 238 deletions(-) delete mode 100644 docs/dev_guide/assets/connect_jmx_failed/check_jmx_opened.jpg diff --git a/docs/dev_guide/assets/connect_jmx_failed/check_jmx_opened.jpg b/docs/dev_guide/assets/connect_jmx_failed/check_jmx_opened.jpg deleted file mode 100644 index 1890983c9048f149fd901328e7131c8030043e2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 390896 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
%s9)yc_-S5<_xxK)+&#m0@v<*KSvR~Q$Yiz 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
YwKy0P^syl0$x+` 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 diff --git "a/docs/dev_guide/\350\247\243\345\206\263\350\277\236\346\216\245JMX\345\244\261\350\264\245.md" "b/docs/dev_guide/\350\247\243\345\206\263\350\277\236\346\216\245JMX\345\244\261\350\264\245.md" index 03271837b..c6f73fb92 100644 --- "a/docs/dev_guide/\350\247\243\345\206\263\350\277\236\346\216\245JMX\345\244\261\350\264\245.md" +++ "b/docs/dev_guide/\350\247\243\345\206\263\350\277\236\346\216\245JMX\345\244\261\350\264\245.md" @@ -2,125 +2,275 @@ ![Logo](https://user-images.githubusercontent.com/71620349/185368586-aed82d30-1534-453d-86ff-ecfa9d0f35bd.png) -## JMX-连接失败问题解决 -集群正常接入`KnowStreaming`之后,即可以看到集群的Broker列表,此时如果查看不了Topic的实时流量,或者是Broker的实时流量信息时,那么大概率就是`JMX`连接的问题了。 +## 2、解决连接 JMX 失败 -下面我们按照步骤来一步一步的检查。 +- [2、解决连接 JMX 失败](#2解决连接-jmx-失败) + - [2.1、正异常现象](#21正异常现象) + - [2.2、异因一:JMX未开启](#22异因一jmx未开启) + - [2.2.1、异常现象](#221异常现象) + - [2.2.2、解决方案](#222解决方案) + - [2.3、异原二:JMX配置错误](#23异原二jmx配置错误) + - [2.3.1、异常现象](#231异常现象) + - [2.3.2、解决方案](#232解决方案) + - [2.4、异因三:JMX开启SSL](#24异因三jmx开启ssl) + - [2.4.1、异常现象](#241异常现象) + - [2.4.2、解决方案](#242解决方案) + - [2.5、异因四:连接了错误IP](#25异因四连接了错误ip) + - [2.5.1、异常现象](#251异常现象) + - [2.5.2、解决方案](#252解决方案) + - [2.6、异因五:连接了错误端口](#26异因五连接了错误端口) + - [2.6.1、异常现象](#261异常现象) + - [2.6.2、解决方案](#262解决方案) -### 1、问题说明 -**类型一:JMX配置未开启** +背景:Kafka 通过 JMX 服务进行运行指标的暴露,因此 `KnowStreaming` 会主动连接 Kafka 的 JMX 服务进行指标采集。如果我们发现页面缺少指标,那么可能原因之一是 Kafka 的 JMX 端口配置的有问题导致指标获取失败,进而页面没有数据。 -未开启时,直接到`2、解决方法`查看如何开启即可。 -![check_jmx_opened](http://img-ys011.didistatic.com/static/dc2img/do1_dRX6UHE2IUSHqsN95DGb) +### 2.1、正异常现象 +**1、异常现象** -**类型二:配置错误** +Broker 列表的 JMX PORT 列出现红色感叹号,则表示 JMX 连接存在异常。 -`JMX`端口已经开启的情况下,有的时候开启的配置不正确,此时也会导致出现连接失败的问题。这里大概列举几种原因: + -- `JMX`配置错误:见`2、解决方法`。 -- 存在防火墙或者网络限制:网络通的另外一台机器`telnet`试一下看是否可以连接上。 -- 需要进行用户名及密码的认证:见`3、解决方法 —— 认证的JMX`。 +**2、正常现象** -错误日志例子: +Broker 列表的 JMX PORT 列出现绿色,则表示 JMX 连接正常。 + + + + +--- + + + + + + +### 2.2、异因一:JMX未开启 + +#### 2.2.1、异常现象 + +broker列表的JMX Port值为-1,对应Broker的JMX未开启。 + + + +#### 2.2.2、解决方案 + +开启JMX,开启流程如下: + +1、修改kafka的bin目录下面的:`kafka-server-start.sh`文件 + +```bash +# 在这个下面增加JMX端口的配置 +if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then + export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G" + export JMX_PORT=9999 # 增加这个配置, 这里的数值并不一定是要9999 +fi ``` -# 错误一: 错误提示的是真实的IP,这样的话基本就是JMX配置的有问题了。 -2021-01-27 10:06:20.730 ERROR 50901 --- [ics-Thread-1-62] c.x.k.m.c.utils.jmx.JmxConnectorWrap : JMX connect exception, host:192.168.0.1 port:9999. -java.rmi.ConnectException: Connection refused to host: 192.168.0.1; nested exception is: -# 错误二:错误提示的是127.0.0.1这个IP,这个是机器的hostname配置的可能有问题。 -2021-01-27 10:06:20.730 ERROR 50901 --- [ics-Thread-1-62] c.x.k.m.c.utils.jmx.JmxConnectorWrap : JMX connect exception, host:127.0.0.1 port:9999. -java.rmi.ConnectException: Connection refused to host: 127.0.0.1;; nested exception is: +2、修改kafka的bin目录下面对的:`kafka-run-class.sh`文件 + +```bash +# JMX settings +if [ -z "$KAFKA_JMX_OPTS" ]; then + KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=当前机器的IP" +fi + +# JMX port to use +if [ $JMX_PORT ]; then + KAFKA_JMX_OPTS="$KAFKA_JMX_OPTS -Dcom.sun.management.jmxremote.port=$JMX_PORT -Dcom.sun.management.jmxremote.rmi.port=$JMX_PORT" +fi ``` -**类型三:连接特定IP** -Broker 配置了内外网,而JMX在配置时,可能配置了内网IP或者外网IP,此时 `KnowStreaming` 需要连接到特定网络的IP才可以进行访问。 -比如: +3、重启Kafka-Broker。 -Broker在ZK的存储结构如下所示,我们期望连接到 `endpoints` 中标记为 `INTERNAL` 的地址,但是 `KnowStreaming` 却连接了 `EXTERNAL` 的地址,此时可以看 `4、解决方法 —— JMX连接特定网络` 进行解决。 -```json - { - "listener_security_protocol_map": {"EXTERNAL":"SASL_PLAINTEXT","INTERNAL":"SASL_PLAINTEXT"}, - "endpoints": ["EXTERNAL://192.168.0.1:7092","INTERNAL://192.168.0.2:7093"], - "jmx_port": 8099, - "host": "192.168.0.1", - "timestamp": "1627289710439", - "port": -1, - "version": 4 - } -``` +--- + + + + + + + +### 2.3、异原二:JMX配置错误 + +#### 2.3.1、异常现象 -### 2、解决方法 +错误日志: -这里仅介绍一下比较通用的解决方式,如若有更好的方式,欢迎大家指导告知一下。 +```log +# 错误一: 错误提示的是真实的IP,这样的话基本就是JMX配置的有问题了。 +2021-01-27 10:06:20.730 ERROR 50901 --- [ics-Thread-1-62] c.x.k.m.c.utils.jmx.JmxConnectorWrap : JMX connect exception, host:192.168.0.1 port:9999. java.rmi.ConnectException: Connection refused to host: 192.168.0.1; nested exception is: -修改`kafka-server-start.sh`文件: +# 错误二:错误提示的是127.0.0.1这个IP,这个是机器的hostname配置的可能有问题。 +2021-01-27 10:06:20.730 ERROR 50901 --- [ics-Thread-1-62] c.x.k.m.c.utils.jmx.JmxConnectorWrap : JMX connect exception, host:127.0.0.1 port:9999. java.rmi.ConnectException: Connection refused to host: 127.0.0.1;; nested exception is: ``` + +#### 2.3.2、解决方案 + +开启JMX,开启流程如下: + +1、修改kafka的bin目录下面的:`kafka-server-start.sh`文件 + +```bash # 在这个下面增加JMX端口的配置 if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then - export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G" - export JMX_PORT=9999 # 增加这个配置, 这里的数值并不一定是要9999 + export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G" + export JMX_PORT=9999 # 增加这个配置, 这里的数值并不一定是要9999 fi ``` -  +2、修改kafka的bin目录下面对的:`kafka-run-class.sh`文件 -修改`kafka-run-class.sh`文件 -``` -# JMX settings +```bash +# JMX settings if [ -z "$KAFKA_JMX_OPTS" ]; then - KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=${当前机器的IP}" -fi + KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=当前机器的IP" +fi -# JMX port to use +# JMX port to use if [ $JMX_PORT ]; then - KAFKA_JMX_OPTS="$KAFKA_JMX_OPTS -Dcom.sun.management.jmxremote.port=$JMX_PORT -Dcom.sun.management.jmxremote.rmi.port=$JMX_PORT" + KAFKA_JMX_OPTS="$KAFKA_JMX_OPTS -Dcom.sun.management.jmxremote.port=$JMX_PORT -Dcom.sun.management.jmxremote.rmi.port=$JMX_PORT" fi ``` +3、重启Kafka-Broker。 + + +--- + + + + + + + +### 2.4、异因三:JMX开启SSL + +#### 2.4.1、异常现象 + +```log +# 连接JMX的日志中,出现SSL认证失败的相关日志。TODO:欢迎补充具体日志案例。 +``` + +#### 2.4.2、解决方案 + + -### 3、解决方法 —— 认证的JMX -如果您是直接看的这个部分,建议先看一下上一节:`2、解决方法`以确保`JMX`的配置没有问题了。 +--- -在`JMX`的配置等都没有问题的情况下,如果是因为认证的原因导致连接不了的,可以在集群接入界面配置你的`JMX`认证信息。 - +### 2.5、异因四:连接了错误IP +#### 2.5.1、异常现象 +Broker 配置了内外网,而JMX在配置时,可能配置了内网IP或者外网IP,此时`KnowStreaming` 需要连接到特定网络的IP才可以进行访问。 -### 4、解决方法 —— JMX连接特定网络 + 比如:Broker在ZK的存储结构如下所示,我们期望连接到 `endpoints` 中标记为 `INTERNAL` 的地址,但是 `KnowStreaming` 却连接了 `EXTERNAL` 的地址。 + +```json +{ + "listener_security_protocol_map": { + "EXTERNAL": "SASL_PLAINTEXT", + "INTERNAL": "SASL_PLAINTEXT" + }, + "endpoints": [ + "EXTERNAL://192.168.0.1:7092", + "INTERNAL://192.168.0.2:7093" + ], + "jmx_port": 8099, + "host": "192.168.0.1", + "timestamp": "1627289710439", + "port": -1, + "version": 4 +} +``` + +#### 2.5.2、解决方案 可以手动往`ks_km_physical_cluster`表的`jmx_properties`字段增加一个`useWhichEndpoint`字段,从而控制 `KnowStreaming` 连接到特定的JMX IP及PORT。 `jmx_properties`格式: + ```json { - "maxConn": 100, # KM对单台Broker的最大JMX连接数 - "username": "xxxxx", # 用户名,可以不填写 - "password": "xxxx", # 密码,可以不填写 - "openSSL": true, # 开启SSL, true表示开启ssl, false表示关闭 - "useWhichEndpoint": "EXTERNAL" #指定要连接的网络名称,填写EXTERNAL就是连接endpoints里面的EXTERNAL地址 + "maxConn": 100, // KM对单台Broker的最大JMX连接数 + "username": "xxxxx", //用户名,可以不填写 + "password": "xxxx", // 密码,可以不填写 + "openSSL": true, //开启SSL, true表示开启ssl, false表示关闭 + "useWhichEndpoint": "EXTERNAL" //指定要连接的网络名称,填写EXTERNAL就是连接endpoints里面的EXTERNAL地址 } ``` -  + SQL例子: + ```sql UPDATE ks_km_physical_cluster SET jmx_properties='{ "maxConn": 10, "username": "xxxxx", "password": "xxxx", "openSSL": false , "useWhichEndpoint": "xxx"}' where id={xxx}; ``` -注意: -+ 目前此功能只支持采用 `ZK` 做分布式协调的kafka集群。 +--- + + + + + + +### 2.6、异因五:连接了错误端口 + +3.3.0 以上版本,或者是 master 分支最新代码,才具备该能力。 + +#### 2.6.1、异常现象 + +在 AWS 或者是容器上的 Kafka-Broker,使用同一个IP,但是外部服务想要去连接 JMX 端口时,需要进行映射。因此 KnowStreaming 如果直接连接 ZK 上获取到的 JMX 端口,会连接失败,因此需要具备连接端口可配置的能力。 + +TODO:补充具体的日志。 + + +#### 2.6.2、解决方案 + +可以手动往`ks_km_physical_cluster`表的`jmx_properties`字段增加一个`specifiedJmxPortList`字段,从而控制 `KnowStreaming` 连接到特定的JMX PORT。 + +`jmx_properties`格式: +```json +{ + "jmxPort": 2445, // 最低优先级使用的jmx端口 + "maxConn": 100, // KM对单台Broker的最大JMX连接数 + "username": "xxxxx", //用户名,可以不填写 + "password": "xxxx", // 密码,可以不填写 + "openSSL": true, //开启SSL, true表示开启ssl, false表示关闭 + "useWhichEndpoint": "EXTERNAL", //指定要连接的网络名称,填写EXTERNAL就是连接endpoints里面的EXTERNAL地址 + "specifiedJmxPortList": [ // 配置最高优先使用的jmx端口 + { + "serverId": "1", // kafka-broker的brokerId, 注意这个是字符串类型,字符串类型的原因是要兼容connect的jmx端口的连接 + "jmxPort": 1234 // 该 broker 所连接的jmx端口 + }, + { + "serverId": "2", + "jmxPort": 1234 + }, + ] +} +``` + + + +SQL例子: + +```sql +UPDATE ks_km_physical_cluster SET jmx_properties='{ "maxConn": 10, "username": "xxxxx", "password": "xxxx", "openSSL": false , "specifiedJmxPortList": [{"serverId": "1", "jmxPort": 1234}] }' where id={xxx}; +``` + - \ No newline at end of file +--- diff --git "a/docs/dev_guide/\351\241\265\351\235\242\346\227\240\346\225\260\346\215\256\346\216\222\346\237\245\346\211\213\345\206\214.md" "b/docs/dev_guide/\351\241\265\351\235\242\346\227\240\346\225\260\346\215\256\346\216\222\346\237\245\346\211\213\345\206\214.md" index b8209086c..6fdd136f4 100644 --- "a/docs/dev_guide/\351\241\265\351\235\242\346\227\240\346\225\260\346\215\256\346\216\222\346\237\245\346\211\213\345\206\214.md" +++ "b/docs/dev_guide/\351\241\265\351\235\242\346\227\240\346\225\260\346\215\256\346\216\222\346\237\245\346\211\213\345\206\214.md" @@ -2,6 +2,23 @@ # 页面无数据排查手册 +- [页面无数据排查手册](#页面无数据排查手册) + - [1、集群接入错误](#1集群接入错误) + - [1.1、异常现象](#11异常现象) + - [1.2、解决方案](#12解决方案) + - [1.3、正常情况](#13正常情况) + - [2、JMX连接失败](#2jmx连接失败) + - [3、ElasticSearch问题](#3elasticsearch问题) + - [3.1、异因一:缺少索引](#31异因一缺少索引) + - [3.1.1、异常现象](#311异常现象) + - [3.1.2、解决方案](#312解决方案) + - [3.2、异因二:索引模板错误](#32异因二索引模板错误) + - [3.2.1、异常现象](#321异常现象) + - [3.2.2、解决方案](#322解决方案) + - [3.3、异因三:集群Shard满](#33异因三集群shard满) + - [3.3.1、异常现象](#331异常现象) + - [3.3.2、解决方案](#332解决方案) + --- @@ -37,184 +54,8 @@ 背景:Kafka 通过 JMX 服务进行运行指标的暴露,因此 `KnowStreaming` 会主动连接 Kafka 的 JMX 服务进行指标采集。如果我们发现页面缺少指标,那么可能原因之一是 Kafka 的 JMX 端口配置的有问题导致指标获取失败,进而页面没有数据。 -### 2.1、正异常现象 - -**1、异常现象** - -Broker 列表的 JMX PORT 列出现红色感叹号,则表示 JMX 连接存在异常。 - - - - -**2、正常现象** - -Broker 列表的 JMX PORT 列出现绿色,则表示 JMX 连接正常。 - - - - ---- - - -### 2.2、异因一:JMX未开启 - -#### 2.2.1、异常现象 - -broker列表的JMX Port值为-1,对应Broker的JMX未开启。 - - - -#### 2.2.2、解决方案 - -开启JMX,开启流程如下: - -1、修改kafka的bin目录下面的:`kafka-server-start.sh`文件 - -```bash -# 在这个下面增加JMX端口的配置 -if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then - export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G" - export JMX_PORT=9999 # 增加这个配置, 这里的数值并不一定是要9999 -fi -``` - - -2、修改kafka的bin目录下面对的:`kafka-run-class.sh`文件 - -```bash -# JMX settings -if [ -z "$KAFKA_JMX_OPTS" ]; then - KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=${当前机器的IP}" -fi - -# JMX port to use -if [ $JMX_PORT ]; then - KAFKA_JMX_OPTS="$KAFKA_JMX_OPTS -Dcom.sun.management.jmxremote.port=$JMX_PORT -Dcom.sun.management.jmxremote.rmi.port=$JMX_PORT" -fi -``` - - - -3、重启Kafka-Broker。 - - ---- - - - -### 2.3、异原二:JMX配置错误 - -#### 2.3.1、异常现象 - -错误日志: - -```log -# 错误一: 错误提示的是真实的IP,这样的话基本就是JMX配置的有问题了。 -2021-01-27 10:06:20.730 ERROR 50901 --- [ics-Thread-1-62] c.x.k.m.c.utils.jmx.JmxConnectorWrap : JMX connect exception, host:192.168.0.1 port:9999. java.rmi.ConnectException: Connection refused to host: 192.168.0.1; nested exception is: - -# 错误二:错误提示的是127.0.0.1这个IP,这个是机器的hostname配置的可能有问题。 -2021-01-27 10:06:20.730 ERROR 50901 --- [ics-Thread-1-62] c.x.k.m.c.utils.jmx.JmxConnectorWrap : JMX connect exception, host:127.0.0.1 port:9999. java.rmi.ConnectException: Connection refused to host: 127.0.0.1;; nested exception is: -``` - -#### 2.3.2、解决方案 -开启JMX,开启流程如下: - -1、修改kafka的bin目录下面的:`kafka-server-start.sh`文件 - -```bash -# 在这个下面增加JMX端口的配置 -if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then - export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G" - export JMX_PORT=9999 # 增加这个配置, 这里的数值并不一定是要9999 -fi -``` - -2、修改kafka的bin目录下面对的:`kafka-run-class.sh`文件 - -```bash -# JMX settings -if [ -z "$KAFKA_JMX_OPTS" ]; then - KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=${当前机器的IP}" -fi - -# JMX port to use -if [ $JMX_PORT ]; then - KAFKA_JMX_OPTS="$KAFKA_JMX_OPTS -Dcom.sun.management.jmxremote.port=$JMX_PORT -Dcom.sun.management.jmxremote.rmi.port=$JMX_PORT" -fi -``` - -3、重启Kafka-Broker。 - - ---- - - -### 2.4、异因三:JMX开启SSL - -#### 2.4.1、异常现象 - -```log -# 连接JMX的日志中,出现SSL认证失败的相关日志。TODO:欢迎补充具体日志案例。 -``` - -#### 2.4.2、解决方案 - - - - ---- - - -### 2.5、异因四:连接了错误IP - -#### 2.5.1、异常现象 - -Broker 配置了内外网,而JMX在配置时,可能配置了内网IP或者外网IP,此时`KnowStreaming` 需要连接到特定网络的IP才可以进行访问。 - - 比如:Broker在ZK的存储结构如下所示,我们期望连接到 `endpoints` 中标记为 `INTERNAL` 的地址,但是 `KnowStreaming` 却连接了 `EXTERNAL` 的地址。 - -```json -{ - "listener_security_protocol_map": { - "EXTERNAL": "SASL_PLAINTEXT", - "INTERNAL": "SASL_PLAINTEXT" - }, - "endpoints": [ - "EXTERNAL://192.168.0.1:7092", - "INTERNAL://192.168.0.2:7093" - ], - "jmx_port": 8099, - "host": "192.168.0.1", - "timestamp": "1627289710439", - "port": -1, - "version": 4 -} -``` - -#### 2.5.2、解决方案 - -可以手动往`ks_km_physical_cluster`表的`jmx_properties`字段增加一个`useWhichEndpoint`字段,从而控制 `KnowStreaming` 连接到特定的JMX IP及PORT。 - -`jmx_properties`格式: - -```json -{ - "maxConn": 100, // KM对单台Broker的最大JMX连接数 - "username": "xxxxx", //用户名,可以不填写 - "password": "xxxx", // 密码,可以不填写 - "openSSL": true, //开启SSL, true表示开启ssl, false表示关闭 - "useWhichEndpoint": "EXTERNAL" //指定要连接的网络名称,填写EXTERNAL就是连接endpoints里面的EXTERNAL地址 -} -``` - - - -SQL例子: - -```sql -UPDATE ks_km_physical_cluster SET jmx_properties='{ "maxConn": 10, "username": "xxxxx", "password": "xxxx", "openSSL": false , "useWhichEndpoint": "xxx"}' where id={xxx}; -``` +具体见同目录下的文档:[解决连接JMX失败](./%E8%A7%A3%E5%86%B3%E8%BF%9E%E6%8E%A5JMX%E5%A4%B1%E8%B4%A5.md) --- From e1e02f7c2a9ac55f043ffd83d9882241b433acbc Mon Sep 17 00:00:00 2001 From: jeff-zou Date: Sat, 3 Jun 2023 12:28:57 +0800 Subject: [PATCH 18/37] =?UTF-8?q?=E6=97=A5=E5=BF=97=E8=BE=93=E5=87=BA?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=94=AF=E6=8C=81MDC=EF=BC=8C=E6=96=B9?= =?UTF-8?q?=E4=BE=BF=E7=94=A8=E6=88=B7=E5=9C=A8logback.xml=E4=B8=ADjson?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E6=97=A5=E5=BF=97=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit true {"system": "know-streaming"} --- pom.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pom.xml b/pom.xml index 4f672e85d..d1e173d30 100644 --- a/pom.xml +++ b/pom.xml @@ -329,6 +329,14 @@ import + + + + net.logstash.logback + logstash-logback-encoder + 7.1.1 + +
\ No newline at end of file From 7f7801a5f731eaf745a1c21145d5aabc1fc4284e Mon Sep 17 00:00:00 2001 From: Richard <18310231437@163.com> Date: Tue, 20 Jun 2023 10:56:22 +0800 Subject: [PATCH 19/37] =?UTF-8?q?[Bugfix]=E6=B6=88=E8=B4=B9=E7=BB=84?= =?UTF-8?q?=E4=B8=8D=E6=94=AF=E6=8C=81=E9=87=8D=E7=BD=AE=E5=88=B0=E6=9C=80?= =?UTF-8?q?=E6=97=A7Offset=E7=9A=84=E9=97=AE=E9=A2=98=20(#1039)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/ConsumerGroup/ResetOffsetDrawer.tsx | 9 +++++---- .../src/pages/Consumers/ResetOffsetDrawer.tsx | 9 +++++---- .../src/pages/TopicDetail/ResetOffsetDrawer.tsx | 9 +++++---- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/km-console/packages/layout-clusters-fe/src/pages/ConsumerGroup/ResetOffsetDrawer.tsx b/km-console/packages/layout-clusters-fe/src/pages/ConsumerGroup/ResetOffsetDrawer.tsx index b03f68327..75cc390a0 100644 --- a/km-console/packages/layout-clusters-fe/src/pages/ConsumerGroup/ResetOffsetDrawer.tsx +++ b/km-console/packages/layout-clusters-fe/src/pages/ConsumerGroup/ResetOffsetDrawer.tsx @@ -19,18 +19,19 @@ const CustomSelectResetTime = (props: { value?: string; onChange?: (val: Number }} onChange={(e) => { setTimeSetMode(e.target.value); - if (e.target.value === 'newest') { - onChange('newest'); + if (e.target.value === 'newest' || e.target.value === 'oldest') { + onChange(e.target.value); } }} value={timeSetMode} > 最新Offset + 最旧Offset 自定义 {timeSetMode === 'custom' && ( { @@ -88,7 +89,7 @@ export default (props: any) => { topicName: record.topicName, }; if (formData.resetType === 'assignedTime') { - resetParams.resetType = formData.timestamp === 'newest' ? 0 : 2; + resetParams.resetType = formData.timestamp === 'newest' ? 0 : formData.timestamp === 'oldest' ? 1 : 2; if (resetParams.resetType === 2) { resetParams.timestamp = formData.timestamp; } diff --git a/km-console/packages/layout-clusters-fe/src/pages/Consumers/ResetOffsetDrawer.tsx b/km-console/packages/layout-clusters-fe/src/pages/Consumers/ResetOffsetDrawer.tsx index bba5059e2..7a2b02e7c 100644 --- a/km-console/packages/layout-clusters-fe/src/pages/Consumers/ResetOffsetDrawer.tsx +++ b/km-console/packages/layout-clusters-fe/src/pages/Consumers/ResetOffsetDrawer.tsx @@ -22,18 +22,19 @@ const CustomSelectResetTime = (props: { value?: string; onChange?: (val: number }} onChange={(e) => { setTimeSetMode(e.target.value); - if (e.target.value === 'newest') { - onChange('newest'); + if (e.target.value === 'newest' || e.target.value === 'oldest') { + onChange(e.target.value); } }} value={timeSetMode} > 最新Offset + 最旧Offset 自定义 {timeSetMode === 'custom' && ( { @@ -91,7 +92,7 @@ export default (props: any) => { topicName: record.topicName, }; if (formData.resetType === 'assignedTime') { - resetParams.resetType = formData.timestamp === 'newest' ? 0 : 2; + resetParams.resetType = formData.timestamp === 'newest' ? 0 : formData.timestamp === 'oldest' ? 1 : 2; if (resetParams.resetType === 2) { resetParams.timestamp = formData.timestamp; } diff --git a/km-console/packages/layout-clusters-fe/src/pages/TopicDetail/ResetOffsetDrawer.tsx b/km-console/packages/layout-clusters-fe/src/pages/TopicDetail/ResetOffsetDrawer.tsx index c079393b0..ed948e8c0 100644 --- a/km-console/packages/layout-clusters-fe/src/pages/TopicDetail/ResetOffsetDrawer.tsx +++ b/km-console/packages/layout-clusters-fe/src/pages/TopicDetail/ResetOffsetDrawer.tsx @@ -19,18 +19,19 @@ const CustomSelectResetTime = (props: { value?: string; onChange?: (val: Number }} onChange={(e) => { setTimeSetMode(e.target.value); - if (e.target.value === 'newest') { - onChange('newest'); + if (e.target.value === 'newest' || e.target.value === 'oldest') { + onChange(e.target.value); } }} value={timeSetMode} > 最新Offset + 最旧Offset 自定义 {timeSetMode === 'custom' && ( { @@ -88,7 +89,7 @@ export default (props: any) => { topicName: record.topicName, }; if (formData.resetType === 'assignedTime') { - resetParams.resetType = formData.timestamp === 'newest' ? 0 : 2; + resetParams.resetType = formData.timestamp === 'newest' ? 0 : formData.timestamp === 'oldest' ? 1 : 2; if (resetParams.resetType === 2) { resetParams.timestamp = formData.timestamp; } From 3f518c9e63b7e7a8836df4e6b70cb3b5165a0527 Mon Sep 17 00:00:00 2001 From: SUZJ <905430093@qq.com> Date: Tue, 6 Jun 2023 19:01:08 +0800 Subject: [PATCH 20/37] =?UTF-8?q?[Bugfix]=E4=BF=AE=E5=A4=8D=E6=9D=83?= =?UTF-8?q?=E9=99=90ACL=E7=AE=A1=E7=90=86=E4=B8=AD=EF=BC=8C=E6=B6=88?= =?UTF-8?q?=E8=B4=B9=E7=BB=84=E5=88=97=E8=A1=A8=E5=B1=95=E7=A4=BA=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E7=9A=84=E9=97=AE=E9=A2=98(#991)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layout-clusters-fe/src/api/index.ts | 1 + .../src/pages/SecurityACLs/EditDrawer.tsx | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/km-console/packages/layout-clusters-fe/src/api/index.ts b/km-console/packages/layout-clusters-fe/src/api/index.ts index 1c6bc77ea..7dadd9ec8 100755 --- a/km-console/packages/layout-clusters-fe/src/api/index.ts +++ b/km-console/packages/layout-clusters-fe/src/api/index.ts @@ -94,6 +94,7 @@ const api = { getTopicGroupPartitionsHistory: (clusterPhyId: number, groupName: string) => getApi(`/clusters/${clusterPhyId}/groups/${groupName}/partitions`), resetGroupOffset: () => getApi('/group-offsets'), + getGroupOverview: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/groups-overview`), // topics列表 getTopicsList: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/topics-overview`), diff --git a/km-console/packages/layout-clusters-fe/src/pages/SecurityACLs/EditDrawer.tsx b/km-console/packages/layout-clusters-fe/src/pages/SecurityACLs/EditDrawer.tsx index 8afcdccc7..a325a9bce 100644 --- a/km-console/packages/layout-clusters-fe/src/pages/SecurityACLs/EditDrawer.tsx +++ b/km-console/packages/layout-clusters-fe/src/pages/SecurityACLs/EditDrawer.tsx @@ -85,6 +85,7 @@ const AddDrawer = forwardRef((_, ref) => { return; }); const [topicMetaData, setTopicMetaData] = React.useState([]); + const [groupMetaData, setGroupMetaData] = React.useState([]); // 获取 Topic 元信息 const getTopicMetaData = (newValue: any) => { @@ -102,6 +103,21 @@ const AddDrawer = forwardRef((_, ref) => { }); }; + // 获取 Group 元信息 + const getGroupMetaData = () => { + Utils.request(api.getGroupOverview(+clusterId), { + method: 'GET', + }).then((res: any) => { + const groups = res?.bizData.map((item: any) => { + return { + label: item.name, + value: item.name, + }; + }); + setGroupMetaData(groups); + }); + }; + // 获取 kafkaUser 列表 const getKafkaUserList = () => { Utils.request(api.getKafkaUsers(clusterId), { @@ -209,6 +225,7 @@ const AddDrawer = forwardRef((_, ref) => { useEffect(() => { getKafkaUserList(); getTopicMetaData(''); + getGroupMetaData(); }, []); return ( @@ -321,7 +338,7 @@ const AddDrawer = forwardRef((_, ref) => { } return false; }} - options={topicMetaData} + options={type === 'topic' ? topicMetaData : groupMetaData} placeholder={`请输入 ${type}Name`} /> From c2bc0f788d71b09c4c2c23f02a5149f7a9299b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=AF=B8=E8=91=9B=E5=AD=90=E6=88=BF?= Date: Tue, 27 Jun 2023 10:58:00 +0800 Subject: [PATCH 21/37] =?UTF-8?q?[Feature]=E5=A2=9E=E5=8A=A0Truncate?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=8A=9F=E8=83=BD(#1062)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增加Truncate数据功能(#1043) 目前已经完成后端部分,前端待补充。 --------- Co-authored-by: duanxiaoqiu --- .../km/biz/topic/OpTopicManager.java | 5 ++ .../km/biz/topic/impl/OpTopicManagerImpl.java | 12 +++++ .../param/topic/TopicTruncateParam.java | 29 ++++++++++ .../km/common/constant/KafkaConstant.java | 2 + .../enums/operaterecord/OperationEnum.java | 2 + .../km/core/service/topic/OpTopicService.java | 6 +++ .../topic/impl/OpTopicServiceImpl.java | 54 +++++++++++++++++++ .../fe/FrontEndControlVersionItems.java | 6 +++ .../km/rest/api/v3/topic/TopicController.java | 7 +++ 9 files changed, 123 insertions(+) create mode 100644 km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/param/topic/TopicTruncateParam.java diff --git a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/topic/OpTopicManager.java b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/topic/OpTopicManager.java index 5c3fa742a..90b7e241f 100644 --- a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/topic/OpTopicManager.java +++ b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/topic/OpTopicManager.java @@ -19,4 +19,9 @@ public interface OpTopicManager { * 扩分区 */ Result expandTopic(TopicExpansionDTO dto, String operator); + + /** + * 清空Topic + */ + Result truncateTopic(Long clusterPhyId, String topicName, String operator); } diff --git a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/topic/impl/OpTopicManagerImpl.java b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/topic/impl/OpTopicManagerImpl.java index 5d27ed746..22d204ea5 100644 --- a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/topic/impl/OpTopicManagerImpl.java +++ b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/topic/impl/OpTopicManagerImpl.java @@ -10,10 +10,12 @@ import com.xiaojukeji.know.streaming.km.common.bean.entity.param.topic.TopicCreateParam; import com.xiaojukeji.know.streaming.km.common.bean.entity.param.topic.TopicParam; import com.xiaojukeji.know.streaming.km.common.bean.entity.param.topic.TopicPartitionExpandParam; +import com.xiaojukeji.know.streaming.km.common.bean.entity.param.topic.TopicTruncateParam; import com.xiaojukeji.know.streaming.km.common.bean.entity.partition.Partition; import com.xiaojukeji.know.streaming.km.common.bean.entity.result.Result; import com.xiaojukeji.know.streaming.km.common.bean.entity.result.ResultStatus; import com.xiaojukeji.know.streaming.km.common.bean.entity.topic.Topic; +import com.xiaojukeji.know.streaming.km.common.constant.KafkaConstant; import com.xiaojukeji.know.streaming.km.common.constant.MsgConstant; import com.xiaojukeji.know.streaming.km.common.utils.BackoffUtils; import com.xiaojukeji.know.streaming.km.common.utils.FutureUtil; @@ -156,6 +158,16 @@ public Result expandTopic(TopicExpansionDTO dto, String operator) { return rv; } + @Override + public Result truncateTopic(Long clusterPhyId, String topicName, String operator) { + // 清空Topic + Result rv = opTopicService.truncateTopic(new TopicTruncateParam(clusterPhyId, topicName, KafkaConstant.TOPICK_TRUNCATE_DEFAULT_OFFSET), operator); + if (rv.failed()) { + return rv; + } + + return Result.buildSuc(); + } /**************************************************** private method ****************************************************/ diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/param/topic/TopicTruncateParam.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/param/topic/TopicTruncateParam.java new file mode 100644 index 000000000..8186b3e5f --- /dev/null +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/param/topic/TopicTruncateParam.java @@ -0,0 +1,29 @@ +package com.xiaojukeji.know.streaming.km.common.bean.entity.param.topic; + +import com.xiaojukeji.know.streaming.km.common.bean.entity.param.cluster.ClusterPhyParam; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TopicTruncateParam extends ClusterPhyParam { + protected String topicName; + protected long offset; + + public TopicTruncateParam(Long clusterPhyId, String topicName, long offset) { + super(clusterPhyId); + this.topicName = topicName; + this.offset = offset; + } + + @Override + public String toString() { + return "TopicParam{" + + "clusterPhyId=" + clusterPhyId + + ", topicName='" + topicName + '\'' + + ", offset='" + offset + '\'' + + '}'; + } +} diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/constant/KafkaConstant.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/constant/KafkaConstant.java index 465f6f8ac..a3f959bf3 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/constant/KafkaConstant.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/constant/KafkaConstant.java @@ -49,6 +49,8 @@ public class KafkaConstant { public static final Map KAFKA_ALL_CONFIG_DEF_MAP = new ConcurrentHashMap<>(); + public static final Integer TOPICK_TRUNCATE_DEFAULT_OFFSET = -1; + static { try { KAFKA_ALL_CONFIG_DEF_MAP.putAll(CollectionConverters.asJava(LogConfig$.MODULE$.configKeys())); diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/enums/operaterecord/OperationEnum.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/enums/operaterecord/OperationEnum.java index 302cb38b0..da25bc14f 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/enums/operaterecord/OperationEnum.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/enums/operaterecord/OperationEnum.java @@ -32,6 +32,8 @@ public enum OperationEnum { RESTART(11, "重启"), + TRUNCATE(12, "清空"), + ; OperationEnum(int code, String desc) { diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/topic/OpTopicService.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/topic/OpTopicService.java index 3b529f817..1f656a6e1 100644 --- a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/topic/OpTopicService.java +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/topic/OpTopicService.java @@ -3,6 +3,7 @@ import com.xiaojukeji.know.streaming.km.common.bean.entity.param.topic.TopicCreateParam; import com.xiaojukeji.know.streaming.km.common.bean.entity.param.topic.TopicParam; import com.xiaojukeji.know.streaming.km.common.bean.entity.param.topic.TopicPartitionExpandParam; +import com.xiaojukeji.know.streaming.km.common.bean.entity.param.topic.TopicTruncateParam; import com.xiaojukeji.know.streaming.km.common.bean.entity.result.Result; import com.xiaojukeji.know.streaming.km.common.bean.entity.topic.Topic; @@ -21,4 +22,9 @@ public interface OpTopicService { * 扩分区 */ Result expandTopic(TopicPartitionExpandParam expandParam, String operator); + + /** + * 清空topic消息 + */ + Result truncateTopic(TopicTruncateParam param, String operator); } diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/topic/impl/OpTopicServiceImpl.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/topic/impl/OpTopicServiceImpl.java index 466f7a2fa..bb3e553d6 100644 --- a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/topic/impl/OpTopicServiceImpl.java +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/topic/impl/OpTopicServiceImpl.java @@ -8,6 +8,7 @@ import com.xiaojukeji.know.streaming.km.common.bean.entity.param.topic.TopicCreateParam; import com.xiaojukeji.know.streaming.km.common.bean.entity.param.topic.TopicParam; import com.xiaojukeji.know.streaming.km.common.bean.entity.param.topic.TopicPartitionExpandParam; +import com.xiaojukeji.know.streaming.km.common.bean.entity.param.topic.TopicTruncateParam; import com.xiaojukeji.know.streaming.km.common.bean.entity.result.Result; import com.xiaojukeji.know.streaming.km.common.bean.entity.result.ResultStatus; import com.xiaojukeji.know.streaming.km.common.constant.KafkaConstant; @@ -33,6 +34,7 @@ import kafka.zk.KafkaZkClient; import org.apache.kafka.clients.admin.*; import org.apache.kafka.common.TopicPartition; +import org.apache.kafka.common.TopicPartitionInfo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import scala.Option; @@ -57,6 +59,7 @@ public class OpTopicServiceImpl extends BaseKafkaVersionControlService implement private static final String TOPIC_CREATE = "createTopic"; private static final String TOPIC_DELETE = "deleteTopic"; private static final String TOPIC_EXPAND = "expandTopic"; + private static final String TOPIC_TRUNCATE = "truncateTopic"; @Autowired private TopicService topicService; @@ -92,6 +95,8 @@ private void init() { registerVCHandler(TOPIC_EXPAND, V_0_10_0_0, V_0_11_0_3, "expandTopicByZKClient", this::expandTopicByZKClient); registerVCHandler(TOPIC_EXPAND, V_0_11_0_3, V_MAX, "expandTopicByKafkaClient", this::expandTopicByKafkaClient); + + registerVCHandler(TOPIC_TRUNCATE, V_0_11_0_0, V_MAX, "truncateTopicByKafkaClient", this::truncateTopicByKafkaClient); } @Override @@ -203,9 +208,58 @@ public Result expandTopic(TopicPartitionExpandParam expandParam, String op return rv; } + @Override + public Result truncateTopic(TopicTruncateParam param, String operator) { + try { + // 清空topic数据 + Result rv = (Result) doVCHandler(param.getClusterPhyId(), TOPIC_TRUNCATE, param); + + if (rv == null || rv.failed()) { + return rv; + } + + // 记录操作 + OplogDTO oplogDTO = new OplogDTO(operator, + OperationEnum.TRUNCATE.getDesc(), + ModuleEnum.KAFKA_TOPIC.getDesc(), + MsgConstant.getTopicBizStr(param.getClusterPhyId(), param.getTopicName()), + String.format("清空Topic:[%s]", param.toString())); + opLogWrapService.saveOplogAndIgnoreException(oplogDTO); + return rv; + } catch (VCHandlerNotExistException e) { + return Result.buildFailure(VC_HANDLE_NOT_EXIST); + } + } /**************************************************** private method ****************************************************/ + private Result truncateTopicByKafkaClient(VersionItemParam itemParam) { + TopicTruncateParam param = (TopicTruncateParam) itemParam; + try { + AdminClient adminClient = kafkaAdminClient.getClient(param.getClusterPhyId()); + //获取topic的分区信息 + DescribeTopicsResult describeTopicsResult = adminClient.describeTopics(Arrays.asList(param.getTopicName()), new DescribeTopicsOptions().timeoutMs(KafkaConstant.ADMIN_CLIENT_REQUEST_TIME_OUT_UNIT_MS)); + Map descriptionMap = describeTopicsResult.all().get(); + + Map recordsToDelete = new HashMap<>(); + RecordsToDelete recordsToDeleteOffset = RecordsToDelete.beforeOffset(param.getOffset()); + + descriptionMap.forEach((topicName, topicDescription) -> { + for (TopicPartitionInfo topicPartition : topicDescription.partitions()) { + recordsToDelete.put(new TopicPartition(topicName, topicPartition.partition()), recordsToDeleteOffset); + } + }); + + DeleteRecordsResult deleteRecordsResult = adminClient.deleteRecords(recordsToDelete, new DeleteRecordsOptions().timeoutMs(KafkaConstant.ADMIN_CLIENT_REQUEST_TIME_OUT_UNIT_MS)); + deleteRecordsResult.all().get(); + } catch (Exception e) { + log.error("truncate topic by kafka-client failed,clusterPhyId:{} topicName:{} offset:{}", param.getClusterPhyId(), param.getTopicName(), param.getOffset(), e); + + return Result.buildFromRSAndMsg(ResultStatus.KAFKA_OPERATE_FAILED, e.getMessage()); + } + + return Result.buildSuc(); + } private Result deleteByKafkaClient(VersionItemParam itemParam) { TopicParam param = (TopicParam) itemParam; diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/version/fe/FrontEndControlVersionItems.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/version/fe/FrontEndControlVersionItems.java index 3f4c0a687..db8baa4aa 100644 --- a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/version/fe/FrontEndControlVersionItems.java +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/version/fe/FrontEndControlVersionItems.java @@ -36,6 +36,8 @@ public class FrontEndControlVersionItems extends BaseMetricVersionMetric { private static final String FE_HA_CREATE_MIRROR_TOPIC = "FEHaCreateMirrorTopic"; private static final String FE_HA_DELETE_MIRROR_TOPIC = "FEHaDeleteMirrorTopic"; + private static final String FE_TRUNCATE_TOPIC = "FETruncateTopic"; + public FrontEndControlVersionItems(){} @Override @@ -89,6 +91,10 @@ public List init(){ itemList.add(buildItem().minVersion(VersionEnum.V_2_5_0_D_300).maxVersion(VersionEnum.V_2_5_0_D_MAX) .name(FE_HA_DELETE_MIRROR_TOPIC).desc("HA-取消Topic复制")); + //truncate topic + itemList.add(buildItem().minVersion(VersionEnum.V_0_11_0_0).maxVersion(VersionEnum.V_MAX) + .name(FE_TRUNCATE_TOPIC).desc("清空topic")); + return itemList; } } diff --git a/km-rest/src/main/java/com/xiaojukeji/know/streaming/km/rest/api/v3/topic/TopicController.java b/km-rest/src/main/java/com/xiaojukeji/know/streaming/km/rest/api/v3/topic/TopicController.java index a2022b8a5..9eaee2df4 100644 --- a/km-rest/src/main/java/com/xiaojukeji/know/streaming/km/rest/api/v3/topic/TopicController.java +++ b/km-rest/src/main/java/com/xiaojukeji/know/streaming/km/rest/api/v3/topic/TopicController.java @@ -61,6 +61,13 @@ public Result expandTopics(@Validated @RequestBody TopicExpansionDTO dto) return opTopicManager.expandTopic(dto, HttpRequestUtil.getOperator()); } + @ApiOperation(value = "Topic数据清空", notes = "") + @PostMapping(value = "topics/truncate-topic") + @ResponseBody + public Result truncateTopic(@Validated @RequestBody ClusterTopicDTO dto) { + return opTopicManager.truncateTopic(dto.getClusterId(), dto.getTopicName(), HttpRequestUtil.getOperator()); + } + @ApiOperation(value = "Topic元信息", notes = "带是否存在信息") @GetMapping(value = "clusters/{clusterPhyId}/topics/{topicName}/metadata-combine-exist") @ResponseBody From b34edb9b641368877064a01da8c01aa72034edfa Mon Sep 17 00:00:00 2001 From: EricZeng Date: Tue, 27 Jun 2023 14:32:57 +0800 Subject: [PATCH 22/37] =?UTF-8?q?[Feature]=E6=96=B0=E5=A2=9E=E5=88=A0?= =?UTF-8?q?=E9=99=A4Group=E6=88=96GroupOffset=E5=8A=9F=E8=83=BD=20(#1064)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 不包括前端,后端新增 1、新增Group删除功能; 2、新增Group-Topic纬度Offset删除功能; 3、新增Group-Topic-Partition纬度Offset删除功能; --- .../streaming/km/biz/group/GroupManager.java | 3 + .../km/biz/group/impl/GroupManagerImpl.java | 59 +++- .../bean/dto/group/GroupOffsetDeleteDTO.java | 40 +++ .../entity/param/group/DeleteGroupParam.java | 16 ++ .../param/group/DeleteGroupTopicParam.java | 16 ++ .../group/DeleteGroupTopicPartitionParam.java | 16 ++ .../bean/entity/param/group/GroupParam.java | 2 - .../enums/group/DeleteGroupTypeEnum.java | 28 ++ .../enums/version/VersionItemTypeEnum.java | 2 + .../km/core/service/group/OpGroupService.java | 11 + .../group/impl/OpGroupServiceImpl.java | 255 ++++++++++++++++++ .../fe/FrontEndControlVersionItems.java | 9 +- .../km/rest/api/v3/group/GroupController.java | 10 +- 13 files changed, 460 insertions(+), 7 deletions(-) create mode 100644 km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/dto/group/GroupOffsetDeleteDTO.java create mode 100644 km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/param/group/DeleteGroupParam.java create mode 100644 km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/param/group/DeleteGroupTopicParam.java create mode 100644 km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/param/group/DeleteGroupTopicPartitionParam.java create mode 100644 km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/enums/group/DeleteGroupTypeEnum.java create mode 100644 km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/OpGroupService.java create mode 100644 km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/impl/OpGroupServiceImpl.java diff --git a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/GroupManager.java b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/GroupManager.java index 5a6d3ac66..a6b41eef5 100644 --- a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/GroupManager.java +++ b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/GroupManager.java @@ -1,6 +1,7 @@ package com.xiaojukeji.know.streaming.km.biz.group; import com.xiaojukeji.know.streaming.km.common.bean.dto.cluster.ClusterGroupSummaryDTO; +import com.xiaojukeji.know.streaming.km.common.bean.dto.group.GroupOffsetDeleteDTO; import com.xiaojukeji.know.streaming.km.common.bean.dto.group.GroupOffsetResetDTO; import com.xiaojukeji.know.streaming.km.common.bean.dto.pagination.PaginationBaseDTO; import com.xiaojukeji.know.streaming.km.common.bean.dto.pagination.PaginationSortDTO; @@ -39,5 +40,7 @@ PaginationResult pagingGroupTopicConsumedMetrics(Lon Result resetGroupOffsets(GroupOffsetResetDTO dto, String operator) throws Exception; + Result deleteGroupOffsets(GroupOffsetDeleteDTO dto, String operator) throws Exception; + List getGroupTopicOverviewVOList(Long clusterPhyId, List groupMemberPOList); } diff --git a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/impl/GroupManagerImpl.java b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/impl/GroupManagerImpl.java index fd05c2181..c80c40205 100644 --- a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/impl/GroupManagerImpl.java +++ b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/impl/GroupManagerImpl.java @@ -4,6 +4,7 @@ import com.didiglobal.logi.log.LogFactory; import com.xiaojukeji.know.streaming.km.biz.group.GroupManager; import com.xiaojukeji.know.streaming.km.common.bean.dto.cluster.ClusterGroupSummaryDTO; +import com.xiaojukeji.know.streaming.km.common.bean.dto.group.GroupOffsetDeleteDTO; import com.xiaojukeji.know.streaming.km.common.bean.dto.group.GroupOffsetResetDTO; import com.xiaojukeji.know.streaming.km.common.bean.dto.pagination.PaginationBaseDTO; import com.xiaojukeji.know.streaming.km.common.bean.dto.pagination.PaginationSortDTO; @@ -17,6 +18,9 @@ import com.xiaojukeji.know.streaming.km.common.bean.entity.kafka.KSMemberDescription; import com.xiaojukeji.know.streaming.km.common.bean.entity.metrics.GroupMetrics; import com.xiaojukeji.know.streaming.km.common.bean.entity.offset.KSOffsetSpec; +import com.xiaojukeji.know.streaming.km.common.bean.entity.param.group.DeleteGroupParam; +import com.xiaojukeji.know.streaming.km.common.bean.entity.param.group.DeleteGroupTopicParam; +import com.xiaojukeji.know.streaming.km.common.bean.entity.param.group.DeleteGroupTopicPartitionParam; import com.xiaojukeji.know.streaming.km.common.bean.entity.result.PaginationResult; import com.xiaojukeji.know.streaming.km.common.bean.entity.result.Result; import com.xiaojukeji.know.streaming.km.common.bean.entity.topic.Topic; @@ -32,6 +36,7 @@ import com.xiaojukeji.know.streaming.km.common.enums.AggTypeEnum; import com.xiaojukeji.know.streaming.km.common.enums.OffsetTypeEnum; import com.xiaojukeji.know.streaming.km.common.enums.SortTypeEnum; +import com.xiaojukeji.know.streaming.km.common.enums.group.DeleteGroupTypeEnum; import com.xiaojukeji.know.streaming.km.common.enums.group.GroupStateEnum; import com.xiaojukeji.know.streaming.km.common.exception.AdminOperateException; import com.xiaojukeji.know.streaming.km.common.exception.NotExistException; @@ -42,6 +47,7 @@ import com.xiaojukeji.know.streaming.km.core.service.cluster.ClusterPhyService; import com.xiaojukeji.know.streaming.km.core.service.group.GroupMetricService; import com.xiaojukeji.know.streaming.km.core.service.group.GroupService; +import com.xiaojukeji.know.streaming.km.core.service.group.OpGroupService; import com.xiaojukeji.know.streaming.km.core.service.partition.PartitionService; import com.xiaojukeji.know.streaming.km.core.service.topic.TopicService; import com.xiaojukeji.know.streaming.km.core.service.version.metrics.kafka.GroupMetricVersionItems; @@ -58,7 +64,7 @@ @Component public class GroupManagerImpl implements GroupManager { - private static final ILog log = LogFactory.getLog(GroupManagerImpl.class); + private static final ILog LOGGER = LogFactory.getLog(GroupManagerImpl.class); @Autowired private TopicService topicService; @@ -66,6 +72,9 @@ public class GroupManagerImpl implements GroupManager { @Autowired private GroupService groupService; + @Autowired + private OpGroupService opGroupService; + @Autowired private PartitionService partitionService; @@ -246,6 +255,52 @@ public Result resetGroupOffsets(GroupOffsetResetDTO dto, String operator) return groupService.resetGroupOffsets(dto.getClusterId(), dto.getGroupName(), offsetMapResult.getData(), operator); } + @Override + public Result deleteGroupOffsets(GroupOffsetDeleteDTO dto, String operator) throws Exception { + ClusterPhy clusterPhy = clusterPhyService.getClusterByCluster(dto.getClusterPhyId()); + if (clusterPhy == null) { + return Result.buildFromRSAndMsg(ResultStatus.CLUSTER_NOT_EXIST, MsgConstant.getClusterPhyNotExist(dto.getClusterPhyId())); + } + + + // 按照group纬度进行删除 + if (ValidateUtils.isBlank(dto.getGroupName())) { + return Result.buildFromRSAndMsg(ResultStatus.PARAM_ILLEGAL, "groupName不允许为空"); + } + if (DeleteGroupTypeEnum.GROUP.getCode().equals(dto.getDeleteType())) { + return opGroupService.deleteGroupOffset( + new DeleteGroupParam(dto.getClusterPhyId(), dto.getGroupName(), DeleteGroupTypeEnum.GROUP), + operator + ); + } + + + // 按照topic纬度进行删除 + if (ValidateUtils.isBlank(dto.getTopicName())) { + return Result.buildFromRSAndMsg(ResultStatus.PARAM_ILLEGAL, "topicName不允许为空"); + } + if (DeleteGroupTypeEnum.GROUP_TOPIC.getCode().equals(dto.getDeleteType())) { + return opGroupService.deleteGroupOffset( + new DeleteGroupTopicParam(dto.getClusterPhyId(), dto.getGroupName(), DeleteGroupTypeEnum.GROUP, dto.getTopicName()), + operator + ); + } + + + // 按照partition纬度进行删除 + if (ValidateUtils.isNullOrLessThanZero(dto.getPartitionId())) { + return Result.buildFromRSAndMsg(ResultStatus.PARAM_ILLEGAL, "partitionId不允许为空或小于0"); + } + if (DeleteGroupTypeEnum.GROUP_TOPIC_PARTITION.getCode().equals(dto.getDeleteType())) { + return opGroupService.deleteGroupOffset( + new DeleteGroupTopicPartitionParam(dto.getClusterPhyId(), dto.getGroupName(), DeleteGroupTypeEnum.GROUP, dto.getTopicName(), dto.getPartitionId()), + operator + ); + } + + return Result.buildFromRSAndMsg(ResultStatus.PARAM_ILLEGAL, "deleteType类型错误"); + } + @Override public List getGroupTopicOverviewVOList(Long clusterPhyId, List groupMemberPOList) { // 获取指标 @@ -257,7 +312,7 @@ public List getGroupTopicOverviewVOList(Long clusterPhyId, ); if (metricsListResult.failed()) { // 如果查询失败,则输出错误信息,但是依旧进行已有数据的返回 - log.error("method=completeMetricData||clusterPhyId={}||result={}||errMsg=search es failed", clusterPhyId, metricsListResult); + LOGGER.error("method=completeMetricData||clusterPhyId={}||result={}||errMsg=search es failed", clusterPhyId, metricsListResult); } return this.convert2GroupTopicOverviewVOList(groupMemberPOList, metricsListResult.getData()); } diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/dto/group/GroupOffsetDeleteDTO.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/dto/group/GroupOffsetDeleteDTO.java new file mode 100644 index 000000000..03cb61c62 --- /dev/null +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/dto/group/GroupOffsetDeleteDTO.java @@ -0,0 +1,40 @@ +package com.xiaojukeji.know.streaming.km.common.bean.dto.group; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.xiaojukeji.know.streaming.km.common.bean.dto.BaseDTO; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * 删除offset + * @author zengqiao + * @date 19/4/8 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class GroupOffsetDeleteDTO extends BaseDTO { + @Min(value = 0, message = "clusterPhyId不允许为null或者小于0") + @ApiModelProperty(value = "集群ID", example = "6") + private Long clusterPhyId; + + @NotBlank(message = "groupName不允许为空") + @ApiModelProperty(value = "消费组名称", example = "g-know-streaming") + private String groupName; + + @ApiModelProperty(value = "Topic名称,按照Topic纬度进行删除时需要传", example = "know-streaming") + protected String topicName; + + @ApiModelProperty(value = "分区ID,按照分区纬度进行删除时需要传") + private Integer partitionId; + + /** + * @see com.xiaojukeji.know.streaming.km.common.enums.group.DeleteGroupTypeEnum + */ + @NotNull(message = "deleteType不允许为空") + @ApiModelProperty(value = "删除类型", example = "0:group纬度,1:Topic纬度,2:Partition纬度") + private Integer deleteType; +} \ No newline at end of file diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/param/group/DeleteGroupParam.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/param/group/DeleteGroupParam.java new file mode 100644 index 000000000..3c9360a50 --- /dev/null +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/param/group/DeleteGroupParam.java @@ -0,0 +1,16 @@ +package com.xiaojukeji.know.streaming.km.common.bean.entity.param.group; + +import com.xiaojukeji.know.streaming.km.common.enums.group.DeleteGroupTypeEnum; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class DeleteGroupParam extends GroupParam { + protected DeleteGroupTypeEnum deleteGroupTypeEnum; + + public DeleteGroupParam(Long clusterPhyId, String groupName, DeleteGroupTypeEnum deleteGroupTypeEnum) { + super(clusterPhyId, groupName); + this.deleteGroupTypeEnum = deleteGroupTypeEnum; + } +} diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/param/group/DeleteGroupTopicParam.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/param/group/DeleteGroupTopicParam.java new file mode 100644 index 000000000..c72fd97c9 --- /dev/null +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/param/group/DeleteGroupTopicParam.java @@ -0,0 +1,16 @@ +package com.xiaojukeji.know.streaming.km.common.bean.entity.param.group; + +import com.xiaojukeji.know.streaming.km.common.enums.group.DeleteGroupTypeEnum; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class DeleteGroupTopicParam extends DeleteGroupParam { + protected String topicName; + + public DeleteGroupTopicParam(Long clusterPhyId, String groupName, DeleteGroupTypeEnum deleteGroupTypeEnum, String topicName) { + super(clusterPhyId, groupName, deleteGroupTypeEnum); + this.topicName = topicName; + } +} diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/param/group/DeleteGroupTopicPartitionParam.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/param/group/DeleteGroupTopicPartitionParam.java new file mode 100644 index 000000000..e2f049cb5 --- /dev/null +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/param/group/DeleteGroupTopicPartitionParam.java @@ -0,0 +1,16 @@ +package com.xiaojukeji.know.streaming.km.common.bean.entity.param.group; + +import com.xiaojukeji.know.streaming.km.common.enums.group.DeleteGroupTypeEnum; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class DeleteGroupTopicPartitionParam extends DeleteGroupTopicParam { + protected Integer partitionId; + + public DeleteGroupTopicPartitionParam(Long clusterPhyId, String groupName, DeleteGroupTypeEnum deleteGroupTypeEnum, String topicName, Integer partitionId) { + super(clusterPhyId, groupName, deleteGroupTypeEnum, topicName); + this.partitionId = partitionId; + } +} diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/param/group/GroupParam.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/param/group/GroupParam.java index d7bf15f84..4f7552d93 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/param/group/GroupParam.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/param/group/GroupParam.java @@ -1,13 +1,11 @@ package com.xiaojukeji.know.streaming.km.common.bean.entity.param.group; import com.xiaojukeji.know.streaming.km.common.bean.entity.param.cluster.ClusterPhyParam; -import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor -@AllArgsConstructor public class GroupParam extends ClusterPhyParam { protected String groupName; diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/enums/group/DeleteGroupTypeEnum.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/enums/group/DeleteGroupTypeEnum.java new file mode 100644 index 000000000..ef99344cb --- /dev/null +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/enums/group/DeleteGroupTypeEnum.java @@ -0,0 +1,28 @@ +package com.xiaojukeji.know.streaming.km.common.enums.group; + +import lombok.Getter; + + +/** + * @author wyb + * @date 2022/10/11 + */ +@Getter +public enum DeleteGroupTypeEnum { + UNKNOWN(-1, "Unknown"), + + GROUP(0, "Group纬度"), + + GROUP_TOPIC(1, "GroupTopic纬度"), + + GROUP_TOPIC_PARTITION(2, "GroupTopicPartition纬度"); + + private final Integer code; + + private final String msg; + + DeleteGroupTypeEnum(Integer code, String msg) { + this.code = code; + this.msg = msg; + } +} diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/enums/version/VersionItemTypeEnum.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/enums/version/VersionItemTypeEnum.java index 7bcf3234b..d11c3bfa2 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/enums/version/VersionItemTypeEnum.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/enums/version/VersionItemTypeEnum.java @@ -41,6 +41,8 @@ public enum VersionItemTypeEnum { SERVICE_OP_REASSIGNMENT(330, "service_reassign_operation"), + SERVICE_OP_GROUP(340, "service_group_operation"), + SERVICE_OP_CONNECT_CLUSTER(400, "service_connect_cluster_operation"), SERVICE_OP_CONNECT_CONNECTOR(401, "service_connect_connector_operation"), SERVICE_OP_CONNECT_PLUGIN(402, "service_connect_plugin_operation"), diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/OpGroupService.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/OpGroupService.java new file mode 100644 index 000000000..83285a488 --- /dev/null +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/OpGroupService.java @@ -0,0 +1,11 @@ +package com.xiaojukeji.know.streaming.km.core.service.group; + +import com.xiaojukeji.know.streaming.km.common.bean.entity.param.group.DeleteGroupParam; +import com.xiaojukeji.know.streaming.km.common.bean.entity.result.Result; + +public interface OpGroupService { + /** + * 删除Topic + */ + Result deleteGroupOffset(DeleteGroupParam param, String operator); +} diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/impl/OpGroupServiceImpl.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/impl/OpGroupServiceImpl.java new file mode 100644 index 000000000..b59519b94 --- /dev/null +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/impl/OpGroupServiceImpl.java @@ -0,0 +1,255 @@ +package com.xiaojukeji.know.streaming.km.core.service.group.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.didiglobal.logi.log.ILog; +import com.didiglobal.logi.log.LogFactory; +import com.didiglobal.logi.security.common.dto.oplog.OplogDTO; +import com.xiaojukeji.know.streaming.km.common.bean.entity.param.VersionItemParam; +import com.xiaojukeji.know.streaming.km.common.bean.entity.param.group.DeleteGroupParam; +import com.xiaojukeji.know.streaming.km.common.bean.entity.param.group.DeleteGroupTopicParam; +import com.xiaojukeji.know.streaming.km.common.bean.entity.param.group.DeleteGroupTopicPartitionParam; +import com.xiaojukeji.know.streaming.km.common.bean.entity.result.Result; +import com.xiaojukeji.know.streaming.km.common.bean.entity.result.ResultStatus; +import com.xiaojukeji.know.streaming.km.common.bean.po.group.GroupMemberPO; +import com.xiaojukeji.know.streaming.km.common.bean.po.group.GroupPO; +import com.xiaojukeji.know.streaming.km.common.constant.KafkaConstant; +import com.xiaojukeji.know.streaming.km.common.enums.group.DeleteGroupTypeEnum; +import com.xiaojukeji.know.streaming.km.common.enums.operaterecord.ModuleEnum; +import com.xiaojukeji.know.streaming.km.common.enums.operaterecord.OperationEnum; +import com.xiaojukeji.know.streaming.km.common.enums.version.VersionItemTypeEnum; +import com.xiaojukeji.know.streaming.km.common.exception.VCHandlerNotExistException; +import com.xiaojukeji.know.streaming.km.common.utils.ConvertUtil; +import com.xiaojukeji.know.streaming.km.core.service.group.OpGroupService; +import com.xiaojukeji.know.streaming.km.core.service.oprecord.OpLogWrapService; +import com.xiaojukeji.know.streaming.km.core.service.version.BaseKafkaVersionControlService; +import com.xiaojukeji.know.streaming.km.persistence.kafka.KafkaAdminClient; +import com.xiaojukeji.know.streaming.km.persistence.mysql.group.GroupDAO; +import com.xiaojukeji.know.streaming.km.persistence.mysql.group.GroupMemberDAO; +import org.apache.kafka.clients.admin.*; +import org.apache.kafka.common.TopicPartition; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.util.*; +import java.util.stream.Collectors; + +import static com.xiaojukeji.know.streaming.km.common.bean.entity.result.ResultStatus.VC_HANDLE_NOT_EXIST; +import static com.xiaojukeji.know.streaming.km.common.enums.version.VersionEnum.*; +import static com.xiaojukeji.know.streaming.km.common.enums.version.VersionItemTypeEnum.SERVICE_OP_GROUP; + +/** + * @author didi + */ +@Service +public class OpGroupServiceImpl extends BaseKafkaVersionControlService implements OpGroupService { + private static final ILog LOGGER = LogFactory.getLog(OpGroupServiceImpl.class); + + private static final String DELETE_GROUP_OFFSET = "deleteGroupOffset"; + + @Autowired + private GroupDAO groupDAO; + + @Autowired + private GroupMemberDAO groupMemberDAO; + + @Autowired + private OpLogWrapService opLogWrapService; + + @Autowired + private KafkaAdminClient kafkaAdminClient; + + @Override + protected VersionItemTypeEnum getVersionItemType() { + return SERVICE_OP_GROUP; + } + + @PostConstruct + private void init() { + registerVCHandler(DELETE_GROUP_OFFSET, V_1_1_0, V_MAX, "deleteGroupOffset", this::deleteGroupOffsetByClient); + } + + @Override + public Result deleteGroupOffset(DeleteGroupParam param, String operator) { + // 日志记录 + LOGGER.info("method=deleteGroupOffset||param={}||operator={}||msg=delete group offset", ConvertUtil.obj2Json(param), operator); + + try { + Result rv = (Result) doVCHandler(param.getClusterPhyId(), DELETE_GROUP_OFFSET, param); + if (rv == null || rv.failed()) { + return rv; + } + + // 清理数据库中的数据 + if (DeleteGroupTypeEnum.GROUP.equals(param.getDeleteGroupTypeEnum())) { + // 记录操作 + OplogDTO oplogDTO = new OplogDTO(operator, + OperationEnum.DELETE.getDesc(), + ModuleEnum.KAFKA_GROUP.getDesc(), + String.format("集群ID:[%d] Group名称:[%s]", param.getClusterPhyId(), param.getGroupName()), + String.format("删除Offset:[%s]", ConvertUtil.obj2Json(param)) + ); + opLogWrapService.saveOplogAndIgnoreException(oplogDTO); + + // 清理Group数据 + this.deleteGroupInDB(param.getClusterPhyId(), param.getGroupName()); + this.deleteGroupMemberInDB(param.getClusterPhyId(), param.getGroupName()); + } else if (DeleteGroupTypeEnum.GROUP_TOPIC.equals(param.getDeleteGroupTypeEnum())) { + // 记录操作 + DeleteGroupTopicParam topicParam = (DeleteGroupTopicParam) param; + OplogDTO oplogDTO = new OplogDTO(operator, + OperationEnum.DELETE.getDesc(), + ModuleEnum.KAFKA_GROUP.getDesc(), + String.format("集群ID:[%d] Group名称:[%s] Topic名称:[%s]", param.getClusterPhyId(), param.getGroupName(), topicParam.getTopicName()), + String.format("删除Offset:[%s]", ConvertUtil.obj2Json(topicParam)) + ); + opLogWrapService.saveOplogAndIgnoreException(oplogDTO); + + // 清理group + topic 数据 + this.deleteGroupMemberInDB(topicParam.getClusterPhyId(), topicParam.getGroupName(), topicParam.getTopicName()); + } else if (DeleteGroupTypeEnum.GROUP_TOPIC_PARTITION.equals(param.getDeleteGroupTypeEnum())) { + // 记录操作 + DeleteGroupTopicPartitionParam partitionParam = (DeleteGroupTopicPartitionParam) param; + OplogDTO oplogDTO = new OplogDTO(operator, + OperationEnum.DELETE.getDesc(), + ModuleEnum.KAFKA_GROUP.getDesc(), + String.format("集群ID:[%d] Group名称:[%s] Topic名称:[%s] PartitionID:[%d]", param.getClusterPhyId(), param.getGroupName(), partitionParam.getTopicName(), partitionParam.getPartitionId()), + String.format("删除Offset:[%s]", ConvertUtil.obj2Json(partitionParam)) + ); + opLogWrapService.saveOplogAndIgnoreException(oplogDTO); + + // 不需要进行清理 + } + + return rv; + } catch (VCHandlerNotExistException e) { + return Result.buildFailure(VC_HANDLE_NOT_EXIST); + } + } + + /**************************************************** private method ****************************************************/ + + private Result deleteGroupOffsetByClient(VersionItemParam itemParam) { + DeleteGroupParam deleteGroupParam = (DeleteGroupParam) itemParam; + + if (DeleteGroupTypeEnum.GROUP.equals(deleteGroupParam.getDeleteGroupTypeEnum())) { + return this.deleteGroupByClient(itemParam); + } else if (DeleteGroupTypeEnum.GROUP_TOPIC.equals(deleteGroupParam.getDeleteGroupTypeEnum())) { + return this.deleteGroupTopicOffsetByClient(itemParam); + } else if (DeleteGroupTypeEnum.GROUP_TOPIC_PARTITION.equals(deleteGroupParam.getDeleteGroupTypeEnum())) { + return this.deleteGroupTopicPartitionOffsetByClient(itemParam); + } + + return Result.buildFromRSAndMsg(ResultStatus.PARAM_ILLEGAL, "删除Offset时,删除的类型参数非法"); + } + + private Result deleteGroupByClient(VersionItemParam itemParam) { + DeleteGroupParam param = (DeleteGroupParam) itemParam; + try { + AdminClient adminClient = kafkaAdminClient.getClient(param.getClusterPhyId()); + + DeleteConsumerGroupsResult deleteConsumerGroupsResult = adminClient.deleteConsumerGroups( + Collections.singletonList(param.getGroupName()), + new DeleteConsumerGroupsOptions().timeoutMs(KafkaConstant.ADMIN_CLIENT_REQUEST_TIME_OUT_UNIT_MS) + ); + + deleteConsumerGroupsResult.all().get(); + } catch (Exception e) { + LOGGER.error( + "method=deleteGroupByClient||clusterPhyId={}||groupName={}||errMsg=delete group failed||msg=exception!", + param.getClusterPhyId(), param.getGroupName(), e + ); + + return Result.buildFromRSAndMsg(ResultStatus.KAFKA_OPERATE_FAILED, e.getMessage()); + } + + return Result.buildSuc(); + } + + private Result deleteGroupTopicOffsetByClient(VersionItemParam itemParam) { + DeleteGroupTopicParam param = (DeleteGroupTopicParam) itemParam; + try { + AdminClient adminClient = kafkaAdminClient.getClient(param.getClusterPhyId()); + + DescribeTopicsResult describeTopicsResult = adminClient.describeTopics(Collections.singletonList( + param.getTopicName()), + new DescribeTopicsOptions().timeoutMs(KafkaConstant.ADMIN_CLIENT_REQUEST_TIME_OUT_UNIT_MS) + ); + + List tpList = describeTopicsResult + .all() + .get() + .get(param.getTopicName()) + .partitions() + .stream() + .map(elem -> new TopicPartition(param.getTopicName(), elem.partition())) + .collect(Collectors.toList()); + + DeleteConsumerGroupOffsetsResult deleteConsumerGroupOffsetsResult = adminClient.deleteConsumerGroupOffsets( + param.getGroupName(), + new HashSet<>(tpList), + new DeleteConsumerGroupOffsetsOptions().timeoutMs(KafkaConstant.ADMIN_CLIENT_REQUEST_TIME_OUT_UNIT_MS) + ); + + deleteConsumerGroupOffsetsResult.all().get(); + } catch (Exception e) { + LOGGER.error( + "method=deleteGroupTopicOffsetByClient||clusterPhyId={}||groupName={}||topicName={}||errMsg=delete group failed||msg=exception!", + param.getClusterPhyId(), param.getGroupName(), param.getTopicName(), e + ); + + return Result.buildFromRSAndMsg(ResultStatus.KAFKA_OPERATE_FAILED, e.getMessage()); + } + + return Result.buildSuc(); + } + + private Result deleteGroupTopicPartitionOffsetByClient(VersionItemParam itemParam) { + DeleteGroupTopicPartitionParam param = (DeleteGroupTopicPartitionParam) itemParam; + try { + AdminClient adminClient = kafkaAdminClient.getClient(param.getClusterPhyId()); + + DeleteConsumerGroupOffsetsResult deleteConsumerGroupOffsetsResult = adminClient.deleteConsumerGroupOffsets( + param.getGroupName(), + new HashSet<>(Arrays.asList(new TopicPartition(param.getTopicName(), param.getPartitionId()))), + new DeleteConsumerGroupOffsetsOptions().timeoutMs(KafkaConstant.ADMIN_CLIENT_REQUEST_TIME_OUT_UNIT_MS) + ); + + deleteConsumerGroupOffsetsResult.all().get(); + } catch (Exception e) { + LOGGER.error( + "method=deleteGroupTopicPartitionOffsetByClient||clusterPhyId={}||groupName={}||topicName={}||partitionId={}||errMsg=delete group failed||msg=exception!", + param.getClusterPhyId(), param.getGroupName(), param.getTopicName(), param.getPartitionId(), e + ); + + return Result.buildFromRSAndMsg(ResultStatus.KAFKA_OPERATE_FAILED, e.getMessage()); + } + + return Result.buildSuc(); + } + + private int deleteGroupInDB(Long clusterPhyId, String groupName) { + LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); + lambdaQueryWrapper.eq(GroupPO::getClusterPhyId, clusterPhyId); + lambdaQueryWrapper.eq(GroupPO::getName, groupName); + + return groupDAO.delete(lambdaQueryWrapper); + } + + private int deleteGroupMemberInDB(Long clusterPhyId, String groupName) { + LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); + lambdaQueryWrapper.eq(GroupMemberPO::getClusterPhyId, clusterPhyId); + lambdaQueryWrapper.eq(GroupMemberPO::getGroupName, groupName); + + return groupMemberDAO.delete(lambdaQueryWrapper); + } + + private int deleteGroupMemberInDB(Long clusterPhyId, String groupName, String topicName) { + LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); + lambdaQueryWrapper.eq(GroupMemberPO::getClusterPhyId, clusterPhyId); + lambdaQueryWrapper.eq(GroupMemberPO::getGroupName, groupName); + lambdaQueryWrapper.eq(GroupMemberPO::getTopicName, topicName); + + return groupMemberDAO.delete(lambdaQueryWrapper); + } +} diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/version/fe/FrontEndControlVersionItems.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/version/fe/FrontEndControlVersionItems.java index db8baa4aa..a56b9b0e7 100644 --- a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/version/fe/FrontEndControlVersionItems.java +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/version/fe/FrontEndControlVersionItems.java @@ -38,6 +38,8 @@ public class FrontEndControlVersionItems extends BaseMetricVersionMetric { private static final String FE_TRUNCATE_TOPIC = "FETruncateTopic"; + private static final String FE_DELETE_GROUP_OFFSET = "FEDeleteGroupOffset"; + public FrontEndControlVersionItems(){} @Override @@ -91,10 +93,13 @@ public List init(){ itemList.add(buildItem().minVersion(VersionEnum.V_2_5_0_D_300).maxVersion(VersionEnum.V_2_5_0_D_MAX) .name(FE_HA_DELETE_MIRROR_TOPIC).desc("HA-取消Topic复制")); - //truncate topic + // truncate topic itemList.add(buildItem().minVersion(VersionEnum.V_0_11_0_0).maxVersion(VersionEnum.V_MAX) - .name(FE_TRUNCATE_TOPIC).desc("清空topic")); + .name(FE_TRUNCATE_TOPIC).desc("清空Topic")); + // truncate topic + itemList.add(buildItem().minVersion(VersionEnum.V_1_1_0).maxVersion(VersionEnum.V_MAX) + .name(FE_DELETE_GROUP_OFFSET).desc("删除GroupOffset")); return itemList; } } diff --git a/km-rest/src/main/java/com/xiaojukeji/know/streaming/km/rest/api/v3/group/GroupController.java b/km-rest/src/main/java/com/xiaojukeji/know/streaming/km/rest/api/v3/group/GroupController.java index 55e7e778c..9233be887 100644 --- a/km-rest/src/main/java/com/xiaojukeji/know/streaming/km/rest/api/v3/group/GroupController.java +++ b/km-rest/src/main/java/com/xiaojukeji/know/streaming/km/rest/api/v3/group/GroupController.java @@ -2,6 +2,7 @@ import com.didiglobal.logi.security.util.HttpRequestUtil; import com.xiaojukeji.know.streaming.km.biz.group.GroupManager; +import com.xiaojukeji.know.streaming.km.common.bean.dto.group.GroupOffsetDeleteDTO; import com.xiaojukeji.know.streaming.km.common.bean.dto.group.GroupOffsetResetDTO; import com.xiaojukeji.know.streaming.km.common.bean.dto.group.GroupTopicConsumedDTO; import com.xiaojukeji.know.streaming.km.common.bean.entity.result.PaginationResult; @@ -32,13 +33,20 @@ public class GroupController { @Autowired private GroupService groupService; - @ApiOperation(value = "重置组消费偏移", notes = "") + @ApiOperation(value = "重置消费偏移", notes = "") @PutMapping(value = "group-offsets") @ResponseBody public Result resetGroupOffsets(@Validated @RequestBody GroupOffsetResetDTO dto) throws Exception { return groupManager.resetGroupOffsets(dto, HttpRequestUtil.getOperator()); } + @ApiOperation(value = "删除消费偏移", notes = "") + @DeleteMapping(value = "group-offsets") + @ResponseBody + public Result deleteGroupOffsets(@Validated @RequestBody GroupOffsetDeleteDTO dto) throws Exception { + return groupManager.deleteGroupOffsets(dto, HttpRequestUtil.getOperator()); + } + @ApiOperation(value = "Group-Topic指标信息", notes = "") @PostMapping(value = "clusters/{clusterId}/topics/{topicName}/groups/{groupName}/metric") @ResponseBody From b3b7ab9f6b1bdce330f64e048964708b49f4e1be Mon Sep 17 00:00:00 2001 From: ZQKC Date: Tue, 27 Jun 2023 14:48:07 +0800 Subject: [PATCH 23/37] =?UTF-8?q?[Doc]=E5=8E=BB=E9=99=A4commit=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E4=B8=AD=E5=B8=A6issue-id=E7=9A=84=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1、使用squash merge后,不需要在写commit-log的时候,带上issue-id的信息; 2、主页的id调整为pr的id; --- ...\264\241\347\214\256\346\214\207\345\215\227.md" | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git "a/docs/contribute_guide/\350\264\241\347\214\256\346\214\207\345\215\227.md" "b/docs/contribute_guide/\350\264\241\347\214\256\346\214\207\345\215\227.md" index 37cf89bc1..8ea7c5516 100644 --- "a/docs/contribute_guide/\350\264\241\347\214\256\346\214\207\345\215\227.md" +++ "b/docs/contribute_guide/\350\264\241\347\214\256\346\214\207\345\215\227.md" @@ -47,14 +47,13 @@ **1、`Header` 规范** -`Header` 格式为 `[Type]Message(#IssueID)`, 主要有三部分组成,分别是`Type`、`Message`、`IssueID`, +`Header` 格式为 `[Type]Message`, 主要有三部分组成,分别是`Type`、`Message`, - `Type`:说明这个提交是哪一个类型的,比如有 Bugfix、Feature、Optimize等; - `Message`:说明提交的信息,比如修复xx问题; -- `IssueID`:该提交,关联的Issue的编号; -实际例子:[`[Bugfix]修复新接入的集群,Controller-Host不显示的问题(#927)`](https://github.com/didi/KnowStreaming/pull/933/commits) +实际例子:[`[Bugfix]修复新接入的集群,Controller-Host不显示的问题`](https://github.com/didi/KnowStreaming/pull/933/commits) @@ -67,7 +66,7 @@ **3、实际例子** ``` -[Optimize]优化 MySQL & ES 测试容器的初始化(#906) +[Optimize]优化 MySQL & ES 测试容器的初始化 主要的变更 1、knowstreaming/knowstreaming-manager 容器; @@ -138,7 +137,7 @@ 1. 切换到主分支:`git checkout github_master`; 2. 主分支拉最新代码:`git pull`; 3. 基于主分支拉新分支:`git checkout -b fix_928`; -4. 提交代码,安装commit的规范进行提交,例如:`git commit -m "[Optimize]优化xxx问题(#928)"`; +4. 提交代码,安装commit的规范进行提交,例如:`git commit -m "[Optimize]优化xxx问题"`; 5. 提交到自己远端仓库:`git push --set-upstream origin fix_928`; 6. `GitHub` 页面发起 `Pull Request` 请求,管理员合入主仓库。这部分详细见下一节; @@ -162,6 +161,8 @@ ### 4.1、如何将多个 Commit-Log 合并为一个? -可以使用 `git rebase -i` 命令进行解决。 +可以不需要将多个commit合并为一个,如果要合并,可以使用 `git rebase -i` 命令进行解决。 + + From 0140b2e898d680b220efd7caf677baabbe161683 Mon Sep 17 00:00:00 2001 From: EricZeng Date: Tue, 27 Jun 2023 16:46:47 +0800 Subject: [PATCH 24/37] =?UTF-8?q?[Optimize]Connector=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E9=87=8D=E5=90=AF=E3=80=81=E7=BC=96=E8=BE=91=E3=80=81=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E7=AD=89=E6=9D=83=E9=99=90=E7=82=B9=20(#1066)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...15\207\347\272\247\346\211\213\345\206\214.md" | 15 +++++++++++++++ .../src/main/resources/sql/dml-logi.sql | 14 ++++++++++++++ 2 files changed, 29 insertions(+) diff --git "a/docs/install_guide/\347\211\210\346\234\254\345\215\207\347\272\247\346\211\213\345\206\214.md" "b/docs/install_guide/\347\211\210\346\234\254\345\215\207\347\272\247\346\211\213\345\206\214.md" index 061c080d0..d11126975 100644 --- "a/docs/install_guide/\347\211\210\346\234\254\345\215\207\347\272\247\346\211\213\345\206\214.md" +++ "b/docs/install_guide/\347\211\210\346\234\254\345\215\207\347\272\247\346\211\213\345\206\214.md" @@ -6,6 +6,21 @@ ### 升级至 `master` 版本 +**SQL 变更** +```sql +-- 多集群管理权限2023-06-27新增 +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2026', 'Connector-新增', '1593', '1', '2', 'Connector-新增', '0', 'know-streaming'); +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2028', 'Connector-编辑', '1593', '1', '2', 'Connector-编辑', '0', 'know-streaming'); +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2030', 'Connector-删除', '1593', '1', '2', 'Connector-删除', '0', 'know-streaming'); +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2032', 'Connector-重启', '1593', '1', '2', 'Connector-重启', '0', 'know-streaming'); +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2034', 'Connector-暂停&恢复', '1593', '1', '2', 'Connector-暂停&恢复', '0', 'know-streaming'); + +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2026', '0', 'know-streaming'); +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2028', '0', 'know-streaming'); +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2030', '0', 'know-streaming'); +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2032', '0', 'know-streaming'); +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2034', '0', 'know-streaming'); +``` ### 升级至 `3.3.0` 版本 diff --git a/km-persistence/src/main/resources/sql/dml-logi.sql b/km-persistence/src/main/resources/sql/dml-logi.sql index 0ad09b4bf..a288b8d13 100644 --- a/km-persistence/src/main/resources/sql/dml-logi.sql +++ b/km-persistence/src/main/resources/sql/dml-logi.sql @@ -119,3 +119,17 @@ INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_del INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2020', '0', 'know-streaming'); INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2022', '0', 'know-streaming'); INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2024', '0', 'know-streaming'); + + +-- 多集群管理权限2023-06-27新增 +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2026', 'Connector-新增', '1593', '1', '2', 'Connector-新增', '0', 'know-streaming'); +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2028', 'Connector-编辑', '1593', '1', '2', 'Connector-编辑', '0', 'know-streaming'); +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2030', 'Connector-删除', '1593', '1', '2', 'Connector-删除', '0', 'know-streaming'); +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2032', 'Connector-重启', '1593', '1', '2', 'Connector-重启', '0', 'know-streaming'); +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2034', 'Connector-暂停&恢复', '1593', '1', '2', 'Connector-暂停&恢复', '0', 'know-streaming'); + +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2026', '0', 'know-streaming'); +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2028', '0', 'know-streaming'); +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2030', '0', 'know-streaming'); +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2032', '0', 'know-streaming'); +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2034', '0', 'know-streaming'); \ No newline at end of file From e98cfbcf912af77dc61620302ba43dada51e9e7b Mon Sep 17 00:00:00 2001 From: EricZeng Date: Wed, 28 Jun 2023 15:59:13 +0800 Subject: [PATCH 25/37] =?UTF-8?q?[Bugfix]=E4=BF=AE=E5=A4=8DConnect-Worker?= =?UTF-8?q?=20Jmx=E4=B8=8D=E7=94=9F=E6=95=88=E7=9A=84=E9=97=AE=E9=A2=98=20?= =?UTF-8?q?(#1067)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1、FAQ中补充JMX连接失败的说明; 2、修复Connect-Worker Jmx不生效的问题; --- docs/user_guide/faq.md | 58 ++++++++++++++----- .../worker/impl/WorkerServiceImpl.java | 7 ++- .../SyncConnectClusterAndWorkerTask.java | 5 +- 3 files changed, 52 insertions(+), 18 deletions(-) diff --git a/docs/user_guide/faq.md b/docs/user_guide/faq.md index 1656ec371..6af0f94e8 100644 --- a/docs/user_guide/faq.md +++ b/docs/user_guide/faq.md @@ -1,13 +1,35 @@ + +![Logo](https://user-images.githubusercontent.com/71620349/185368586-aed82d30-1534-453d-86ff-ecfa9d0f35bd.png) + # FAQ -## 8.1、支持哪些 Kafka 版本? +- [FAQ](#faq) + - [1、支持哪些 Kafka 版本?](#1支持哪些-kafka-版本) + - [1、2.x 版本和 3.0 版本有什么差异?](#12x-版本和-30-版本有什么差异) + - [3、页面流量信息等无数据?](#3页面流量信息等无数据) + - [8.4、`Jmx`连接失败如何解决?](#84jmx连接失败如何解决) + - [5、有没有 API 文档?](#5有没有-api-文档) + - [6、删除 Topic 成功后,为何过段时间又出现了?](#6删除-topic-成功后为何过段时间又出现了) + - [7、如何在不登录的情况下,调用接口?](#7如何在不登录的情况下调用接口) + - [8、Specified key was too long; max key length is 767 bytes](#8specified-key-was-too-long-max-key-length-is-767-bytes) + - [9、出现 ESIndexNotFoundEXception 报错](#9出现-esindexnotfoundexception-报错) + - [10、km-console 打包构建失败](#10km-console-打包构建失败) + - [11、在 `km-console` 目录下执行 `npm run start` 时看不到应用构建和热加载过程?如何启动单个应用?](#11在-km-console-目录下执行-npm-run-start-时看不到应用构建和热加载过程如何启动单个应用) + - [12、权限识别失败问题](#12权限识别失败问题) + - [13、接入开启kerberos认证的kafka集群](#13接入开启kerberos认证的kafka集群) + - [14、对接Ldap的配置](#14对接ldap的配置) + - [15、测试时使用Testcontainers的说明](#15测试时使用testcontainers的说明) + - [16、JMX连接失败怎么办](#16jmx连接失败怎么办) + + +## 1、支持哪些 Kafka 版本? - 支持 0.10+ 的 Kafka 版本; - 支持 ZK 及 Raft 运行模式的 Kafka 版本;   -## 8.1、2.x 版本和 3.0 版本有什么差异? +## 1、2.x 版本和 3.0 版本有什么差异? **全新设计理念** @@ -23,7 +45,7 @@   -## 8.3、页面流量信息等无数据? +## 3、页面流量信息等无数据? - 1、`Broker JMX`未正确开启 @@ -41,7 +63,7 @@   -## 8.5、有没有 API 文档? +## 5、有没有 API 文档? `KnowStreaming` 采用 Swagger 进行 API 说明,在启动 KnowStreaming 服务之后,就可以从下面地址看到。 @@ -49,7 +71,7 @@ Swagger-API 地址: [http://IP:PORT/swagger-ui.html#/](http://IP:PORT/swagger-   -## 8.6、删除 Topic 成功后,为何过段时间又出现了? +## 6、删除 Topic 成功后,为何过段时间又出现了? **原因说明:** @@ -74,7 +96,7 @@ for (int i= 0; i < 100000; ++i) {   -## 8.7、如何在不登录的情况下,调用接口? +## 7、如何在不登录的情况下,调用接口? 步骤一:接口调用时,在 header 中,增加如下信息: @@ -109,7 +131,7 @@ SECURITY.TRICK_USERS 但是还有一点需要注意,绕过的用户仅能调用他有权限的接口,比如一个普通用户,那么他就只能调用普通的接口,不能去调用运维人员的接口。 -## 8.8、Specified key was too long; max key length is 767 bytes +## 8、Specified key was too long; max key length is 767 bytes **原因:** 不同版本的 InoDB 引擎,参数‘innodb_large_prefix’默认值不同,即在 5.6 默认值为 OFF,5.7 默认值为 ON。 @@ -121,13 +143,13 @@ SECURITY.TRICK_USERS - 将字符集改为 latin1(一个字符=一个字节)。 - 开启‘innodb_large_prefix’,修改默认行格式‘innodb_file_format’为 Barracuda,并设置 row_format=dynamic。 -## 8.9、出现 ESIndexNotFoundEXception 报错 +## 9、出现 ESIndexNotFoundEXception 报错 **原因 :**没有创建 ES 索引模版 **解决方案:**执行 init_es_template.sh 脚本,创建 ES 索引模版即可。 -## 8.10、km-console 打包构建失败 +## 10、km-console 打包构建失败 首先,**请确保您正在使用最新版本**,版本列表见 [Tags](https://github.com/didi/KnowStreaming/tags)。如果不是最新版本,请升级后再尝试有无问题。 @@ -161,14 +183,14 @@ Node 版本: v12.22.12 错误截图: ``` -## 8.11、在 `km-console` 目录下执行 `npm run start` 时看不到应用构建和热加载过程?如何启动单个应用? +## 11、在 `km-console` 目录下执行 `npm run start` 时看不到应用构建和热加载过程?如何启动单个应用? 需要到具体的应用中执行 `npm run start`,例如 `cd packages/layout-clusters-fe` 后,执行 `npm run start`。 应用启动后需要到基座应用中查看(需要启动基座应用,即 layout-clusters-fe)。 -## 8.12、权限识别失败问题 +## 12、权限识别失败问题 1、使用admin账号登陆KnowStreaming时,点击系统管理-用户管理-角色管理-新增角色,查看页面是否正常。 @@ -184,7 +206,7 @@ Node 版本: v12.22.12 + 解决方案:清空数据库数据,将数据库字符集调整为utf8,最后重新执行[dml-logi.sql](https://github.com/didi/KnowStreaming/blob/master/km-dist/init/sql/dml-logi.sql)脚本导入数据即可。 -## 8.13、接入开启kerberos认证的kafka集群 +## 13、接入开启kerberos认证的kafka集群 1. 部署KnowStreaming的机器上安装krb客户端; 2. 替换/etc/krb5.conf配置文件; @@ -200,7 +222,7 @@ Node 版本: v12.22.12 ``` -## 8.14、对接Ldap的配置 +## 14、对接Ldap的配置 ```yaml # 需要在application.yml中增加如下配置。相关配置的信息,按实际情况进行调整 @@ -223,6 +245,12 @@ spring: login-extend-bean-name: ksLdapLoginService # 表示使用ldap的service ``` -## 8.15、测试时使用Testcontainers的说明 +## 15、测试时使用Testcontainers的说明 + 1. 需要docker运行环境 [Testcontainers运行环境说明](https://www.testcontainers.org/supported_docker_environment/) -2. 如果本机没有docker,可以使用[远程访问docker](https://docs.docker.com/config/daemon/remote-access/) [Testcontainers配置说明](https://www.testcontainers.org/features/configuration/#customizing-docker-host-detection) \ No newline at end of file +2. 如果本机没有docker,可以使用[远程访问docker](https://docs.docker.com/config/daemon/remote-access/) [Testcontainers配置说明](https://www.testcontainers.org/features/configuration/#customizing-docker-host-detection) + + +## 16、JMX连接失败怎么办 + +详细见:[解决连接JMX失败](../dev_guide/%E8%A7%A3%E5%86%B3%E8%BF%9E%E6%8E%A5JMX%E5%A4%B1%E8%B4%A5.md) \ No newline at end of file diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/connect/worker/impl/WorkerServiceImpl.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/connect/worker/impl/WorkerServiceImpl.java index c52998f18..bb5dacc6a 100644 --- a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/connect/worker/impl/WorkerServiceImpl.java +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/connect/worker/impl/WorkerServiceImpl.java @@ -7,8 +7,8 @@ import com.xiaojukeji.know.streaming.km.common.bean.entity.connect.ConnectWorker; import com.xiaojukeji.know.streaming.km.common.bean.entity.result.PaginationResult; import com.xiaojukeji.know.streaming.km.common.bean.po.connect.ConnectWorkerPO; -import com.xiaojukeji.know.streaming.km.common.bean.po.group.GroupMemberPO; import com.xiaojukeji.know.streaming.km.common.bean.vo.cluster.connector.ClusterWorkerOverviewVO; +import com.xiaojukeji.know.streaming.km.common.enums.jmx.JmxEnum; import com.xiaojukeji.know.streaming.km.common.utils.ConvertUtil; import com.xiaojukeji.know.streaming.km.common.utils.ValidateUtils; import com.xiaojukeji.know.streaming.km.core.service.connect.cluster.ConnectClusterService; @@ -50,6 +50,11 @@ public void batchReplaceInDB(Long connectClusterId, List workerLi connectWorkerDAO.insert(newPO); } else { newPO.setId(oldPO.getId()); + if (JmxEnum.UNKNOWN.getPort().equals(newPO.getJmxPort())) { + // 如果所获取的jmx端口未知,则不更新jmx端口 + newPO.setJmxPort(oldPO.getJmxPort()); + } + connectWorkerDAO.updateById(newPO); } } catch (DuplicateKeyException dke) { diff --git a/km-task/src/main/java/com/xiaojukeji/know/streaming/km/task/kafka/metadata/SyncConnectClusterAndWorkerTask.java b/km-task/src/main/java/com/xiaojukeji/know/streaming/km/task/kafka/metadata/SyncConnectClusterAndWorkerTask.java index 76fb2b992..cb886eea3 100644 --- a/km-task/src/main/java/com/xiaojukeji/know/streaming/km/task/kafka/metadata/SyncConnectClusterAndWorkerTask.java +++ b/km-task/src/main/java/com/xiaojukeji/know/streaming/km/task/kafka/metadata/SyncConnectClusterAndWorkerTask.java @@ -18,6 +18,7 @@ import com.xiaojukeji.know.streaming.km.common.constant.Constant; import com.xiaojukeji.know.streaming.km.common.enums.group.GroupStateEnum; import com.xiaojukeji.know.streaming.km.common.enums.group.GroupTypeEnum; +import com.xiaojukeji.know.streaming.km.common.enums.jmx.JmxEnum; import com.xiaojukeji.know.streaming.km.core.service.connect.cluster.ConnectClusterService; import com.xiaojukeji.know.streaming.km.core.service.connect.worker.WorkerConnectorService; import com.xiaojukeji.know.streaming.km.core.service.connect.worker.WorkerService; @@ -104,7 +105,7 @@ private Result handleWorkerMetadata(Long connectClusterId, KSGroupDescript connectClusterId, memberDescription.consumerId(), memberDescription.host().substring(1), - Constant.INVALID_CODE, + JmxEnum.UNKNOWN.getPort(), assignment.getWorkerState().url(), assignment.getAssignment().leaderUrl(), memberDescription.consumerId().equals(assignment.getAssignment().leader()) ? Constant.YES : Constant.NO @@ -115,7 +116,7 @@ private Result handleWorkerMetadata(Long connectClusterId, KSGroupDescript connectClusterId, memberDescription.consumerId(), memberDescription.host().substring(1), - Constant.INVALID_CODE, + JmxEnum.UNKNOWN.getPort(), "", "", Constant.NO From c11aa4fd17c32d4cbf350a4515a186a38606a85e Mon Sep 17 00:00:00 2001 From: EricZeng Date: Thu, 29 Jun 2023 21:36:58 +0800 Subject: [PATCH 26/37] =?UTF-8?q?[Bugfix]=E4=BF=AE=E5=A4=8DSecurity?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=EF=BC=8C=E5=90=8E=E7=AB=AF=E6=9D=83=E9=99=90?= =?UTF-8?q?=E7=82=B9=E7=BC=BA=E5=A4=B1=E9=97=AE=E9=A2=98=20(#1069)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1、补充Security模块-ACL相关权限点; 2、补充Security模块-User相关权限点; --- ...5\207\347\272\247\346\211\213\345\206\214.md" | 15 +++++++++++++++ .../src/main/resources/sql/dml-logi.sql | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git "a/docs/install_guide/\347\211\210\346\234\254\345\215\207\347\272\247\346\211\213\345\206\214.md" "b/docs/install_guide/\347\211\210\346\234\254\345\215\207\347\272\247\346\211\213\345\206\214.md" index d11126975..0a192658b 100644 --- "a/docs/install_guide/\347\211\210\346\234\254\345\215\207\347\272\247\346\211\213\345\206\214.md" +++ "b/docs/install_guide/\347\211\210\346\234\254\345\215\207\347\272\247\346\211\213\345\206\214.md" @@ -20,6 +20,21 @@ INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_del INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2030', '0', 'know-streaming'); INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2032', '0', 'know-streaming'); INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2034', '0', 'know-streaming'); + + +-- 多集群管理权限2023-06-29新增 +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2036', 'Security-ACL新增', '1593', '1', '2', 'Security-ACL新增', '0', 'know-streaming'); +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2038', 'Security-ACL删除', '1593', '1', '2', 'Security-ACL删除', '0', 'know-streaming'); +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2040', 'Security-User新增', '1593', '1', '2', 'Security-User新增', '0', 'know-streaming'); +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2042', 'Security-User删除', '1593', '1', '2', 'Security-User删除', '0', 'know-streaming'); +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2044', 'Security-User修改密码', '1593', '1', '2', 'Security-User修改密码', '0', 'know-streaming'); + +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2036', '0', 'know-streaming'); +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2038', '0', 'know-streaming'); +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2040', '0', 'know-streaming'); +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2042', '0', 'know-streaming'); +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2044', '0', 'know-streaming'); + ``` ### 升级至 `3.3.0` 版本 diff --git a/km-persistence/src/main/resources/sql/dml-logi.sql b/km-persistence/src/main/resources/sql/dml-logi.sql index a288b8d13..b19c9410c 100644 --- a/km-persistence/src/main/resources/sql/dml-logi.sql +++ b/km-persistence/src/main/resources/sql/dml-logi.sql @@ -132,4 +132,18 @@ INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_del INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2028', '0', 'know-streaming'); INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2030', '0', 'know-streaming'); INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2032', '0', 'know-streaming'); -INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2034', '0', 'know-streaming'); \ No newline at end of file +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2034', '0', 'know-streaming'); + + +-- 多集群管理权限2023-06-29新增 +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2036', 'Security-ACL新增', '1593', '1', '2', 'Security-ACL新增', '0', 'know-streaming'); +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2038', 'Security-ACL删除', '1593', '1', '2', 'Security-ACL删除', '0', 'know-streaming'); +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2040', 'Security-User新增', '1593', '1', '2', 'Security-User新增', '0', 'know-streaming'); +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2042', 'Security-User删除', '1593', '1', '2', 'Security-User删除', '0', 'know-streaming'); +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2044', 'Security-User修改密码', '1593', '1', '2', 'Security-User修改密码', '0', 'know-streaming'); + +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2036', '0', 'know-streaming'); +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2038', '0', 'know-streaming'); +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2040', '0', 'know-streaming'); +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2042', '0', 'know-streaming'); +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2044', '0', 'know-streaming'); From 75be94fbea2886a0335c46121d5fd163dddfc7a1 Mon Sep 17 00:00:00 2001 From: HwiLu <2418979865@qq.com> Date: Thu, 29 Jun 2023 21:51:08 +0800 Subject: [PATCH 27/37] =?UTF-8?q?[Doc]=E8=A1=A5=E5=85=85ZK=E6=97=A0?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=8E=92=E6=9F=A5=E8=AF=B4=E6=98=8E=20(#1004?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补充ZK无数据排查说明 --------- Co-authored-by: EricZeng --- docs/user_guide/faq.md | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/docs/user_guide/faq.md b/docs/user_guide/faq.md index 6af0f94e8..b66523ff4 100644 --- a/docs/user_guide/faq.md +++ b/docs/user_guide/faq.md @@ -20,7 +20,7 @@ - [14、对接Ldap的配置](#14对接ldap的配置) - [15、测试时使用Testcontainers的说明](#15测试时使用testcontainers的说明) - [16、JMX连接失败怎么办](#16jmx连接失败怎么办) - + - [17、zk监控无数据问题](#17zk监控无数据问题) ## 1、支持哪些 Kafka 版本? @@ -253,4 +253,28 @@ spring: ## 16、JMX连接失败怎么办 -详细见:[解决连接JMX失败](../dev_guide/%E8%A7%A3%E5%86%B3%E8%BF%9E%E6%8E%A5JMX%E5%A4%B1%E8%B4%A5.md) \ No newline at end of file +详细见:[解决连接JMX失败](../dev_guide/%E8%A7%A3%E5%86%B3%E8%BF%9E%E6%8E%A5JMX%E5%A4%B1%E8%B4%A5.md) + + +## 17、zk监控无数据问题 + +**现象:** +zookeeper集群正常,但Ks上zk页面所有监控指标无数据,`KnowStreaming` log_error.log日志提示 + +```vim +[MetricCollect-Shard-0-8-thread-1] ERROR class=c.x.k.s.k.c.s.h.c.z.HealthCheckZookeeperService||method=checkWatchCount||param=ZookeeperParam(zkAddressList=[Tuple{v1=192.168.xxx.xx, v2=2181}, Tuple{v1=192.168.xxx.xx, v2=2181}, Tuple{v1=192.168.xxx.xx, v2=2181}], zkConfig=null)||config=HealthAmountRatioConfig(amount=100000, ratio=0.8)||result=Result{message='mntr is not executed because it is not in the whitelist. +', code=8031, data=null}||errMsg=get metrics failed, may be collect failed or zk mntr command not in whitelist. +2023-04-23 14:39:07.234 [MetricCollect-Shard-0-8-thread-1] ERROR class=c.x.k.s.k.c.s.h.checker.AbstractHeal +``` + + +原因就很明确了。需要开放zk的四字命令,在`zoo.cfg`配置文件中添加 +``` +4lw.commands.whitelist=mntr,stat,ruok,envi,srvr,envi,cons,conf,wchs,wchp +``` + + +建议至少开放上述几个四字命令,当然,您也可以全部开放 +``` +4lw.commands.whitelist=* +``` From d68a19679e19f81135abe17ad209acff6cf95703 Mon Sep 17 00:00:00 2001 From: EricZeng Date: Mon, 3 Jul 2023 14:37:35 +0800 Subject: [PATCH 28/37] =?UTF-8?q?[Optimize]Group=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E7=9A=84maxLag=E6=8C=87=E6=A0=87=E8=B0=83=E6=95=B4=E4=B8=BA?= =?UTF-8?q?=E5=AE=9E=E6=97=B6=E8=8E=B7=E5=8F=96=20(#1074)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1、增加调用的超时时间,在前端需要的超时时间内返回; 2、将Group列表的maxLag指标调整为实时获取; --- ...07\347\272\247\346\211\213\345\206\214.md" | 9 ++ .../streaming/km/biz/group/GroupManager.java | 2 + .../km/biz/group/impl/GroupManagerImpl.java | 89 +++++++++++++++++-- .../biz/topic/impl/TopicStateManagerImpl.java | 16 +++- .../km/core/service/config/ConfigUtils.java | 16 ---- .../km/core/service/config/KSConfigUtils.java | 24 +++++ .../group/impl/GroupMetricServiceImpl.java | 30 +++++-- .../core/utils/ApiCallThreadPoolService.java | 9 ++ km-rest/src/main/resources/application.yml | 4 + 9 files changed, 162 insertions(+), 37 deletions(-) delete mode 100644 km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/config/ConfigUtils.java create mode 100644 km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/config/KSConfigUtils.java diff --git "a/docs/install_guide/\347\211\210\346\234\254\345\215\207\347\272\247\346\211\213\345\206\214.md" "b/docs/install_guide/\347\211\210\346\234\254\345\215\207\347\272\247\346\211\213\345\206\214.md" index 0a192658b..ba8fa8d88 100644 --- "a/docs/install_guide/\347\211\210\346\234\254\345\215\207\347\272\247\346\211\213\345\206\214.md" +++ "b/docs/install_guide/\347\211\210\346\234\254\345\215\207\347\272\247\346\211\213\345\206\214.md" @@ -6,6 +6,15 @@ ### 升级至 `master` 版本 +**配置变更** + +```yaml +# 新增的配置 +request: # 请求相关的配置 + api-call: # api调用 + timeout-unit-ms: 8000 # 超时时间,默认8000毫秒 +``` + **SQL 变更** ```sql -- 多集群管理权限2023-06-27新增 diff --git a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/GroupManager.java b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/GroupManager.java index a6b41eef5..ea6465a38 100644 --- a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/GroupManager.java +++ b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/GroupManager.java @@ -42,5 +42,7 @@ PaginationResult pagingGroupTopicConsumedMetrics(Lon Result deleteGroupOffsets(GroupOffsetDeleteDTO dto, String operator) throws Exception; + @Deprecated List getGroupTopicOverviewVOList(Long clusterPhyId, List groupMemberPOList); + List getGroupTopicOverviewVOList(Long clusterPhyId, List groupMemberPOList, Integer timeoutUnitMs); } diff --git a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/impl/GroupManagerImpl.java b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/impl/GroupManagerImpl.java index c80c40205..1f37c368c 100644 --- a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/impl/GroupManagerImpl.java +++ b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/impl/GroupManagerImpl.java @@ -45,12 +45,14 @@ import com.xiaojukeji.know.streaming.km.common.utils.PaginationUtil; import com.xiaojukeji.know.streaming.km.common.utils.ValidateUtils; import com.xiaojukeji.know.streaming.km.core.service.cluster.ClusterPhyService; +import com.xiaojukeji.know.streaming.km.core.service.config.KSConfigUtils; import com.xiaojukeji.know.streaming.km.core.service.group.GroupMetricService; import com.xiaojukeji.know.streaming.km.core.service.group.GroupService; import com.xiaojukeji.know.streaming.km.core.service.group.OpGroupService; import com.xiaojukeji.know.streaming.km.core.service.partition.PartitionService; import com.xiaojukeji.know.streaming.km.core.service.topic.TopicService; import com.xiaojukeji.know.streaming.km.core.service.version.metrics.kafka.GroupMetricVersionItems; +import com.xiaojukeji.know.streaming.km.core.utils.ApiCallThreadPoolService; import com.xiaojukeji.know.streaming.km.persistence.es.dao.GroupMetricESDAO; import org.apache.kafka.common.ConsumerGroupState; import org.apache.kafka.common.TopicPartition; @@ -58,6 +60,7 @@ import org.springframework.stereotype.Component; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import static com.xiaojukeji.know.streaming.km.common.enums.group.GroupTypeEnum.CONNECT_CLUSTER_PROTOCOL_TYPE; @@ -87,6 +90,9 @@ public class GroupManagerImpl implements GroupManager { @Autowired private ClusterPhyService clusterPhyService; + @Autowired + private KSConfigUtils ksConfigUtils; + @Override public PaginationResult pagingGroupMembers(Long clusterPhyId, String topicName, @@ -94,19 +100,27 @@ public PaginationResult pagingGroupMembers(Long clusterPhy String searchTopicKeyword, String searchGroupKeyword, PaginationBaseDTO dto) { + long startTimeUnitMs = System.currentTimeMillis(); + PaginationResult paginationResult = groupService.pagingGroupMembers(clusterPhyId, topicName, groupName, searchTopicKeyword, searchGroupKeyword, dto); if (!paginationResult.hasData()) { return PaginationResult.buildSuc(new ArrayList<>(), paginationResult); } - List groupTopicVOList = this.getGroupTopicOverviewVOList(clusterPhyId, paginationResult.getData().getBizData()); + List groupTopicVOList = this.getGroupTopicOverviewVOList( + clusterPhyId, + paginationResult.getData().getBizData(), + ksConfigUtils.getApiCallLeftTimeUnitMs(System.currentTimeMillis() - startTimeUnitMs) // 超时时间 + ); return PaginationResult.buildSuc(groupTopicVOList, paginationResult); } @Override public PaginationResult pagingGroupTopicMembers(Long clusterPhyId, String groupName, PaginationBaseDTO dto) { + long startTimeUnitMs = System.currentTimeMillis(); + Group group = groupService.getGroupFromDB(clusterPhyId, groupName); //没有topicMember则直接返回 @@ -122,7 +136,14 @@ public PaginationResult pagingGroupTopicMembers(Long clust List groupMemberPOList = paginationResult.getData().getBizData().stream().map(elem -> new GroupMemberPO(clusterPhyId, elem.getTopicName(), groupName, group.getState().getState(), elem.getMemberCount())).collect(Collectors.toList()); - return PaginationResult.buildSuc(this.getGroupTopicOverviewVOList(clusterPhyId, groupMemberPOList), paginationResult); + return PaginationResult.buildSuc( + this.getGroupTopicOverviewVOList( + clusterPhyId, + groupMemberPOList, + ksConfigUtils.getApiCallLeftTimeUnitMs(System.currentTimeMillis() - startTimeUnitMs) // 超时时间 + ), + paginationResult + ); } @Override @@ -317,6 +338,49 @@ public List getGroupTopicOverviewVOList(Long clusterPhyId, return this.convert2GroupTopicOverviewVOList(groupMemberPOList, metricsListResult.getData()); } + @Override + public List getGroupTopicOverviewVOList(Long clusterPhyId, List poList, Integer timeoutUnitMs) { + Set requestedGroupSet = new HashSet<>(); + + // 获取指标 + Map> groupTopicLagMap = new ConcurrentHashMap<>(); + poList.forEach(elem -> { + if (requestedGroupSet.contains(elem.getGroupName())) { + // 该Group已经处理过 + return; + } + + requestedGroupSet.add(elem.getGroupName()); + ApiCallThreadPoolService.runnableTask( + String.format("clusterPhyId=%d||groupName=%s||msg=getGroupTopicLag", clusterPhyId, elem.getGroupName()), + timeoutUnitMs, + () -> { + Result> listResult = groupMetricService.collectGroupMetricsFromKafka(clusterPhyId, elem.getGroupName(), GroupMetricVersionItems.GROUP_METRIC_LAG); + if (listResult == null || !listResult.hasData()) { + return; + } + + Map lagMetricMap = new HashMap<>(); + listResult.getData().forEach(item -> { + Float newLag = item.getMetric(GroupMetricVersionItems.GROUP_METRIC_LAG); + if (newLag == null) { + return; + } + + Float oldLag = lagMetricMap.getOrDefault(item.getTopic(), newLag); + lagMetricMap.put(item.getTopic(), Math.max(oldLag, newLag)); + }); + + groupTopicLagMap.put(elem.getGroupName(), lagMetricMap); + } + ); + }); + + ApiCallThreadPoolService.waitResult(); + + return this.convert2GroupTopicOverviewVOList(poList, groupTopicLagMap); + } + /**************************************************** private method ****************************************************/ @@ -370,13 +434,22 @@ private List convert2GroupTopicOverviewVOList(List(); } - // > - Map> metricsMap = new HashMap<>(); + // > + Map> metricsMap = new HashMap<>(); metricsList.stream().forEach(elem -> { + Float metricValue = elem.getMetrics().get(GroupMetricVersionItems.GROUP_METRIC_LAG); + if (metricValue == null) { + return; + } + metricsMap.putIfAbsent(elem.getGroup(), new HashMap<>()); - metricsMap.get(elem.getGroup()).put(elem.getTopic(), elem); + metricsMap.get(elem.getGroup()).put(elem.getTopic(), metricValue); }); + return this.convert2GroupTopicOverviewVOList(poList, metricsMap); + } + + private List convert2GroupTopicOverviewVOList(List poList, Map> metricsMap) { List voList = new ArrayList<>(); for (GroupMemberPO po: poList) { GroupTopicOverviewVO vo = ConvertUtil.obj2Obj(po, GroupTopicOverviewVO.class); @@ -384,9 +457,9 @@ private List convert2GroupTopicOverviewVOList(List()).get(po.getTopicName()); - if (metrics != null) { - vo.setMaxLag(ConvertUtil.Float2Long(metrics.getMetrics().get(GroupMetricVersionItems.GROUP_METRIC_LAG))); + Float metricValue = metricsMap.getOrDefault(po.getGroupName(), new HashMap<>()).get(po.getTopicName()); + if (metricValue != null) { + vo.setMaxLag(ConvertUtil.Float2Long(metricValue)); } voList.add(vo); diff --git a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/topic/impl/TopicStateManagerImpl.java b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/topic/impl/TopicStateManagerImpl.java index cd970528b..3b4b5b5f4 100644 --- a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/topic/impl/TopicStateManagerImpl.java +++ b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/topic/impl/TopicStateManagerImpl.java @@ -38,6 +38,7 @@ import com.xiaojukeji.know.streaming.km.common.utils.ValidateUtils; import com.xiaojukeji.know.streaming.km.core.service.broker.BrokerService; import com.xiaojukeji.know.streaming.km.core.service.cluster.ClusterPhyService; +import com.xiaojukeji.know.streaming.km.core.service.config.KSConfigUtils; import com.xiaojukeji.know.streaming.km.core.service.group.GroupService; import com.xiaojukeji.know.streaming.km.core.service.partition.PartitionMetricService; import com.xiaojukeji.know.streaming.km.core.service.partition.PartitionService; @@ -89,6 +90,9 @@ public class TopicStateManagerImpl implements TopicStateManager { @Autowired private GroupManager groupManager; + @Autowired + private KSConfigUtils ksConfigUtils; + @Override public TopicBrokerAllVO getTopicBrokerAll(Long clusterPhyId, String topicName, String searchBrokerHost) throws NotExistException { Topic topic = topicService.getTopic(clusterPhyId, topicName); @@ -178,9 +182,7 @@ public Result> getTopicMessages(Long clusterPhyId, String to // 获取指定时间每个分区的offset(按指定开始时间查询消息时) if (OffsetTypeEnum.PRECISE_TIMESTAMP.getResetType() == dto.getFilterOffsetReset()) { Map timestampsToSearch = new HashMap<>(); - partitionList.forEach(topicPartition -> { - timestampsToSearch.put(topicPartition, dto.getStartTimestampUnitMs()); - }); + partitionList.forEach(topicPartition -> timestampsToSearch.put(topicPartition, dto.getStartTimestampUnitMs())); partitionOffsetAndTimestampMap = kafkaConsumer.offsetsForTimes(timestampsToSearch); } @@ -360,13 +362,19 @@ public Result getTopicBrokersPartitionsSummary( @Override public PaginationResult pagingTopicGroupsOverview(Long clusterPhyId, String topicName, String searchGroupName, PaginationBaseDTO dto) { + long startTimeUnitMs = System.currentTimeMillis(); + PaginationResult paginationResult = groupService.pagingGroupMembers(clusterPhyId, topicName, "", "", searchGroupName, dto); if (!paginationResult.hasData()) { return PaginationResult.buildSuc(new ArrayList<>(), paginationResult); } - List groupTopicVOList = groupManager.getGroupTopicOverviewVOList(clusterPhyId, paginationResult.getData().getBizData()); + List groupTopicVOList = groupManager.getGroupTopicOverviewVOList( + clusterPhyId, + paginationResult.getData().getBizData(), + ksConfigUtils.getApiCallLeftTimeUnitMs(System.currentTimeMillis() - startTimeUnitMs) // 超时时间 + ); return PaginationResult.buildSuc(groupTopicVOList, paginationResult); } diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/config/ConfigUtils.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/config/ConfigUtils.java deleted file mode 100644 index 58c26b691..000000000 --- a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/config/ConfigUtils.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.xiaojukeji.know.streaming.km.core.service.config; - -import lombok.Getter; -import org.springframework.stereotype.Service; - - -/** - * @author zengqiao - * @date 22/6/14 - */ -@Getter -@Service -public class ConfigUtils { - private ConfigUtils() { - } -} diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/config/KSConfigUtils.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/config/KSConfigUtils.java new file mode 100644 index 000000000..55cdf051b --- /dev/null +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/config/KSConfigUtils.java @@ -0,0 +1,24 @@ +package com.xiaojukeji.know.streaming.km.core.service.config; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + + +/** + * @author zengqiao + * @date 22/6/14 + */ +@Getter +@Service +public class KSConfigUtils { + private KSConfigUtils() { + } + + @Value(value = "${request.api-call.timeout-unit-ms:8000}") + private Integer apiCallTimeoutUnitMs; + + public Integer getApiCallLeftTimeUnitMs(Long costedUnitMs) { + return Math.max(1000, (int)(apiCallTimeoutUnitMs - costedUnitMs)); + } +} diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/impl/GroupMetricServiceImpl.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/impl/GroupMetricServiceImpl.java index c9d65468c..1303c2aef 100644 --- a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/impl/GroupMetricServiceImpl.java +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/impl/GroupMetricServiceImpl.java @@ -39,7 +39,7 @@ */ @Service("groupMetricService") public class GroupMetricServiceImpl extends BaseMetricService implements GroupMetricService { - private static final ILog LOGGER = LogFactory.getLog( GroupMetricServiceImpl.class); + private static final ILog LOGGER = LogFactory.getLog(GroupMetricServiceImpl.class); public static final String GROUP_METHOD_GET_JUST_FRO_TEST = "getMetricJustForTest"; public static final String GROUP_METHOD_GET_HEALTH_SCORE = "getMetricHealthScore"; @@ -54,7 +54,7 @@ protected List listMetricPOFields(){ @Override protected void initRegisterVCHandler(){ registerVCHandler( GROUP_METHOD_GET_JUST_FRO_TEST, this::getMetricJustForTest); - registerVCHandler( GROUP_METHOD_GET_LAG_RELEVANT_FROM_ADMIN_CLIENT, this::getLagRelevantFromAdminClient ); + registerVCHandler( GROUP_METHOD_GET_LAG_RELEVANT_FROM_ADMIN_CLIENT, this::getLagRelevantFromAdminClient); registerVCHandler( GROUP_METHOD_GET_HEALTH_SCORE, this::getMetricHealthScore); registerVCHandler( GROUP_METHOD_GET_STATE, this::getGroupState); } @@ -129,8 +129,14 @@ public Result> collectGroupMetricsFromKafka(Long clusterId, S @Override public Result> listGroupMetricsFromES(Long clusterId, MetricGroupPartitionDTO dto) { Table> retTable = groupMetricESDAO.listGroupMetrics( - clusterId, dto.getGroup(), dto.getGroupTopics(), dto.getMetricsNames(), - dto.getAggType(), dto.getStartTime(), dto.getEndTime()); + clusterId, + dto.getGroup(), + dto.getGroupTopics(), + dto.getMetricsNames(), + dto.getAggType(), + dto.getStartTime(), + dto.getEndTime() + ); List multiLinesVOS = metricMap2VO(clusterId, retTable.rowMap()); return Result.buildSuc(multiLinesVOS); @@ -140,7 +146,11 @@ public Result> listGroupMetricsFromES(Long clusterId, M public Result> listLatestMetricsAggByGroupTopicFromES(Long clusterPhyId, List groupTopicList, List metricNames, AggTypeEnum aggType) { List groupMetricPOS = groupMetricESDAO.listLatestMetricsAggByGroupTopic( - clusterPhyId, groupTopicList, metricNames, aggType); + clusterPhyId, + groupTopicList, + metricNames, + aggType + ); return Result.buildSuc( ConvertUtil.list2List(groupMetricPOS, GroupMetrics.class)); } @@ -149,7 +159,11 @@ public Result> listLatestMetricsAggByGroupTopicFromES(Long cl public Result> listPartitionLatestMetricsFromES(Long clusterPhyId, String groupName, String topicName, List metricNames) { List groupMetricPOS = groupMetricESDAO.listPartitionLatestMetrics( - clusterPhyId, groupName, topicName, metricNames); + clusterPhyId, + groupName, + topicName, + metricNames + ); return Result.buildSuc( ConvertUtil.list2List(groupMetricPOS, GroupMetrics.class)); } @@ -158,9 +172,7 @@ public Result> listPartitionLatestMetricsFromES(Long clusterP public Result countMetricValueOccurrencesFromES(Long clusterPhyId, String groupName, SearchTerm term, Long startTime, Long endTime) { setQueryMetricFlag(term); - int count = groupMetricESDAO.countMetricValue(clusterPhyId, groupName, - term, startTime, endTime); - + int count = groupMetricESDAO.countMetricValue(clusterPhyId, groupName, term, startTime, endTime); if(count < 0){ return Result.buildFail(); } diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/utils/ApiCallThreadPoolService.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/utils/ApiCallThreadPoolService.java index e66b4aa5c..cce34c6f7 100644 --- a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/utils/ApiCallThreadPoolService.java +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/utils/ApiCallThreadPoolService.java @@ -37,7 +37,16 @@ public static void runnableTask(String taskName, Integer timeoutUnisMs, Callable apiFutureUtil.runnableTask(taskName, timeoutUnisMs, callable); } + public static void runnableTask(String taskName, Integer timeoutUnisMs, Runnable runnable) { + apiFutureUtil.runnableTask(taskName, timeoutUnisMs, runnable); + } + + @Deprecated public static void waitResult(Integer stepWaitTimeUnitMs) { apiFutureUtil.waitResult(stepWaitTimeUnitMs); } + + public static void waitResult() { + apiFutureUtil.waitResult(0); + } } \ No newline at end of file diff --git a/km-rest/src/main/resources/application.yml b/km-rest/src/main/resources/application.yml index b8849dbf6..40152cdda 100644 --- a/km-rest/src/main/resources/application.yml +++ b/km-rest/src/main/resources/application.yml @@ -95,6 +95,10 @@ es: index: expire: 15 # 索引过期天数,15表示超过15天的索引会被KS过期删除 +request: # 请求相关的配置 + api-call: # api调用 + timeout-unit-ms: 8000 # 超时时间,默认8000毫秒 + # 普罗米修斯指标导出相关配置 management: endpoints: From 49e7fea6d375eb187aaa301b8ce28aadfffba6b1 Mon Sep 17 00:00:00 2001 From: EricZeng Date: Mon, 3 Jul 2023 15:33:15 +0800 Subject: [PATCH 29/37] =?UTF-8?q?[Optimize]Topic-Messages=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=90=8E=E7=AB=AF=E5=A2=9E=E5=8A=A0=E6=8C=89=E7=85=A7?= =?UTF-8?q?Partition=E5=92=8COffset=E7=BA=AC=E5=BA=A6=E7=9A=84=E6=8E=92?= =?UTF-8?q?=E5=BA=8F=20(#1075)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../biz/topic/impl/TopicStateManagerImpl.java | 203 ++++++++++-------- .../common/constant/PaginationConstant.java | 5 +- 2 files changed, 113 insertions(+), 95 deletions(-) diff --git a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/topic/impl/TopicStateManagerImpl.java b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/topic/impl/TopicStateManagerImpl.java index 3b4b5b5f4..c2a4f2444 100644 --- a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/topic/impl/TopicStateManagerImpl.java +++ b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/topic/impl/TopicStateManagerImpl.java @@ -28,6 +28,7 @@ import com.xiaojukeji.know.streaming.km.common.constant.Constant; import com.xiaojukeji.know.streaming.km.common.constant.KafkaConstant; import com.xiaojukeji.know.streaming.km.common.constant.MsgConstant; +import com.xiaojukeji.know.streaming.km.common.constant.PaginationConstant; import com.xiaojukeji.know.streaming.km.common.converter.TopicVOConverter; import com.xiaojukeji.know.streaming.km.common.enums.OffsetTypeEnum; import com.xiaojukeji.know.streaming.km.common.enums.SortTypeEnum; @@ -46,8 +47,6 @@ import com.xiaojukeji.know.streaming.km.core.service.topic.TopicMetricService; import com.xiaojukeji.know.streaming.km.core.service.topic.TopicService; import com.xiaojukeji.know.streaming.km.core.service.version.metrics.kafka.TopicMetricVersionItems; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; import org.apache.kafka.clients.consumer.*; import org.apache.kafka.common.TopicPartition; import org.apache.kafka.common.config.TopicConfig; @@ -105,7 +104,7 @@ public TopicBrokerAllVO getTopicBrokerAll(Long clusterPhyId, String topicName, S TopicBrokerAllVO allVO = new TopicBrokerAllVO(); allVO.setTotal(topic.getBrokerIdSet().size()); - allVO.setLive((int)brokerMap.values().stream().filter(elem -> elem.alive()).count()); + allVO.setLive((int)brokerMap.values().stream().filter(Broker::alive).count()); allVO.setDead(allVO.getTotal() - allVO.getLive()); allVO.setPartitionCount(topic.getPartitionNum()); @@ -157,95 +156,28 @@ public Result> getTopicMessages(Long clusterPhyId, String to return Result.buildFromIgnoreData(endOffsetsMapResult); } - List voList = new ArrayList<>(); - - KafkaConsumer kafkaConsumer = null; - try { - // 创建kafka-consumer - kafkaConsumer = new KafkaConsumer<>(this.generateClientProperties(clusterPhy, dto.getMaxRecords())); - - List partitionList = new ArrayList<>(); - long maxMessage = 0; - for (Map.Entry entry : endOffsetsMapResult.getData().entrySet()) { - long begin = beginOffsetsMapResult.getData().get(entry.getKey()); - long end = entry.getValue(); - if (begin == end){ - continue; - } - maxMessage += end - begin; - partitionList.add(entry.getKey()); - } - maxMessage = Math.min(maxMessage, dto.getMaxRecords()); - kafkaConsumer.assign(partitionList); - - Map partitionOffsetAndTimestampMap = new HashMap<>(); - // 获取指定时间每个分区的offset(按指定开始时间查询消息时) - if (OffsetTypeEnum.PRECISE_TIMESTAMP.getResetType() == dto.getFilterOffsetReset()) { - Map timestampsToSearch = new HashMap<>(); - partitionList.forEach(topicPartition -> timestampsToSearch.put(topicPartition, dto.getStartTimestampUnitMs())); - partitionOffsetAndTimestampMap = kafkaConsumer.offsetsForTimes(timestampsToSearch); - } - - for (TopicPartition partition : partitionList) { - if (OffsetTypeEnum.EARLIEST.getResetType() == dto.getFilterOffsetReset()) { - // 重置到最旧 - kafkaConsumer.seek(partition, beginOffsetsMapResult.getData().get(partition)); - } else if (OffsetTypeEnum.PRECISE_TIMESTAMP.getResetType() == dto.getFilterOffsetReset()) { - // 重置到指定时间 - kafkaConsumer.seek(partition, partitionOffsetAndTimestampMap.get(partition).offset()); - } else if (OffsetTypeEnum.PRECISE_OFFSET.getResetType() == dto.getFilterOffsetReset()) { - // 重置到指定位置 - - } else { - // 默认,重置到最新 - kafkaConsumer.seek(partition, Math.max(beginOffsetsMapResult.getData().get(partition), endOffsetsMapResult.getData().get(partition) - dto.getMaxRecords())); - } - } - - // 这里需要减去 KafkaConstant.POLL_ONCE_TIMEOUT_UNIT_MS 是因为poll一次需要耗时,如果这里不减去,则可能会导致poll之后,超过要求的时间 - while (System.currentTimeMillis() - startTime <= dto.getPullTimeoutUnitMs() && voList.size() < maxMessage) { - ConsumerRecords consumerRecords = kafkaConsumer.poll(Duration.ofMillis(KafkaConstant.POLL_ONCE_TIMEOUT_UNIT_MS)); - for (ConsumerRecord consumerRecord : consumerRecords) { - if (this.checkIfIgnore(consumerRecord, dto.getFilterKey(), dto.getFilterValue())) { - continue; - } - - voList.add(TopicVOConverter.convert2TopicRecordVO(topicName, consumerRecord)); - if (voList.size() >= dto.getMaxRecords()) { - break; - } - } - - // 超时则返回 - if (System.currentTimeMillis() - startTime + KafkaConstant.POLL_ONCE_TIMEOUT_UNIT_MS > dto.getPullTimeoutUnitMs() - || voList.size() > dto.getMaxRecords()) { - break; - } - } - - // 排序 - if (ObjectUtils.isNotEmpty(voList)) { - // 默认按时间倒序排序 - if (StringUtils.isBlank(dto.getSortType())) { - dto.setSortType(SortTypeEnum.DESC.getSortType()); - } - PaginationUtil.pageBySort(voList, dto.getSortField(), dto.getSortType()); - } + // 数据采集 + List voList = this.getTopicMessages(clusterPhy, topicName, beginOffsetsMapResult.getData(), endOffsetsMapResult.getData(), startTime, dto); - return Result.buildSuc(voList.subList(0, Math.min(dto.getMaxRecords(), voList.size()))); - } catch (Exception e) { - log.error("method=getTopicMessages||clusterPhyId={}||topicName={}||param={}||errMsg=exception", clusterPhyId, topicName, dto, e); + // 排序 + if (ValidateUtils.isBlank(dto.getSortType())) { + // 默认按时间倒序排序 + dto.setSortType(SortTypeEnum.DESC.getSortType()); + } + if (ValidateUtils.isBlank(dto.getSortField())) { + // 默认按照timestampUnitMs字段排序 + dto.setSortField(PaginationConstant.TOPIC_RECORDS_TIME_SORTED_FIELD); + } - throw new AdminOperateException(e.getMessage(), e, ResultStatus.KAFKA_OPERATE_FAILED); - } finally { - if (kafkaConsumer != null) { - try { - kafkaConsumer.close(Duration.ofMillis(KafkaConstant.POLL_ONCE_TIMEOUT_UNIT_MS)); - } catch (Exception e) { - // ignore - } - } + if (PaginationConstant.TOPIC_RECORDS_TIME_SORTED_FIELD.equals(dto.getSortField())) { + // 如果是时间类型,则第二排序规则是offset + PaginationUtil.pageBySort(voList, dto.getSortField(), dto.getSortType(), PaginationConstant.TOPIC_RECORDS_OFFSET_SORTED_FIELD, dto.getSortType()); + } else { + // 如果是非时间类型,则第二排序规则是时间 + PaginationUtil.pageBySort(voList, dto.getSortField(), dto.getSortType(), PaginationConstant.TOPIC_RECORDS_TIME_SORTED_FIELD, dto.getSortType()); } + + return Result.buildSuc(voList.subList(0, Math.min(dto.getMaxRecords(), voList.size()))); } @Override @@ -338,7 +270,7 @@ public Result getTopicBrokersPartitionsSummary( // Broker统计信息 vo.setBrokerCount(brokerMap.size()); - vo.setLiveBrokerCount((int)brokerMap.values().stream().filter(elem -> elem.alive()).count()); + vo.setLiveBrokerCount((int)brokerMap.values().stream().filter(Broker::alive).count()); vo.setDeadBrokerCount(vo.getBrokerCount() - vo.getLiveBrokerCount()); // Partition统计信息 @@ -394,11 +326,8 @@ private boolean checkIfIgnore(ConsumerRecord consumerRecord, Str // ignore return true; } - if (filterValue != null && consumerRecord.value() != null && !consumerRecord.value().contains(filterValue)) { - return true; - } - return false; + return (filterValue != null && consumerRecord.value() != null && !consumerRecord.value().contains(filterValue)); } private TopicBrokerSingleVO getTopicBrokerSingle(Long clusterPhyId, @@ -458,4 +387,90 @@ private Properties generateClientProperties(ClusterPhy clusterPhy, Integer maxPo props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, Math.max(2, Math.min(5, maxPollRecords))); return props; } + + private List getTopicMessages(ClusterPhy clusterPhy, + String topicName, + Map beginOffsetsMap, + Map endOffsetsMap, + long startTime, + TopicRecordDTO dto) throws AdminOperateException { + List voList = new ArrayList<>(); + + try (KafkaConsumer kafkaConsumer = new KafkaConsumer<>(this.generateClientProperties(clusterPhy, dto.getMaxRecords()))) { + // 移动到指定位置 + long maxMessage = this.assignAndSeekToSpecifiedOffset(kafkaConsumer, beginOffsetsMap, endOffsetsMap, dto); + + // 这里需要减去 KafkaConstant.POLL_ONCE_TIMEOUT_UNIT_MS 是因为poll一次需要耗时,如果这里不减去,则可能会导致poll之后,超过要求的时间 + while (System.currentTimeMillis() - startTime <= dto.getPullTimeoutUnitMs() && voList.size() < maxMessage) { + ConsumerRecords consumerRecords = kafkaConsumer.poll(Duration.ofMillis(KafkaConstant.POLL_ONCE_TIMEOUT_UNIT_MS)); + for (ConsumerRecord consumerRecord : consumerRecords) { + if (this.checkIfIgnore(consumerRecord, dto.getFilterKey(), dto.getFilterValue())) { + continue; + } + + voList.add(TopicVOConverter.convert2TopicRecordVO(topicName, consumerRecord)); + if (voList.size() >= dto.getMaxRecords()) { + break; + } + } + + // 超时则返回 + if (System.currentTimeMillis() - startTime + KafkaConstant.POLL_ONCE_TIMEOUT_UNIT_MS > dto.getPullTimeoutUnitMs() + || voList.size() > dto.getMaxRecords()) { + break; + } + } + + return voList; + } catch (Exception e) { + log.error("method=getTopicMessages||clusterPhyId={}||topicName={}||param={}||errMsg=exception", clusterPhy.getId(), topicName, dto, e); + + throw new AdminOperateException(e.getMessage(), e, ResultStatus.KAFKA_OPERATE_FAILED); + } + } + + private long assignAndSeekToSpecifiedOffset(KafkaConsumer kafkaConsumer, + Map beginOffsetsMap, + Map endOffsetsMap, + TopicRecordDTO dto) { + List partitionList = new ArrayList<>(); + long maxMessage = 0; + for (Map.Entry entry : endOffsetsMap.entrySet()) { + long begin = beginOffsetsMap.get(entry.getKey()); + long end = entry.getValue(); + if (begin == end){ + continue; + } + maxMessage += end - begin; + partitionList.add(entry.getKey()); + } + maxMessage = Math.min(maxMessage, dto.getMaxRecords()); + kafkaConsumer.assign(partitionList); + + Map partitionOffsetAndTimestampMap = new HashMap<>(); + // 获取指定时间每个分区的offset(按指定开始时间查询消息时) + if (OffsetTypeEnum.PRECISE_TIMESTAMP.getResetType() == dto.getFilterOffsetReset()) { + Map timestampsToSearch = new HashMap<>(); + partitionList.forEach(topicPartition -> timestampsToSearch.put(topicPartition, dto.getStartTimestampUnitMs())); + partitionOffsetAndTimestampMap = kafkaConsumer.offsetsForTimes(timestampsToSearch); + } + + for (TopicPartition partition : partitionList) { + if (OffsetTypeEnum.EARLIEST.getResetType() == dto.getFilterOffsetReset()) { + // 重置到最旧 + kafkaConsumer.seek(partition, beginOffsetsMap.get(partition)); + } else if (OffsetTypeEnum.PRECISE_TIMESTAMP.getResetType() == dto.getFilterOffsetReset()) { + // 重置到指定时间 + kafkaConsumer.seek(partition, partitionOffsetAndTimestampMap.get(partition).offset()); + } else if (OffsetTypeEnum.PRECISE_OFFSET.getResetType() == dto.getFilterOffsetReset()) { + // 重置到指定位置 + + } else { + // 默认,重置到最新 + kafkaConsumer.seek(partition, Math.max(beginOffsetsMap.get(partition), endOffsetsMap.get(partition) - dto.getMaxRecords())); + } + } + + return maxMessage; + } } diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/constant/PaginationConstant.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/constant/PaginationConstant.java index 9b8def80c..9d3b83558 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/constant/PaginationConstant.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/constant/PaginationConstant.java @@ -27,5 +27,8 @@ private PaginationConstant() { /** * groupTopic列表的默认排序规则 */ - public static final String DEFAULT_GROUP_TOPIC_SORTED_FIELD = "topicName"; + public static final String DEFAULT_GROUP_TOPIC_SORTED_FIELD = "topicName"; + + public static final String TOPIC_RECORDS_TIME_SORTED_FIELD = "timestampUnitMs"; + public static final String TOPIC_RECORDS_OFFSET_SORTED_FIELD = "offset"; } From abaadfb9a8c96e442dbe15a1d5d8afcbee0d5987 Mon Sep 17 00:00:00 2001 From: EricZeng Date: Tue, 4 Jul 2023 14:18:12 +0800 Subject: [PATCH 30/37] =?UTF-8?q?[Optimize]Topic-Partitions=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E4=B8=BB=E5=8A=A8=E8=B6=85=E6=97=B6=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=20(#1076)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题: leader=-1的分区获取offset信息时,耗时时间过久会导致前端超时,进而整个页面的数据都获取不到; 解决: 后端主动在前端超时前,对一些请求进行超时,避免导致所有的信息都没有返回给前端; --- .../mm2/impl/MirrorMakerManagerImpl.java | 7 ++- .../biz/topic/impl/TopicStateManagerImpl.java | 44 ++++++++++++------- .../core/utils/ApiCallThreadPoolService.java | 16 ++++--- .../api/v3/topic/TopicStateController.java | 4 +- 4 files changed, 42 insertions(+), 29 deletions(-) diff --git a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/connect/mm2/impl/MirrorMakerManagerImpl.java b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/connect/mm2/impl/MirrorMakerManagerImpl.java index 803daa266..99f2747f2 100644 --- a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/connect/mm2/impl/MirrorMakerManagerImpl.java +++ b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/connect/mm2/impl/MirrorMakerManagerImpl.java @@ -48,6 +48,7 @@ import org.springframework.stereotype.Service; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.stream.Collectors; @@ -597,7 +598,7 @@ public static List supplyData2ClusterMirrorMakerOv private List completeClusterInfo(List mirrorMakerVOList) { - Map connectorInfoMap = new HashMap<>(); + Map connectorInfoMap = new ConcurrentHashMap<>(); for (ClusterMirrorMakerOverviewVO mirrorMakerVO : mirrorMakerVOList) { ApiCallThreadPoolService.runnableTask(String.format("method=completeClusterInfo||connectClusterId=%d||connectorName=%s||getMirrorMakerInfo", mirrorMakerVO.getConnectClusterId(), mirrorMakerVO.getConnectorName()), @@ -607,12 +608,10 @@ private List completeClusterInfo(List newMirrorMakerVOList = new ArrayList<>(); for (ClusterMirrorMakerOverviewVO mirrorMakerVO : mirrorMakerVOList) { diff --git a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/topic/impl/TopicStateManagerImpl.java b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/topic/impl/TopicStateManagerImpl.java index c2a4f2444..d973ece98 100644 --- a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/topic/impl/TopicStateManagerImpl.java +++ b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/topic/impl/TopicStateManagerImpl.java @@ -47,6 +47,7 @@ import com.xiaojukeji.know.streaming.km.core.service.topic.TopicMetricService; import com.xiaojukeji.know.streaming.km.core.service.topic.TopicService; import com.xiaojukeji.know.streaming.km.core.service.version.metrics.kafka.TopicMetricVersionItems; +import com.xiaojukeji.know.streaming.km.core.utils.ApiCallThreadPoolService; import org.apache.kafka.clients.consumer.*; import org.apache.kafka.common.TopicPartition; import org.apache.kafka.common.config.TopicConfig; @@ -60,7 +61,7 @@ @Component public class TopicStateManagerImpl implements TopicStateManager { - private static final ILog log = LogFactory.getLog(TopicStateManagerImpl.class); + private static final ILog LOGGER = LogFactory.getLog(TopicStateManagerImpl.class); @Autowired private TopicService topicService; @@ -232,26 +233,37 @@ public Result getTopicState(Long clusterPhyId, String topicName) { @Override public Result> getTopicPartitions(Long clusterPhyId, String topicName, List metricsNames) { + long startTime = System.currentTimeMillis(); + List partitionList = partitionService.listPartitionByTopic(clusterPhyId, topicName); if (ValidateUtils.isEmptyList(partitionList)) { return Result.buildSuc(); } - Result> metricsResult = partitionMetricService.collectPartitionsMetricsFromKafka(clusterPhyId, topicName, metricsNames); - if (metricsResult.failed()) { - // 仅打印错误日志,但是不直接返回错误 - log.error( - "method=getTopicPartitions||clusterPhyId={}||topicName={}||result={}||msg=get metrics from es failed", - clusterPhyId, topicName, metricsResult - ); - } - - // 转map Map metricsMap = new HashMap<>(); - if (metricsResult.hasData()) { - for (PartitionMetrics metrics: metricsResult.getData()) { - metricsMap.put(metrics.getPartitionId(), metrics); - } + ApiCallThreadPoolService.runnableTask( + String.format("clusterPhyId=%d||topicName=%s||method=getTopicPartitions", clusterPhyId, topicName), + ksConfigUtils.getApiCallLeftTimeUnitMs(System.currentTimeMillis() - startTime), + () -> { + Result> metricsResult = partitionMetricService.collectPartitionsMetricsFromKafka(clusterPhyId, topicName, metricsNames); + if (metricsResult.failed()) { + // 仅打印错误日志,但是不直接返回错误 + LOGGER.error( + "method=getTopicPartitions||clusterPhyId={}||topicName={}||result={}||msg=get metrics from kafka failed", + clusterPhyId, topicName, metricsResult + ); + } + + for (PartitionMetrics metrics: metricsResult.getData()) { + metricsMap.put(metrics.getPartitionId(), metrics); + } + } + ); + boolean finished = ApiCallThreadPoolService.waitResultAndReturnFinished(1); + + if (!finished && metricsMap.isEmpty()) { + // 未完成 -> 打印日志 + LOGGER.error("method=getTopicPartitions||clusterPhyId={}||topicName={}||msg=get metrics from kafka failed", clusterPhyId, topicName); } List voList = new ArrayList<>(); @@ -423,7 +435,7 @@ private List getTopicMessages(ClusterPhy clusterPhy, return voList; } catch (Exception e) { - log.error("method=getTopicMessages||clusterPhyId={}||topicName={}||param={}||errMsg=exception", clusterPhy.getId(), topicName, dto, e); + LOGGER.error("method=getTopicMessages||clusterPhyId={}||topicName={}||param={}||errMsg=exception", clusterPhy.getId(), topicName, dto, e); throw new AdminOperateException(e.getMessage(), e, ResultStatus.KAFKA_OPERATE_FAILED); } diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/utils/ApiCallThreadPoolService.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/utils/ApiCallThreadPoolService.java index cce34c6f7..671616577 100644 --- a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/utils/ApiCallThreadPoolService.java +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/utils/ApiCallThreadPoolService.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; +import java.util.List; import java.util.concurrent.Callable; /** @@ -21,7 +22,7 @@ public class ApiCallThreadPoolService { @Value(value = "${thread-pool.api.queue-size:500}") private Integer queueSize; - private static FutureWaitUtil apiFutureUtil; + private static FutureWaitUtil apiFutureUtil; @PostConstruct private void init() { @@ -33,7 +34,7 @@ private void init() { ); } - public static void runnableTask(String taskName, Integer timeoutUnisMs, Callable callable) { + public static void runnableTask(String taskName, Integer timeoutUnisMs, Callable callable) { apiFutureUtil.runnableTask(taskName, timeoutUnisMs, callable); } @@ -41,12 +42,13 @@ public static void runnableTask(String taskName, Integer timeoutUnisMs, Runnable apiFutureUtil.runnableTask(taskName, timeoutUnisMs, runnable); } - @Deprecated - public static void waitResult(Integer stepWaitTimeUnitMs) { - apiFutureUtil.waitResult(stepWaitTimeUnitMs); - } - public static void waitResult() { apiFutureUtil.waitResult(0); } + + public static boolean waitResultAndReturnFinished(int taskNum) { + List resultList = apiFutureUtil.waitResult(0); + + return resultList != null && resultList.size() == taskNum; + } } \ No newline at end of file diff --git a/km-rest/src/main/java/com/xiaojukeji/know/streaming/km/rest/api/v3/topic/TopicStateController.java b/km-rest/src/main/java/com/xiaojukeji/know/streaming/km/rest/api/v3/topic/TopicStateController.java index b03715370..c1b79c902 100644 --- a/km-rest/src/main/java/com/xiaojukeji/know/streaming/km/rest/api/v3/topic/TopicStateController.java +++ b/km-rest/src/main/java/com/xiaojukeji/know/streaming/km/rest/api/v3/topic/TopicStateController.java @@ -74,7 +74,7 @@ public Result getTopicBrokers(@PathVariable Long clusterPhyId, @GetMapping(value = "clusters/{clusterPhyId}/topics/{topicName}/brokers-partitions-summary") @ResponseBody public Result getTopicBrokersPartitionsSummary(@PathVariable Long clusterPhyId, - @PathVariable String topicName) throws Exception { + @PathVariable String topicName) { return topicStateManager.getTopicBrokersPartitionsSummary(clusterPhyId, topicName); } @@ -83,7 +83,7 @@ public Result getTopicBrokersPartitionsSummary( @ResponseBody public Result> getTopicPartitions(@PathVariable Long clusterPhyId, @PathVariable String topicName, - @RequestBody List metricsNames) throws Exception { + @RequestBody List metricsNames) { return topicStateManager.getTopicPartitions(clusterPhyId, topicName, metricsNames); } From 0cd071c5c63a1b078096078084e5fb54652acd2b Mon Sep 17 00:00:00 2001 From: EricZeng Date: Wed, 5 Jul 2023 11:55:16 +0800 Subject: [PATCH 31/37] =?UTF-8?q?[Optimize]=E5=8E=BB=E9=99=A4=E5=AF=B9Conn?= =?UTF-8?q?ect=E9=9B=86=E7=BE=A4=E7=9A=84clusterUrl=E7=9A=84=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E6=9B=B4=E6=96=B0=20(#1079)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题: clusterUrl动态更新可能会获取到错误的地址,导致请求connect集群相关信息失败; 解决: 去除动态更新,仅支持用户输入; 遗留: 前端需要支持用户输入; --- .../bean/entity/connect/ConnectCluster.java | 17 ++++++++++++++++ .../bean/po/connect/ConnectClusterPO.java | 2 +- .../impl/ConnectClusterServiceImpl.java | 14 ++++--------- .../connector/impl/ConnectorServiceImpl.java | 20 +++++++++---------- .../plugin/impl/PluginServiceImpl.java | 4 ++-- .../impl/WorkerConnectorServiceImpl.java | 2 +- .../SyncConnectClusterAndWorkerTask.java | 5 ----- 7 files changed, 35 insertions(+), 29 deletions(-) diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/connect/ConnectCluster.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/connect/ConnectCluster.java index a4c67bbc3..43a6ce217 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/connect/ConnectCluster.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/entity/connect/ConnectCluster.java @@ -1,6 +1,7 @@ package com.xiaojukeji.know.streaming.km.common.bean.entity.connect; import com.xiaojukeji.know.streaming.km.common.bean.entity.EntityIdInterface; +import com.xiaojukeji.know.streaming.km.common.utils.ValidateUtils; import lombok.Data; import java.io.Serializable; @@ -54,6 +55,22 @@ public class ConnectCluster implements Serializable, Comparable, */ private String clusterUrl; + public String getSuitableRequestUrl() { + // 优先使用用户填写的url + String suitableRequestUrl = this.clusterUrl; + if (ValidateUtils.isBlank(suitableRequestUrl)) { + // 用户如果没有填写,则使用元信息中的url + suitableRequestUrl = this.memberLeaderUrl; + } + + //url去斜杠 + if (suitableRequestUrl.length() > 0 && suitableRequestUrl.charAt(suitableRequestUrl.length() - 1) == '/') { + return suitableRequestUrl.substring(0, suitableRequestUrl.length() - 1); + } + + return suitableRequestUrl; + } + @Override public int compareTo(ConnectCluster connectCluster) { return this.id.compareTo(connectCluster.getId()); diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/po/connect/ConnectClusterPO.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/po/connect/ConnectClusterPO.java index f0a364e61..9175a6c10 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/po/connect/ConnectClusterPO.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/po/connect/ConnectClusterPO.java @@ -29,7 +29,7 @@ public class ConnectClusterPO extends BasePO { private Integer state; /** - * 集群地址 + * 用户填写的集群地址 */ private String clusterUrl; diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/connect/cluster/impl/ConnectClusterServiceImpl.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/connect/cluster/impl/ConnectClusterServiceImpl.java index 030b78ad1..86879662d 100644 --- a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/connect/cluster/impl/ConnectClusterServiceImpl.java +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/connect/cluster/impl/ConnectClusterServiceImpl.java @@ -40,12 +40,6 @@ public class ConnectClusterServiceImpl implements ConnectClusterService { @Override public Long replaceAndReturnIdInDB(ConnectClusterMetadata metadata) { - //url去斜杠 - String clusterUrl = metadata.getMemberLeaderUrl(); - if (clusterUrl.charAt(clusterUrl.length() - 1) == '/') { - clusterUrl = clusterUrl.substring(0, clusterUrl.length() - 1); - } - ConnectClusterPO oldPO = this.getPOFromDB(metadata.getKafkaClusterPhyId(), metadata.getGroupName()); if (oldPO == null) { oldPO = new ConnectClusterPO(); @@ -54,7 +48,7 @@ public Long replaceAndReturnIdInDB(ConnectClusterMetadata metadata) { oldPO.setName(metadata.getGroupName()); oldPO.setState(metadata.getState().getCode()); oldPO.setMemberLeaderUrl(metadata.getMemberLeaderUrl()); - oldPO.setClusterUrl(clusterUrl); + oldPO.setClusterUrl(""); oldPO.setVersion(KafkaConstant.DEFAULT_CONNECT_VERSION); connectClusterDAO.insert(oldPO); @@ -69,11 +63,11 @@ public Long replaceAndReturnIdInDB(ConnectClusterMetadata metadata) { if (ValidateUtils.isBlank(oldPO.getVersion())) { oldPO.setVersion(KafkaConstant.DEFAULT_CONNECT_VERSION); } - if (!ValidateUtils.isBlank(clusterUrl)) { - oldPO.setClusterUrl(clusterUrl); + if (ValidateUtils.isNull(oldPO.getClusterUrl())) { + oldPO.setClusterUrl(""); } - connectClusterDAO.updateById(oldPO); + connectClusterDAO.updateById(oldPO); return oldPO.getId(); } diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/connect/connector/impl/ConnectorServiceImpl.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/connect/connector/impl/ConnectorServiceImpl.java index c042276df..133355a84 100644 --- a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/connect/connector/impl/ConnectorServiceImpl.java +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/connect/connector/impl/ConnectorServiceImpl.java @@ -87,7 +87,7 @@ public Result createConnector(Long connectClusterId, String con props.put("config", configs); ConnectorInfo connectorInfo = restTool.postObjectWithJsonContent( - connectCluster.getClusterUrl() + CREATE_CONNECTOR_URI, + connectCluster.getSuitableRequestUrl() + CREATE_CONNECTOR_URI, props, ConnectorInfo.class ); @@ -127,7 +127,7 @@ public Result> listConnectorsFromCluster(Long connectClusterId) { } List nameList = restTool.getArrayObjectWithJsonContent( - connectCluster.getClusterUrl() + LIST_CONNECTORS_URI, + connectCluster.getSuitableRequestUrl() + LIST_CONNECTORS_URI, new HashMap<>(), String.class ); @@ -224,7 +224,7 @@ public Result resumeConnector(Long connectClusterId, String connectorName, } restTool.putJsonForObject( - connectCluster.getClusterUrl() + String.format(RESUME_CONNECTOR_URI, connectorName), + connectCluster.getSuitableRequestUrl() + String.format(RESUME_CONNECTOR_URI, connectorName), new HashMap<>(), String.class ); @@ -259,7 +259,7 @@ public Result restartConnector(Long connectClusterId, String connectorName } restTool.postObjectWithJsonContent( - connectCluster.getClusterUrl() + String.format(RESTART_CONNECTOR_URI, connectorName), + connectCluster.getSuitableRequestUrl() + String.format(RESTART_CONNECTOR_URI, connectorName), new HashMap<>(), String.class ); @@ -294,7 +294,7 @@ public Result stopConnector(Long connectClusterId, String connectorName, S } restTool.putJsonForObject( - connectCluster.getClusterUrl() + String.format(PAUSE_CONNECTOR_URI, connectorName), + connectCluster.getSuitableRequestUrl() + String.format(PAUSE_CONNECTOR_URI, connectorName), new HashMap<>(), String.class ); @@ -329,7 +329,7 @@ public Result deleteConnector(Long connectClusterId, String connectorName, } restTool.deleteWithParamsAndHeader( - connectCluster.getClusterUrl() + String.format(DELETE_CONNECTOR_URI, connectorName), + connectCluster.getSuitableRequestUrl() + String.format(DELETE_CONNECTOR_URI, connectorName), new HashMap<>(), new HashMap<>(), String.class @@ -365,7 +365,7 @@ public Result updateConnectorConfig(Long connectClusterId, String connecto } ConnectorInfo connectorInfo = restTool.putJsonForObject( - connectCluster.getClusterUrl() + String.format(UPDATE_CONNECTOR_CONFIG_URI, connectorName), + connectCluster.getSuitableRequestUrl() + String.format(UPDATE_CONNECTOR_CONFIG_URI, connectorName), configs, org.apache.kafka.connect.runtime.rest.entities.ConnectorInfo.class ); @@ -532,7 +532,7 @@ private int deleteConnectorInDB(Long connectClusterId, String connectorName) { private Result getConnectorInfoFromCluster(ConnectCluster connectCluster, String connectorName) { try { ConnectorInfo connectorInfo = restTool.getForObject( - connectCluster.getClusterUrl() + GET_CONNECTOR_INFO_PREFIX_URI + "/" + connectorName, + connectCluster.getSuitableRequestUrl() + GET_CONNECTOR_INFO_PREFIX_URI + "/" + connectorName, new HashMap<>(), ConnectorInfo.class ); @@ -558,7 +558,7 @@ private Result getConnectorInfoFromCluster(ConnectCluster conne private Result> getConnectorTopicsFromCluster(ConnectCluster connectCluster, String connectorName) { try { Properties properties = restTool.getForObject( - connectCluster.getClusterUrl() + String.format(GET_CONNECTOR_TOPICS_URI, connectorName), + connectCluster.getSuitableRequestUrl() + String.format(GET_CONNECTOR_TOPICS_URI, connectorName), new HashMap<>(), Properties.class ); @@ -578,7 +578,7 @@ private Result> getConnectorTopicsFromCluster(ConnectCluster connec private Result getConnectorStateInfoFromCluster(ConnectCluster connectCluster, String connectorName) { try { KSConnectorStateInfo connectorStateInfo = restTool.getForObject( - connectCluster.getClusterUrl() + String.format(GET_CONNECTOR_STATUS_URI, connectorName), + connectCluster.getSuitableRequestUrl() + String.format(GET_CONNECTOR_STATUS_URI, connectorName), new HashMap<>(), KSConnectorStateInfo.class ); diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/connect/plugin/impl/PluginServiceImpl.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/connect/plugin/impl/PluginServiceImpl.java index fa6f13942..8ef4d3917 100644 --- a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/connect/plugin/impl/PluginServiceImpl.java +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/connect/plugin/impl/PluginServiceImpl.java @@ -66,7 +66,7 @@ public Result validateConfig(Long connectClusterId, Properti // 通过参数检查接口,获取插件配置 ConfigInfos configInfos = restTool.putJsonForObject( - connectCluster.getClusterUrl() + String.format(GET_PLUGIN_CONFIG_DESC_URI, props.getProperty(KafkaConnectConstant.CONNECTOR_CLASS_FILED_NAME)), + connectCluster.getSuitableRequestUrl() + String.format(GET_PLUGIN_CONFIG_DESC_URI, props.getProperty(KafkaConnectConstant.CONNECTOR_CLASS_FILED_NAME)), props, ConfigInfos.class ); @@ -94,7 +94,7 @@ public Result> listPluginsFromCluster(Long connectClust // 通过参数检查接口,获取插件配置 List pluginList = restTool.getArrayObjectWithJsonContent( - connectCluster.getClusterUrl() + GET_ALL_PLUGINS_URI, + connectCluster.getSuitableRequestUrl() + GET_ALL_PLUGINS_URI, new HashMap<>(), ConnectPluginBasic.class ); diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/connect/worker/impl/WorkerConnectorServiceImpl.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/connect/worker/impl/WorkerConnectorServiceImpl.java index 99fb9ba21..eb2c80fc9 100644 --- a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/connect/worker/impl/WorkerConnectorServiceImpl.java +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/connect/worker/impl/WorkerConnectorServiceImpl.java @@ -105,7 +105,7 @@ public Result actionTask(TaskActionDTO dto) { return Result.buildFailure(ResultStatus.NOT_EXIST); } - String url = String.format(RESTART_TASK_URI, connectCluster.getClusterUrl(), dto.getConnectorName(), dto.getTaskId()); + String url = String.format(RESTART_TASK_URI, connectCluster.getSuitableRequestUrl(), dto.getConnectorName(), dto.getTaskId()); try { restTool.postObjectWithJsonContent(url, null, String.class); } catch (Exception e) { diff --git a/km-task/src/main/java/com/xiaojukeji/know/streaming/km/task/kafka/metadata/SyncConnectClusterAndWorkerTask.java b/km-task/src/main/java/com/xiaojukeji/know/streaming/km/task/kafka/metadata/SyncConnectClusterAndWorkerTask.java index cb886eea3..646bf6c0e 100644 --- a/km-task/src/main/java/com/xiaojukeji/know/streaming/km/task/kafka/metadata/SyncConnectClusterAndWorkerTask.java +++ b/km-task/src/main/java/com/xiaojukeji/know/streaming/km/task/kafka/metadata/SyncConnectClusterAndWorkerTask.java @@ -20,7 +20,6 @@ import com.xiaojukeji.know.streaming.km.common.enums.group.GroupTypeEnum; import com.xiaojukeji.know.streaming.km.common.enums.jmx.JmxEnum; import com.xiaojukeji.know.streaming.km.core.service.connect.cluster.ConnectClusterService; -import com.xiaojukeji.know.streaming.km.core.service.connect.worker.WorkerConnectorService; import com.xiaojukeji.know.streaming.km.core.service.connect.worker.WorkerService; import com.xiaojukeji.know.streaming.km.core.service.group.GroupService; import com.xiaojukeji.know.streaming.km.persistence.connect.cache.LoadedConnectClusterCache; @@ -47,9 +46,6 @@ public class SyncConnectClusterAndWorkerTask extends AbstractAsyncMetadataDispat @Autowired private WorkerService workerService; - @Autowired - private WorkerConnectorService workerConnectorService; - @Autowired private ConnectClusterService connectClusterService; @@ -60,7 +56,6 @@ public TaskResult processClusterTask(ClusterPhy clusterPhy, long triggerTimeUnit //获取connect集群 List groupList = groupService.listClusterGroups(clusterPhy.getId()).stream().filter(elem->elem.getType()==GroupTypeEnum.CONNECT_CLUSTER).collect(Collectors.toList()); for (Group group: groupList) { - try { KSGroupDescription ksGroupDescription = groupService.getGroupDescriptionFromKafka(clusterPhy, group.getName()); if (!ksGroupDescription.protocolType().equals(CONNECT_CLUSTER_PROTOCOL_TYPE)) { From bd58b48bcb34ec1e1fe3d54e32ed2df6e9c85908 Mon Sep 17 00:00:00 2001 From: EricZeng Date: Wed, 5 Jul 2023 13:43:19 +0800 Subject: [PATCH 32/37] =?UTF-8?q?[Optimize]Connector=E5=A2=9E=E6=94=B9?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E7=9A=84configs=E5=AD=97=E6=AE=B5=E5=90=8D?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=B8=BAconfig=20(#1080)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1、保持和原生一致; 2、当前是兼容状态,可同时支持configs和config; --- .../connector/impl/ConnectorManagerImpl.java | 8 +++---- .../mm2/impl/MirrorMakerManagerImpl.java | 18 +++++++-------- .../connect/connector/ConnectorCreateDTO.java | 18 ++++++++++----- .../dto/connect/mm2/MirrorMakerCreateDTO.java | 2 +- .../cluster/mm2/MirrorMakerBaseStateVO.java | 1 - .../v3/connect/KafkaConnectorController.java | 22 +++++++++++++++++-- 6 files changed, 46 insertions(+), 23 deletions(-) diff --git a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/connect/connector/impl/ConnectorManagerImpl.java b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/connect/connector/impl/ConnectorManagerImpl.java index 5800b26f0..191afc6bb 100644 --- a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/connect/connector/impl/ConnectorManagerImpl.java +++ b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/connect/connector/impl/ConnectorManagerImpl.java @@ -49,9 +49,9 @@ public Result updateConnectorConfig(Long connectClusterId, String connecto @Override public Result createConnector(ConnectorCreateDTO dto, String operator) { - dto.getConfigs().put(KafkaConnectConstant.MIRROR_MAKER_NAME_FIELD_NAME, dto.getConnectorName()); + dto.getSuitableConfig().put(KafkaConnectConstant.MIRROR_MAKER_NAME_FIELD_NAME, dto.getConnectorName()); - Result createResult = connectorService.createConnector(dto.getConnectClusterId(), dto.getConnectorName(), dto.getConfigs(), operator); + Result createResult = connectorService.createConnector(dto.getConnectClusterId(), dto.getConnectorName(), dto.getSuitableConfig(), operator); if (createResult.failed()) { return Result.buildFromIgnoreData(createResult); } @@ -67,9 +67,9 @@ public Result createConnector(ConnectorCreateDTO dto, String operator) { @Override public Result createConnector(ConnectorCreateDTO dto, String heartbeatName, String checkpointName, String operator) { - dto.getConfigs().put(KafkaConnectConstant.MIRROR_MAKER_NAME_FIELD_NAME, dto.getConnectorName()); + dto.getSuitableConfig().put(KafkaConnectConstant.MIRROR_MAKER_NAME_FIELD_NAME, dto.getConnectorName()); - Result createResult = connectorService.createConnector(dto.getConnectClusterId(), dto.getConnectorName(), dto.getConfigs(), operator); + Result createResult = connectorService.createConnector(dto.getConnectClusterId(), dto.getConnectorName(), dto.getSuitableConfig(), operator); if (createResult.failed()) { return Result.buildFromIgnoreData(createResult); } diff --git a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/connect/mm2/impl/MirrorMakerManagerImpl.java b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/connect/mm2/impl/MirrorMakerManagerImpl.java index 99f2747f2..de10b0f00 100644 --- a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/connect/mm2/impl/MirrorMakerManagerImpl.java +++ b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/connect/mm2/impl/MirrorMakerManagerImpl.java @@ -132,17 +132,17 @@ public Result createMirrorMaker(MirrorMakerCreateDTO dto, String operator) } else if (checkpointResult.failed() && checkpointResult.failed()) { return Result.buildFromRSAndMsg( ResultStatus.KAFKA_CONNECTOR_OPERATE_FAILED, - String.format("创建 checkpoint & heartbeat 失败.\n失败信息分别为:%s\n\n%s", checkpointResult.getMessage(), heartbeatResult.getMessage()) + String.format("创建 checkpoint & heartbeat 失败.%n失败信息分别为:%s%n%n%s", checkpointResult.getMessage(), heartbeatResult.getMessage()) ); } else if (checkpointResult.failed()) { return Result.buildFromRSAndMsg( ResultStatus.KAFKA_CONNECTOR_OPERATE_FAILED, - String.format("创建 checkpoint 失败.\n失败信息分别为:%s", checkpointResult.getMessage()) + String.format("创建 checkpoint 失败.%n失败信息分别为:%s", checkpointResult.getMessage()) ); } else{ return Result.buildFromRSAndMsg( ResultStatus.KAFKA_CONNECTOR_OPERATE_FAILED, - String.format("创建 heartbeat 失败.\n失败信息分别为:%s", heartbeatResult.getMessage()) + String.format("创建 heartbeat 失败.%n失败信息分别为:%s", heartbeatResult.getMessage()) ); } } @@ -194,7 +194,7 @@ public Result modifyMirrorMakerConfig(MirrorMakerCreateDTO dto, String ope return rv; } - return connectorService.updateConnectorConfig(dto.getConnectClusterId(), dto.getConnectorName(), dto.getConfigs(), operator); + return connectorService.updateConnectorConfig(dto.getConnectClusterId(), dto.getConnectorName(), dto.getSuitableConfig(), operator); } @Override @@ -426,7 +426,7 @@ public Result> getMM2Configs(Long connectClusterId, String conn public Result> validateConnectors(MirrorMakerCreateDTO dto) { List voList = new ArrayList<>(); - Result infoResult = pluginService.validateConfig(dto.getConnectClusterId(), dto.getConfigs()); + Result infoResult = pluginService.validateConfig(dto.getConnectClusterId(), dto.getSuitableConfig()); if (infoResult.failed()) { return Result.buildFromIgnoreData(infoResult); } @@ -480,11 +480,11 @@ public Result checkCreateMirrorMakerParamAndUnifyData(MirrorMakerCreateDTO return Result.buildFromRSAndMsg(ResultStatus.CLUSTER_NOT_EXIST, MsgConstant.getClusterPhyNotExist(connectCluster.getKafkaClusterPhyId())); } - if (!dto.getConfigs().containsKey(CONNECTOR_CLASS_FILED_NAME)) { + if (!dto.getSuitableConfig().containsKey(CONNECTOR_CLASS_FILED_NAME)) { return Result.buildFromRSAndMsg(ResultStatus.PARAM_ILLEGAL, "SourceConnector缺少connector.class"); } - if (!MIRROR_MAKER_SOURCE_CONNECTOR_TYPE.equals(dto.getConfigs().getProperty(CONNECTOR_CLASS_FILED_NAME))) { + if (!MIRROR_MAKER_SOURCE_CONNECTOR_TYPE.equals(dto.getSuitableConfig().getProperty(CONNECTOR_CLASS_FILED_NAME))) { return Result.buildFromRSAndMsg(ResultStatus.PARAM_ILLEGAL, "SourceConnector的connector.class类型错误"); } @@ -589,9 +589,7 @@ public static List supplyData2ClusterMirrorMakerOv } } - voList.forEach(elem -> { - elem.setMetricLines(metricLineMap.get(elem.getConnectClusterId() + "#" + elem.getConnectorName())); - }); + voList.forEach(elem -> elem.setMetricLines(metricLineMap.get(elem.getConnectClusterId() + "#" + elem.getConnectorName()))); return voList; } diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/dto/connect/connector/ConnectorCreateDTO.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/dto/connect/connector/ConnectorCreateDTO.java index 46639f0e4..038e617f4 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/dto/connect/connector/ConnectorCreateDTO.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/dto/connect/connector/ConnectorCreateDTO.java @@ -1,12 +1,12 @@ package com.xiaojukeji.know.streaming.km.common.bean.dto.connect.connector; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.xiaojukeji.know.streaming.km.common.bean.dto.connect.ClusterConnectorDTO; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.NoArgsConstructor; -import javax.validation.constraints.NotNull; import java.util.Properties; /** @@ -14,15 +14,23 @@ * @date 2022-10-17 */ @Data +@JsonIgnoreProperties(ignoreUnknown = true) @NoArgsConstructor @ApiModel(description = "创建Connector") public class ConnectorCreateDTO extends ClusterConnectorDTO { - @NotNull(message = "configs不允许为空") - @ApiModelProperty(value = "配置", example = "") + @Deprecated + @ApiModelProperty(value = "配置, 优先使用config字段,3.5.0版本将删除该字段", example = "") protected Properties configs; - public ConnectorCreateDTO(Long connectClusterId, String connectorName, Properties configs) { + @ApiModelProperty(value = "配置", example = "") + protected Properties config; + + public ConnectorCreateDTO(Long connectClusterId, String connectorName, Properties config) { super(connectClusterId, connectorName); - this.configs = configs; + this.config = config; + } + + public Properties getSuitableConfig() { + return config != null? config: configs; } } diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/dto/connect/mm2/MirrorMakerCreateDTO.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/dto/connect/mm2/MirrorMakerCreateDTO.java index fa9867ecb..c2a60dacc 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/dto/connect/mm2/MirrorMakerCreateDTO.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/dto/connect/mm2/MirrorMakerCreateDTO.java @@ -40,7 +40,7 @@ public void unifyData(Long sourceKafkaClusterId, String sourceBootstrapServers, targetKafkaProps = new Properties(); } - this.unifyData(this.configs, sourceKafkaClusterId, sourceBootstrapServers, sourceKafkaProps, targetKafkaClusterId, targetBootstrapServers, targetKafkaProps); + this.unifyData(this.getSuitableConfig(), sourceKafkaClusterId, sourceBootstrapServers, sourceKafkaProps, targetKafkaClusterId, targetBootstrapServers, targetKafkaProps); if (heartbeatConnectorConfigs != null) { this.unifyData(this.heartbeatConnectorConfigs, sourceKafkaClusterId, sourceBootstrapServers, sourceKafkaProps, targetKafkaClusterId, targetBootstrapServers, targetKafkaProps); diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/vo/cluster/mm2/MirrorMakerBaseStateVO.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/vo/cluster/mm2/MirrorMakerBaseStateVO.java index 04aed0356..33ae15daf 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/vo/cluster/mm2/MirrorMakerBaseStateVO.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/bean/vo/cluster/mm2/MirrorMakerBaseStateVO.java @@ -13,7 +13,6 @@ @Data @ApiModel(description = "集群MM2状态信息") public class MirrorMakerBaseStateVO extends BaseVO { - @ApiModelProperty(value = "worker数", example = "1") private Integer workerCount; diff --git a/km-rest/src/main/java/com/xiaojukeji/know/streaming/km/rest/api/v3/connect/KafkaConnectorController.java b/km-rest/src/main/java/com/xiaojukeji/know/streaming/km/rest/api/v3/connect/KafkaConnectorController.java index d60314bbb..b03ca7cce 100644 --- a/km-rest/src/main/java/com/xiaojukeji/know/streaming/km/rest/api/v3/connect/KafkaConnectorController.java +++ b/km-rest/src/main/java/com/xiaojukeji/know/streaming/km/rest/api/v3/connect/KafkaConnectorController.java @@ -14,6 +14,7 @@ import com.xiaojukeji.know.streaming.km.common.constant.Constant; import com.xiaojukeji.know.streaming.km.common.enums.connect.ConnectActionEnum; import com.xiaojukeji.know.streaming.km.common.utils.ConvertUtil; +import com.xiaojukeji.know.streaming.km.common.utils.ValidateUtils; import com.xiaojukeji.know.streaming.km.core.service.connect.connector.ConnectorService; import com.xiaojukeji.know.streaming.km.core.service.connect.plugin.PluginService; import io.swagger.annotations.Api; @@ -44,6 +45,10 @@ public class KafkaConnectorController { @PostMapping(value = "connectors") @ResponseBody public Result createConnector(@Validated @RequestBody ConnectorCreateDTO dto) { + if (ValidateUtils.isNull(dto.getSuitableConfig())) { + return Result.buildFromRSAndMsg(ResultStatus.PARAM_ILLEGAL, "config字段不能为空"); + } + return connectorManager.createConnector(dto, HttpRequestUtil.getOperator()); } @@ -73,14 +78,27 @@ public Result operateConnectors(@Validated @RequestBody ConnectorActionDTO @PutMapping(value ="connectors-config") @ResponseBody public Result modifyConnectors(@Validated @RequestBody ConnectorCreateDTO dto) { - return connectorManager.updateConnectorConfig(dto.getConnectClusterId(), dto.getConnectorName(), dto.getConfigs(), HttpRequestUtil.getOperator()); + if (ValidateUtils.isNull(dto.getSuitableConfig())) { + return Result.buildFromRSAndMsg(ResultStatus.PARAM_ILLEGAL, "config字段不能为空"); + } + + return connectorManager.updateConnectorConfig( + dto.getConnectClusterId(), + dto.getConnectorName(), + dto.getSuitableConfig(), + HttpRequestUtil.getOperator() + ); } @ApiOperation(value = "校验Connector配置", notes = "") @PutMapping(value ="connectors-config/validate") @ResponseBody public Result validateConnectors(@Validated @RequestBody ConnectorCreateDTO dto) { - Result infoResult = pluginService.validateConfig(dto.getConnectClusterId(), dto.getConfigs()); + if (ValidateUtils.isNull(dto.getSuitableConfig())) { + return Result.buildFromRSAndMsg(ResultStatus.PARAM_ILLEGAL, "config字段不能为空"); + } + + Result infoResult = pluginService.validateConfig(dto.getConnectClusterId(), dto.getSuitableConfig()); if (infoResult.failed()) { return Result.buildFromIgnoreData(infoResult); } From 470e471cad08a761847ffbbbdc606131ab6991a7 Mon Sep 17 00:00:00 2001 From: EricZeng Date: Wed, 5 Jul 2023 16:12:12 +0800 Subject: [PATCH 33/37] =?UTF-8?q?=C2=A0=E6=96=B0=E5=A2=9Ebuild-all.yml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-all.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/build-all.yml diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml new file mode 100644 index 000000000..5a380619c --- /dev/null +++ b/.github/workflows/build-all.yml @@ -0,0 +1,28 @@ +name: Build All + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + cache: maven + - name: Setup Node + uses: actions/setup-node@v1 + with: + node-version: '12.22.12' + - name: Build with Maven + run: mvn -U clean package -Dmaven.test.skip=true + From 0a6e9b7633abe611e150935fc64650f91e9d79ee Mon Sep 17 00:00:00 2001 From: EricZeng Date: Thu, 6 Jul 2023 15:39:51 +0800 Subject: [PATCH 34/37] =?UTF-8?q?[Optimize]Jmx=E7=9B=B8=E5=85=B3=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E4=BC=98=E5=8C=96=20(#1082)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1、统一Jmx客户端相关日志格式; 2、增加创建的Jmx-Connector时,所使用的信息; 3、优化日志级别; --- .../streaming/km/common/jmx/JmxConnectorWrap.java | 12 +++++++++++- .../km/persistence/connect/ConnectJMXClient.java | 11 +++++++---- .../km/persistence/kafka/KafkaJMXClient.java | 4 ++-- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/jmx/JmxConnectorWrap.java b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/jmx/JmxConnectorWrap.java index 071072ade..366fe9478 100644 --- a/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/jmx/JmxConnectorWrap.java +++ b/km-common/src/main/java/com/xiaojukeji/know/streaming/km/common/jmx/JmxConnectorWrap.java @@ -46,7 +46,12 @@ public class JmxConnectorWrap { private JmxAuthConfig jmxConfig; public JmxConnectorWrap(String clientLogIdent, Long brokerStartupTime, String jmxHost, Integer jmxPort, JmxAuthConfig jmxConfig) { - this.clientLogIdent=clientLogIdent; + LOGGER.info( + "method=JmxConnectorWrap||clientLogIdent={}||brokerStartupTime={}||jmxHost={}||jmxPort={}||jmxConfig={}||msg=start construct JmxWrap.", + clientLogIdent, brokerStartupTime, jmxHost, jmxPort, jmxConfig + ); + + this.clientLogIdent = clientLogIdent; this.brokerStartupTime = brokerStartupTime; this.jmxHost = jmxHost; this.jmxPort = (jmxPort == null? JmxEnum.UNKNOWN.getPort() : jmxPort); @@ -160,6 +165,11 @@ private synchronized boolean createJmxConnector() { if (jmxConnector != null) { return true; } + LOGGER.info( + "method=createJmxConnector||clientLogIdent={}||brokerStartupTime={}||jmxHost={}||jmxPort={}||jmxConfig={}||msg=start create jmx connector.", + clientLogIdent, brokerStartupTime, jmxHost, jmxPort, jmxConfig + ); + String jmxUrl = String.format("service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi", jmxHost, jmxPort); try { Map environment = new HashMap(); diff --git a/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/connect/ConnectJMXClient.java b/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/connect/ConnectJMXClient.java index 300243b7c..ea7e38c5c 100644 --- a/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/connect/ConnectJMXClient.java +++ b/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/connect/ConnectJMXClient.java @@ -78,16 +78,15 @@ private JmxConnectorWrap createJmxConnectorWrap(ConnectCluster connectCluster, S return jmxConnectorWrap; } - log.debug("method=createJmxConnectorWrap||connectClusterId={}||workerId={}||msg=create JmxConnectorWrap starting", connectCluster.getId(), workerId); + log.info("method=createJmxConnectorWrap||connectClusterId={}||workerId={}||msg=create JmxConnectorWrap starting", connectCluster.getId(), workerId); JmxConfig jmxConfig = ConvertUtil.str2ObjByJson(connectCluster.getJmxProperties(), JmxConfig.class); if (jmxConfig == null) { jmxConfig = new JmxConfig(); } - jmxConnectorWrap = new JmxConnectorWrap( - "connectClusterId: " + connectCluster.getId() + " workerId: " + workerId, + String.format("clusterPhyId=%s,workerId=%s", connectCluster.getId(), workerId), null, connectWorker.getHost(), jmxConfig.getFinallyJmxPort(workerId, connectWorker.getJmxPort()), @@ -97,12 +96,16 @@ private JmxConnectorWrap createJmxConnectorWrap(ConnectCluster connectCluster, S Map workerMap = JMX_MAP.getOrDefault(connectCluster.getId(), new ConcurrentHashMap<>()); workerMap.put(workerId, jmxConnectorWrap); JMX_MAP.put(connectCluster.getId(), workerMap); + + log.info("method=createJmxConnectorWrap||clusterPhyId={}||workerId={}||msg=create JmxConnectorWrap success", connectCluster.getId(), workerId); + return jmxConnectorWrap; } catch (Exception e) { - log.debug("method=createJmxConnectorWrap||connectClusterId={}||workerId={}||msg=create JmxConnectorWrap failed||errMsg=exception||", connectCluster.getId(), workerId, e); + log.error("method=createJmxConnectorWrap||connectClusterId={}||workerId={}||msg=create JmxConnectorWrap failed||errMsg=exception||", connectCluster.getId(), workerId, e); } finally { modifyClientMapLock.unlock(); } + return null; } diff --git a/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/kafka/KafkaJMXClient.java b/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/kafka/KafkaJMXClient.java index e759da609..8c8ca9cd1 100644 --- a/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/kafka/KafkaJMXClient.java +++ b/km-persistence/src/main/java/com/xiaojukeji/know/streaming/km/persistence/kafka/KafkaJMXClient.java @@ -151,7 +151,7 @@ private JmxConnectorWrap createJmxConnectorWrap(ClusterPhy clusterPhy, Integer b return jmxMap; } - log.debug("method=createJmxConnectorWrap||clusterPhyId={}||brokerId={}||msg=create JmxConnectorWrap starting", clusterPhy.getId(), brokerId); + log.info("method=createJmxConnectorWrap||clusterPhyId={}||brokerId={}||msg=create JmxConnectorWrap starting", clusterPhy.getId(), brokerId); JmxConfig jmxConfig = ConvertUtil.str2ObjByJson(clusterPhy.getJmxProperties(), JmxConfig.class); if (jmxConfig == null) { @@ -159,7 +159,7 @@ private JmxConnectorWrap createJmxConnectorWrap(ClusterPhy clusterPhy, Integer b } JmxConnectorWrap jmxConnectorWrap = new JmxConnectorWrap( - "clusterPhyId: " + clusterPhy.getId() + " brokerId: " + brokerId, + String.format("clusterPhyId=%s,brokerId=%d", clusterPhy.getId(), brokerId), broker.getStartTimestamp(), broker.getJmxHost(jmxConfig.getUseWhichEndpoint()), jmxConfig.getFinallyJmxPort(String.valueOf(brokerId), broker.getJmxPort()), From dd3dcd37e9e294ea341dfa131c48d8a2490609ac Mon Sep 17 00:00:00 2001 From: EricZeng Date: Thu, 6 Jul 2023 15:42:18 +0800 Subject: [PATCH 35/37] =?UTF-8?q?[Feature]=E6=96=B0=E5=A2=9EGroup=E5=8F=8A?= =?UTF-8?q?GroupOffset=E5=88=A0=E9=99=A4=E5=8A=9F=E8=83=BDPart2=20(#1084)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1、修复版本控制错误的问题; 2、增加相关权限点; PS:仅后端代码,前端待补充。 --- ...07\347\272\247\346\211\213\345\206\214.md" | 9 ++ .../km/biz/group/impl/GroupManagerImpl.java | 6 +- .../km/core/service/group/OpGroupService.java | 6 +- .../group/impl/OpGroupServiceImpl.java | 129 ++++++++++-------- .../fe/FrontEndControlVersionItems.java | 14 +- .../src/main/resources/sql/dml-logi.sql | 10 ++ 6 files changed, 111 insertions(+), 63 deletions(-) diff --git "a/docs/install_guide/\347\211\210\346\234\254\345\215\207\347\272\247\346\211\213\345\206\214.md" "b/docs/install_guide/\347\211\210\346\234\254\345\215\207\347\272\247\346\211\213\345\206\214.md" index ba8fa8d88..675a9dabd 100644 --- "a/docs/install_guide/\347\211\210\346\234\254\345\215\207\347\272\247\346\211\213\345\206\214.md" +++ "b/docs/install_guide/\347\211\210\346\234\254\345\215\207\347\272\247\346\211\213\345\206\214.md" @@ -44,6 +44,15 @@ INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_del INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2042', '0', 'know-streaming'); INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2044', '0', 'know-streaming'); + +-- 多集群管理权限2023-07-06新增 +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2046', 'Group-删除', '1593', '1', '2', 'Group-删除', '0', 'know-streaming'); +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2048', 'GroupOffset-Topic纬度删除', '1593', '1', '2', 'GroupOffset-Topic纬度删除', '0', 'know-streaming'); +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2050', 'GroupOffset-Partition纬度删除', '1593', '1', '2', 'GroupOffset-Partition纬度删除', '0', 'know-streaming'); + +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2046', '0', 'know-streaming'); +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2048', '0', 'know-streaming'); +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2050', '0', 'know-streaming'); ``` ### 升级至 `3.3.0` 版本 diff --git a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/impl/GroupManagerImpl.java b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/impl/GroupManagerImpl.java index 1f37c368c..753768dfc 100644 --- a/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/impl/GroupManagerImpl.java +++ b/km-biz/src/main/java/com/xiaojukeji/know/streaming/km/biz/group/impl/GroupManagerImpl.java @@ -151,7 +151,7 @@ public PaginationResult pagingClusterGroupsOverview(Long cluste List groupList = groupService.listClusterGroups(clusterPhyId); // 类型转化 - List voList = groupList.stream().map(elem -> GroupConverter.convert2GroupOverviewVO(elem)).collect(Collectors.toList()); + List voList = groupList.stream().map(GroupConverter::convert2GroupOverviewVO).collect(Collectors.toList()); // 搜索groupName voList = PaginationUtil.pageByFuzzyFilter(voList, dto.getSearchGroupName(), Arrays.asList("name")); @@ -301,7 +301,7 @@ public Result deleteGroupOffsets(GroupOffsetDeleteDTO dto, String operator return Result.buildFromRSAndMsg(ResultStatus.PARAM_ILLEGAL, "topicName不允许为空"); } if (DeleteGroupTypeEnum.GROUP_TOPIC.getCode().equals(dto.getDeleteType())) { - return opGroupService.deleteGroupOffset( + return opGroupService.deleteGroupTopicOffset( new DeleteGroupTopicParam(dto.getClusterPhyId(), dto.getGroupName(), DeleteGroupTypeEnum.GROUP, dto.getTopicName()), operator ); @@ -313,7 +313,7 @@ public Result deleteGroupOffsets(GroupOffsetDeleteDTO dto, String operator return Result.buildFromRSAndMsg(ResultStatus.PARAM_ILLEGAL, "partitionId不允许为空或小于0"); } if (DeleteGroupTypeEnum.GROUP_TOPIC_PARTITION.getCode().equals(dto.getDeleteType())) { - return opGroupService.deleteGroupOffset( + return opGroupService.deleteGroupTopicPartitionOffset( new DeleteGroupTopicPartitionParam(dto.getClusterPhyId(), dto.getGroupName(), DeleteGroupTypeEnum.GROUP, dto.getTopicName(), dto.getPartitionId()), operator ); diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/OpGroupService.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/OpGroupService.java index 83285a488..dbd6bae5d 100644 --- a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/OpGroupService.java +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/OpGroupService.java @@ -1,11 +1,15 @@ package com.xiaojukeji.know.streaming.km.core.service.group; import com.xiaojukeji.know.streaming.km.common.bean.entity.param.group.DeleteGroupParam; +import com.xiaojukeji.know.streaming.km.common.bean.entity.param.group.DeleteGroupTopicParam; +import com.xiaojukeji.know.streaming.km.common.bean.entity.param.group.DeleteGroupTopicPartitionParam; import com.xiaojukeji.know.streaming.km.common.bean.entity.result.Result; public interface OpGroupService { /** - * 删除Topic + * 删除Offset */ Result deleteGroupOffset(DeleteGroupParam param, String operator); + Result deleteGroupTopicOffset(DeleteGroupTopicParam param, String operator); + Result deleteGroupTopicPartitionOffset(DeleteGroupTopicPartitionParam param, String operator); } diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/impl/OpGroupServiceImpl.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/impl/OpGroupServiceImpl.java index b59519b94..e82c36a9c 100644 --- a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/impl/OpGroupServiceImpl.java +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/group/impl/OpGroupServiceImpl.java @@ -13,7 +13,6 @@ import com.xiaojukeji.know.streaming.km.common.bean.po.group.GroupMemberPO; import com.xiaojukeji.know.streaming.km.common.bean.po.group.GroupPO; import com.xiaojukeji.know.streaming.km.common.constant.KafkaConstant; -import com.xiaojukeji.know.streaming.km.common.enums.group.DeleteGroupTypeEnum; import com.xiaojukeji.know.streaming.km.common.enums.operaterecord.ModuleEnum; import com.xiaojukeji.know.streaming.km.common.enums.operaterecord.OperationEnum; import com.xiaojukeji.know.streaming.km.common.enums.version.VersionItemTypeEnum; @@ -46,6 +45,8 @@ public class OpGroupServiceImpl extends BaseKafkaVersionControlService implement private static final ILog LOGGER = LogFactory.getLog(OpGroupServiceImpl.class); private static final String DELETE_GROUP_OFFSET = "deleteGroupOffset"; + private static final String DELETE_GROUP_TOPIC_OFFSET = "deleteGroupTopicOffset"; + private static final String DELETE_GROUP_TP_OFFSET = "deleteGroupTopicPartitionOffset"; @Autowired private GroupDAO groupDAO; @@ -66,7 +67,9 @@ protected VersionItemTypeEnum getVersionItemType() { @PostConstruct private void init() { - registerVCHandler(DELETE_GROUP_OFFSET, V_1_1_0, V_MAX, "deleteGroupOffset", this::deleteGroupOffsetByClient); + registerVCHandler(DELETE_GROUP_OFFSET, V_2_0_0, V_MAX, "deleteGroupOffsetByClient", this::deleteGroupOffsetByClient); + registerVCHandler(DELETE_GROUP_TOPIC_OFFSET, V_2_4_0, V_MAX, "deleteGroupTopicOffsetByClient", this::deleteGroupTopicOffsetByClient); + registerVCHandler(DELETE_GROUP_TP_OFFSET, V_2_4_0, V_MAX, "deleteGroupTopicPartitionOffsetByClient", this::deleteGroupTopicPartitionOffsetByClient); } @Override @@ -80,46 +83,18 @@ public Result deleteGroupOffset(DeleteGroupParam param, String operator) { return rv; } - // 清理数据库中的数据 - if (DeleteGroupTypeEnum.GROUP.equals(param.getDeleteGroupTypeEnum())) { - // 记录操作 - OplogDTO oplogDTO = new OplogDTO(operator, - OperationEnum.DELETE.getDesc(), - ModuleEnum.KAFKA_GROUP.getDesc(), - String.format("集群ID:[%d] Group名称:[%s]", param.getClusterPhyId(), param.getGroupName()), - String.format("删除Offset:[%s]", ConvertUtil.obj2Json(param)) - ); - opLogWrapService.saveOplogAndIgnoreException(oplogDTO); - - // 清理Group数据 - this.deleteGroupInDB(param.getClusterPhyId(), param.getGroupName()); - this.deleteGroupMemberInDB(param.getClusterPhyId(), param.getGroupName()); - } else if (DeleteGroupTypeEnum.GROUP_TOPIC.equals(param.getDeleteGroupTypeEnum())) { - // 记录操作 - DeleteGroupTopicParam topicParam = (DeleteGroupTopicParam) param; - OplogDTO oplogDTO = new OplogDTO(operator, - OperationEnum.DELETE.getDesc(), - ModuleEnum.KAFKA_GROUP.getDesc(), - String.format("集群ID:[%d] Group名称:[%s] Topic名称:[%s]", param.getClusterPhyId(), param.getGroupName(), topicParam.getTopicName()), - String.format("删除Offset:[%s]", ConvertUtil.obj2Json(topicParam)) - ); - opLogWrapService.saveOplogAndIgnoreException(oplogDTO); - - // 清理group + topic 数据 - this.deleteGroupMemberInDB(topicParam.getClusterPhyId(), topicParam.getGroupName(), topicParam.getTopicName()); - } else if (DeleteGroupTypeEnum.GROUP_TOPIC_PARTITION.equals(param.getDeleteGroupTypeEnum())) { - // 记录操作 - DeleteGroupTopicPartitionParam partitionParam = (DeleteGroupTopicPartitionParam) param; - OplogDTO oplogDTO = new OplogDTO(operator, - OperationEnum.DELETE.getDesc(), - ModuleEnum.KAFKA_GROUP.getDesc(), - String.format("集群ID:[%d] Group名称:[%s] Topic名称:[%s] PartitionID:[%d]", param.getClusterPhyId(), param.getGroupName(), partitionParam.getTopicName(), partitionParam.getPartitionId()), - String.format("删除Offset:[%s]", ConvertUtil.obj2Json(partitionParam)) - ); - opLogWrapService.saveOplogAndIgnoreException(oplogDTO); - - // 不需要进行清理 - } + // 记录操作 + OplogDTO oplogDTO = new OplogDTO(operator, + OperationEnum.DELETE.getDesc(), + ModuleEnum.KAFKA_GROUP.getDesc(), + String.format("集群ID:[%d] Group名称:[%s]", param.getClusterPhyId(), param.getGroupName()), + String.format("删除Offset:[%s]", ConvertUtil.obj2Json(param)) + ); + opLogWrapService.saveOplogAndIgnoreException(oplogDTO); + + // 清理Group数据 + this.deleteGroupInDB(param.getClusterPhyId(), param.getGroupName()); + this.deleteGroupMemberInDB(param.getClusterPhyId(), param.getGroupName()); return rv; } catch (VCHandlerNotExistException e) { @@ -127,23 +102,65 @@ public Result deleteGroupOffset(DeleteGroupParam param, String operator) { } } - /**************************************************** private method ****************************************************/ + @Override + public Result deleteGroupTopicOffset(DeleteGroupTopicParam param, String operator) { + // 日志记录 + LOGGER.info("method=deleteGroupTopicOffset||param={}||operator={}||msg=delete group topic offset", ConvertUtil.obj2Json(param), operator); - private Result deleteGroupOffsetByClient(VersionItemParam itemParam) { - DeleteGroupParam deleteGroupParam = (DeleteGroupParam) itemParam; - - if (DeleteGroupTypeEnum.GROUP.equals(deleteGroupParam.getDeleteGroupTypeEnum())) { - return this.deleteGroupByClient(itemParam); - } else if (DeleteGroupTypeEnum.GROUP_TOPIC.equals(deleteGroupParam.getDeleteGroupTypeEnum())) { - return this.deleteGroupTopicOffsetByClient(itemParam); - } else if (DeleteGroupTypeEnum.GROUP_TOPIC_PARTITION.equals(deleteGroupParam.getDeleteGroupTypeEnum())) { - return this.deleteGroupTopicPartitionOffsetByClient(itemParam); + try { + Result rv = (Result) doVCHandler(param.getClusterPhyId(), DELETE_GROUP_TOPIC_OFFSET, param); + if (rv == null || rv.failed()) { + return rv; + } + + // 清理数据库中的数据 + // 记录操作 + OplogDTO oplogDTO = new OplogDTO(operator, + OperationEnum.DELETE.getDesc(), + ModuleEnum.KAFKA_GROUP.getDesc(), + String.format("集群ID:[%d] Group名称:[%s] Topic名称:[%s]", param.getClusterPhyId(), param.getGroupName(), param.getTopicName()), + String.format("删除Offset:[%s]", ConvertUtil.obj2Json(param)) + ); + opLogWrapService.saveOplogAndIgnoreException(oplogDTO); + + // 清理group + topic 数据 + this.deleteGroupMemberInDB(param.getClusterPhyId(), param.getGroupName(), param.getTopicName()); + + return rv; + } catch (VCHandlerNotExistException e) { + return Result.buildFailure(VC_HANDLE_NOT_EXIST); } + } + + @Override + public Result deleteGroupTopicPartitionOffset(DeleteGroupTopicPartitionParam param, String operator) { + // 日志记录 + LOGGER.info("method=deleteGroupTopicPartitionOffset||param={}||operator={}||msg=delete group topic partition offset", ConvertUtil.obj2Json(param), operator); + + try { + Result rv = (Result) doVCHandler(param.getClusterPhyId(), DELETE_GROUP_TP_OFFSET, param); + if (rv == null || rv.failed()) { + return rv; + } - return Result.buildFromRSAndMsg(ResultStatus.PARAM_ILLEGAL, "删除Offset时,删除的类型参数非法"); + // 记录操作 + OplogDTO oplogDTO = new OplogDTO(operator, + OperationEnum.DELETE.getDesc(), + ModuleEnum.KAFKA_GROUP.getDesc(), + String.format("集群ID:[%d] Group名称:[%s] Topic名称:[%s] PartitionID:[%d]", param.getClusterPhyId(), param.getGroupName(), param.getTopicName(), param.getPartitionId()), + String.format("删除Offset:[%s]", ConvertUtil.obj2Json(param)) + ); + opLogWrapService.saveOplogAndIgnoreException(oplogDTO); + + return rv; + } catch (VCHandlerNotExistException e) { + return Result.buildFailure(VC_HANDLE_NOT_EXIST); + } } - private Result deleteGroupByClient(VersionItemParam itemParam) { + /**************************************************** private method ****************************************************/ + + private Result deleteGroupOffsetByClient(VersionItemParam itemParam) { DeleteGroupParam param = (DeleteGroupParam) itemParam; try { AdminClient adminClient = kafkaAdminClient.getClient(param.getClusterPhyId()); @@ -156,7 +173,7 @@ private Result deleteGroupByClient(VersionItemParam itemParam) { deleteConsumerGroupsResult.all().get(); } catch (Exception e) { LOGGER.error( - "method=deleteGroupByClient||clusterPhyId={}||groupName={}||errMsg=delete group failed||msg=exception!", + "method=deleteGroupOffsetByClient||clusterPhyId={}||groupName={}||errMsg=delete group failed||msg=exception!", param.getClusterPhyId(), param.getGroupName(), e ); @@ -172,7 +189,7 @@ private Result deleteGroupTopicOffsetByClient(VersionItemParam itemParam) AdminClient adminClient = kafkaAdminClient.getClient(param.getClusterPhyId()); DescribeTopicsResult describeTopicsResult = adminClient.describeTopics(Collections.singletonList( - param.getTopicName()), + param.getTopicName()), new DescribeTopicsOptions().timeoutMs(KafkaConstant.ADMIN_CLIENT_REQUEST_TIME_OUT_UNIT_MS) ); diff --git a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/version/fe/FrontEndControlVersionItems.java b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/version/fe/FrontEndControlVersionItems.java index a56b9b0e7..18cdb44e1 100644 --- a/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/version/fe/FrontEndControlVersionItems.java +++ b/km-core/src/main/java/com/xiaojukeji/know/streaming/km/core/service/version/fe/FrontEndControlVersionItems.java @@ -39,8 +39,12 @@ public class FrontEndControlVersionItems extends BaseMetricVersionMetric { private static final String FE_TRUNCATE_TOPIC = "FETruncateTopic"; private static final String FE_DELETE_GROUP_OFFSET = "FEDeleteGroupOffset"; + private static final String FE_DELETE_GROUP_TOPIC_OFFSET = "FEDeleteGroupTopicOffset"; + private static final String FE_DELETE_GROUP_TOPIC_PARTITION_OFFSET = "FEDeleteGroupTopicPartitionOffset"; - public FrontEndControlVersionItems(){} + public FrontEndControlVersionItems() { + // ignore + } @Override public int versionItemType() { @@ -97,9 +101,13 @@ public List init(){ itemList.add(buildItem().minVersion(VersionEnum.V_0_11_0_0).maxVersion(VersionEnum.V_MAX) .name(FE_TRUNCATE_TOPIC).desc("清空Topic")); - // truncate topic - itemList.add(buildItem().minVersion(VersionEnum.V_1_1_0).maxVersion(VersionEnum.V_MAX) + // 删除Offset + itemList.add(buildItem().minVersion(VersionEnum.V_2_0_0).maxVersion(VersionEnum.V_MAX) .name(FE_DELETE_GROUP_OFFSET).desc("删除GroupOffset")); + itemList.add(buildItem().minVersion(VersionEnum.V_2_4_0).maxVersion(VersionEnum.V_MAX) + .name(FE_DELETE_GROUP_TOPIC_OFFSET).desc("删除GroupTopicOffset")); + itemList.add(buildItem().minVersion(VersionEnum.V_2_4_0).maxVersion(VersionEnum.V_MAX) + .name(FE_DELETE_GROUP_TOPIC_PARTITION_OFFSET).desc("删除GroupTopicPartitionOffset")); return itemList; } } diff --git a/km-persistence/src/main/resources/sql/dml-logi.sql b/km-persistence/src/main/resources/sql/dml-logi.sql index b19c9410c..2beff22e1 100644 --- a/km-persistence/src/main/resources/sql/dml-logi.sql +++ b/km-persistence/src/main/resources/sql/dml-logi.sql @@ -147,3 +147,13 @@ INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_del INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2040', '0', 'know-streaming'); INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2042', '0', 'know-streaming'); INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2044', '0', 'know-streaming'); + + +-- 多集群管理权限2023-07-06新增 +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2046', 'Group-删除', '1593', '1', '2', 'Group-删除', '0', 'know-streaming'); +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2048', 'GroupOffset-Topic纬度删除', '1593', '1', '2', 'GroupOffset-Topic纬度删除', '0', 'know-streaming'); +INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2050', 'GroupOffset-Partition纬度删除', '1593', '1', '2', 'GroupOffset-Partition纬度删除', '0', 'know-streaming'); + +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2046', '0', 'know-streaming'); +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2048', '0', 'know-streaming'); +INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2050', '0', 'know-streaming'); From 94b1e508fd12a5286c4d5ef54c5a35e9bd479022 Mon Sep 17 00:00:00 2001 From: EricZeng Date: Fri, 7 Jul 2023 12:36:50 +0800 Subject: [PATCH 36/37] =?UTF-8?q?Build=E4=B9=8B=E5=90=8E=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1、修改文件名; 2、Build之后生成安装包,方便用户直接下载使用; --- .github/workflows/build-all.yml | 28 --------------------- .github/workflows/ci_build.yml | 43 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 28 deletions(-) delete mode 100644 .github/workflows/build-all.yml create mode 100644 .github/workflows/ci_build.yml diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml deleted file mode 100644 index 5a380619c..000000000 --- a/.github/workflows/build-all.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Build All - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Set up JDK 11 - uses: actions/setup-java@v3 - with: - java-version: '11' - distribution: 'temurin' - cache: maven - - name: Setup Node - uses: actions/setup-node@v1 - with: - node-version: '12.22.12' - - name: Build with Maven - run: mvn -U clean package -Dmaven.test.skip=true - diff --git a/.github/workflows/ci_build.yml b/.github/workflows/ci_build.yml new file mode 100644 index 000000000..0476a94a7 --- /dev/null +++ b/.github/workflows/ci_build.yml @@ -0,0 +1,43 @@ +name: KnowStreaming Build + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + cache: maven + + - name: Setup Node + uses: actions/setup-node@v1 + with: + node-version: '12.22.12' + + - name: Build With Maven + run: mvn -Prelease-package -Dmaven.test.skip=true clean install -U + + - name: Get KnowStreaming Version + if: ${{ success() }} + run: | + version=`mvn -Dexec.executable='echo' -Dexec.args='${project.version}' --non-recursive exec:exec -q` + echo "VERSION=${version}" >> $GITHUB_ENV + + - name: Upload Binary Package + if: ${{ success() }} + uses: actions/upload-artifact@v3 + with: + name: KnowStreaming-${{ env.VERSION }}.tar.gz + path: km-dist/target/KnowStreaming-${{ env.VERSION }}.tar.gz From 42195c3180060037c6eaf2b64cf94dbc5a1361d9 Mon Sep 17 00:00:00 2001 From: EricZeng Date: Fri, 7 Jul 2023 12:45:27 +0800 Subject: [PATCH 37/37] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AF=B9=E4=BC=81?= =?UTF-8?q?=E4=B8=9A=E7=89=88&=E4=BD=93=E9=AA=8C=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E5=88=86=E6=94=AF=E7=9A=84CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci_build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci_build.yml b/.github/workflows/ci_build.yml index 0476a94a7..073d46522 100644 --- a/.github/workflows/ci_build.yml +++ b/.github/workflows/ci_build.yml @@ -2,9 +2,9 @@ name: KnowStreaming Build on: push: - branches: [ "master" ] + branches: [ "master", "ve_3.x", "ve_demo_3.x" ] pull_request: - branches: [ "master" ] + branches: [ "master", "ve_3.x", "ve_demo_3.x" ] jobs: build: