Skip to content

Commit 9ce9ccd

Browse files
committed
Flink集群Slot校验工具类, 用于在部署Flink Job之前校验集群是否有足够的可用slot
1 parent b9bcd7a commit 9ce9ccd

10 files changed

Lines changed: 272 additions & 83 deletions

File tree

tis-incr/tis-realtime-flink/src/main/java/com/qlangtech/plugins/incr/flink/alert/AddMonitorForEvents.java

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package com.qlangtech.plugins.incr.flink.alert;
22

33
import com.alibaba.citrus.turbine.Context;
4+
import com.alibaba.fastjson.JSONObject;
45
import com.google.common.collect.Sets;
56
import com.qlangtech.tis.config.ParamsConfig;
67
import com.qlangtech.tis.datax.DefaultDataXProcessorManipulate;
8+
import com.qlangtech.tis.datax.IManipulateStatus;
9+
import com.qlangtech.tis.datax.TimeFormat;
10+
import com.qlangtech.tis.extension.Descriptor;
711
import com.qlangtech.tis.extension.TISExtension;
812
import com.qlangtech.tis.plugin.IEndTypeGetter;
13+
import com.qlangtech.tis.plugin.IdentityDesc;
914
import com.qlangtech.tis.plugin.IdentityName;
1015
import com.qlangtech.tis.plugin.alert.AlertChannel;
1116
import com.qlangtech.tis.plugin.annotation.FormField;
@@ -15,6 +20,7 @@
1520
import com.qlangtech.tis.runtime.module.misc.IFieldErrorHandler;
1621
import com.qlangtech.tis.util.IPluginContext;
1722

23+
import java.util.Collections;
1824
import java.util.List;
1925
import java.util.Optional;
2026

@@ -24,7 +30,8 @@
2430
* @author 百岁 (baisui@qlangtech.com)
2531
* @date 2025/11/17
2632
*/
27-
public class AddMonitorForEvents extends DefaultDataXProcessorManipulate implements IdentityName, DefaultDataXProcessorManipulate.MonitorForEventsManager {
33+
public class AddMonitorForEvents extends DefaultDataXProcessorManipulate
34+
implements IdentityName, DefaultDataXProcessorManipulate.MonitorForEventsManager, IManipulateStatus, IdentityDesc<JSONObject> {
2835

2936
private static final String KEY_ALERT_CHANNEL = "alertChannel";
3037
/**
@@ -33,6 +40,9 @@ public class AddMonitorForEvents extends DefaultDataXProcessorManipulate impleme
3340
@FormField(ordinal = 2, type = FormFieldType.ENUM, validate = {Validator.require})
3441
public Boolean turnOn;
3542

43+
private transient int alertSendCount;
44+
private transient long lastSendTimestamp = -1;
45+
3646
/**
3747
* 选择发送渠道
3848
*/
@@ -44,6 +54,36 @@ public boolean isActivate() {
4454
return this.turnOn;
4555
}
4656

57+
@Override
58+
public JSONObject describePlugin() {
59+
return Descriptor.getManipulateMeta(false, this);
60+
}
61+
62+
/**
63+
* 在前端tag中可以显示执行状态
64+
*
65+
* @return
66+
*/
67+
@Override
68+
public ManipulateStateSummary manipulateStatusSummary() {
69+
final StringBuilder summary = new StringBuilder("当前处于‘" + (turnOn ? "激活" : "禁用") + "’状态");
70+
if (turnOn) {
71+
summary.append(",已经向消息通道发送").append(this.alertSendCount).append("条报警消息");
72+
if (this.lastSendTimestamp > 0) {
73+
summary.append(",最近发送时间:").append(TimeFormat.yyyyMMdd_HH_mm_ss.format(this.lastSendTimestamp));
74+
}
75+
}
76+
return new ManipulateStateSummary(
77+
Collections.singletonList(IManipulateStatus.create(String.valueOf(this.alertSendCount)))
78+
, summary.toString(), turnOn);
79+
}
80+
81+
@Override
82+
public void addSendCount() {
83+
alertSendCount++;
84+
this.lastSendTimestamp = TimeFormat.getCurrentTimeStamp();
85+
}
86+
4787
@Override
4888
public List<AlertChannel> getAlertChannels() {
4989
return AlertChannel.load(Sets.newHashSet(alertChannel));

tis-incr/tis-realtime-flink/src/main/java/com/qlangtech/plugins/incr/flink/alert/AlterChannelElement.java

Lines changed: 0 additions & 16 deletions
This file was deleted.

tis-incr/tis-realtime-flink/src/main/java/com/qlangtech/plugins/incr/flink/alert/AlterChannelElementCreatorFactory.java

Lines changed: 0 additions & 45 deletions
This file was deleted.

tis-incr/tis-realtime-flink/src/main/java/com/qlangtech/plugins/incr/flink/launch/FlinkIncrJobStatus.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,13 @@
4141
**/
4242
public class FlinkIncrJobStatus extends BasicFinkIncrJobStatus {
4343
static final String KEY_SAVEPOINT_DISCARD_PREFIX = "discard";
44-
// private final File incrJobFile;
45-
// private JobID jobID;
46-
private List<FlinkSavepoint> savepointPaths = Lists.newArrayList();
47-
// 当前job的状态
48-
// private State state;
4944

45+
private List<FlinkSavepoint> savepointPaths = Lists.newArrayList();
5046
// 已经废弃的savepoint路径集合
5147
private final Set<String> discardPaths = Sets.newHashSet();
5248
private final Function<JobID, String> savePointRootPathCreator;
5349

5450
public Optional<FlinkSavepoint> containSavepoint(String path) {
55-
5651
for (FlinkSavepoint sp : savepointPaths) {
5752
if (StringUtils.equals(path, sp.getPath())) {
5853
return Optional.of(sp);
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
* <p>
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
* <p>
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package com.qlangtech.plugins.incr.flink.launch;
20+
21+
import com.qlangtech.tis.coredefine.module.action.TargetResName;
22+
import com.qlangtech.tis.lang.TisException;
23+
import org.apache.flink.client.program.rest.RestClusterClient;
24+
import org.apache.flink.runtime.rest.handler.legacy.messages.ClusterOverviewWithVersion;
25+
import org.apache.flink.runtime.rest.messages.EmptyMessageParameters;
26+
import org.apache.flink.runtime.rest.messages.EmptyRequestBody;
27+
import org.apache.flink.runtime.rest.messages.taskmanager.TaskManagerInfo;
28+
import org.apache.flink.runtime.rest.messages.taskmanager.TaskManagersHeaders;
29+
import org.apache.flink.runtime.rest.messages.taskmanager.TaskManagersInfo;
30+
import org.slf4j.Logger;
31+
import org.slf4j.LoggerFactory;
32+
33+
import java.util.Collection;
34+
import java.util.concurrent.CompletableFuture;
35+
import java.util.concurrent.TimeUnit;
36+
37+
/**
38+
* Flink集群Slot校验工具类
39+
* 用于在部署Flink Job之前校验集群是否有足够的可用slot
40+
*
41+
* @author: 百岁(baisui@qlangtech.com)
42+
* @create: 2025-11-26
43+
**/
44+
public class FlinkSlotValidator {
45+
private static final Logger logger = LoggerFactory.getLogger(FlinkSlotValidator.class);
46+
47+
/**
48+
* 校验Flink集群是否有足够的可用slot
49+
*
50+
* @param restClient RestClusterClient实例
51+
* @param requiredSlots 所需的slot数量(等于配置的parallelism)
52+
* @param collection 任务目标资源名称
53+
* @throws TisException 当可用slot不足时抛出异常
54+
*/
55+
public static void validateAvailableSlots(
56+
RestClusterClient<?> restClient,
57+
int requiredSlots,
58+
TargetResName collection) throws TisException {
59+
60+
if (requiredSlots <= 0) {
61+
throw new IllegalArgumentException("Required slots must be greater than 0, but got: " + requiredSlots);
62+
}
63+
64+
String clusterUrl = restClient.getWebInterfaceURL();
65+
66+
try {
67+
logger.info("Starting slot validation for job: {}, required slots: {}, cluster: {}",
68+
collection.getName(), requiredSlots, clusterUrl);
69+
70+
// 使用Flink原生API获取集群概览信息
71+
CompletableFuture<ClusterOverviewWithVersion> overviewFuture = restClient.sendRequest(
72+
org.apache.flink.runtime.rest.messages.ClusterOverviewHeaders.getInstance(),
73+
EmptyMessageParameters.getInstance(),
74+
EmptyRequestBody.getInstance()
75+
);
76+
ClusterOverviewWithVersion overview = overviewFuture.get(10, TimeUnit.SECONDS);
77+
78+
// 使用Flink原生API获取TaskManager详细信息
79+
CompletableFuture<TaskManagersInfo> taskManagersFuture = restClient.sendRequest(
80+
TaskManagersHeaders.getInstance(),
81+
EmptyMessageParameters.getInstance(),
82+
EmptyRequestBody.getInstance()
83+
);
84+
TaskManagersInfo taskManagersInfo = taskManagersFuture.get(10, TimeUnit.SECONDS);
85+
86+
// 计算可用slot总数
87+
int totalSlots = overview.getNumSlotsTotal();
88+
int availableSlots = overview.getNumSlotsAvailable();
89+
int occupiedSlots = totalSlots - availableSlots;
90+
int taskManagerCount = overview.getNumTaskManagersConnected();
91+
92+
logger.info("Cluster slot status - Total: {}, Available: {}, Occupied: {}, TaskManagers: {}",
93+
totalSlots, availableSlots, occupiedSlots, taskManagerCount);
94+
95+
// 校验可用slot是否足够
96+
if (availableSlots < requiredSlots) {
97+
String errorMessage = buildInsufficientSlotsErrorMessage(
98+
collection,
99+
requiredSlots,
100+
availableSlots,
101+
totalSlots,
102+
occupiedSlots,
103+
taskManagerCount,
104+
taskManagersInfo,
105+
clusterUrl
106+
);
107+
108+
// logger.error(errorMessage);
109+
throw TisException.create(errorMessage);
110+
}
111+
112+
logger.info("Slot validation passed. Available slots ({}) >= Required slots ({})",
113+
availableSlots, requiredSlots);
114+
115+
} catch (TisException e) {
116+
throw e;
117+
} catch (Exception e) {
118+
String errorMsg = "Failed to validate available slots for job: " + collection.getName()
119+
+ ", cluster address: " + clusterUrl;
120+
logger.error(errorMsg, e);
121+
throw TisException.create(errorMsg, e);
122+
}
123+
}
124+
125+
/**
126+
* 构建slot不足时的详细错误信息
127+
*/
128+
private static String buildInsufficientSlotsErrorMessage(
129+
TargetResName collection,
130+
int requiredSlots,
131+
int availableSlots,
132+
int totalSlots,
133+
int occupiedSlots,
134+
int taskManagerCount,
135+
TaskManagersInfo taskManagersInfo,
136+
String clusterUrl) {
137+
138+
StringBuilder errorMsg = new StringBuilder();
139+
errorMsg.append("Insufficient available slots in Flink cluster for job: ").append(collection.getName()).append("\n");
140+
errorMsg.append("========================================\n");
141+
errorMsg.append("Required slots: ").append(requiredSlots).append("\n");
142+
errorMsg.append("Available slots: ").append(availableSlots).append("\n");
143+
errorMsg.append("Total slots: ").append(totalSlots).append("\n");
144+
errorMsg.append("Occupied slots: ").append(occupiedSlots).append("\n");
145+
errorMsg.append("TaskManager count: ").append(taskManagerCount).append("\n");
146+
147+
// 添加TaskManager详细信息
148+
if (taskManagersInfo != null && taskManagersInfo.getTaskManagerInfos() != null) {
149+
Collection<TaskManagerInfo> taskManagers = taskManagersInfo.getTaskManagerInfos();
150+
if (!taskManagers.isEmpty()) {
151+
errorMsg.append("\nTaskManager Details:\n");
152+
int index = 1;
153+
for (TaskManagerInfo tmInfo : taskManagers) {
154+
errorMsg.append(" [").append(index++).append("] ");
155+
errorMsg.append("ID: ").append(tmInfo.getResourceId().getResourceIdString());
156+
errorMsg.append(", Slots: ").append(tmInfo.getNumberSlots());
157+
errorMsg.append(", Free: ").append(tmInfo.getNumberAvailableSlots());
158+
errorMsg.append("\n");
159+
}
160+
}
161+
}
162+
163+
errorMsg.append("\nCluster Address: ").append(clusterUrl).append("\n");
164+
errorMsg.append("========================================\n");
165+
errorMsg.append("Suggestions:\n");
166+
errorMsg.append(" 1. Increase the number of TaskManager instances\n");
167+
errorMsg.append(" 2. Increase slots per TaskManager (taskmanager.numberOfTaskSlots)\n");
168+
errorMsg.append(" 3. Reduce the parallelism from ").append(requiredSlots)
169+
.append(" to ").append(availableSlots).append(" or less\n");
170+
errorMsg.append(" 4. Wait for running jobs to complete and release slots\n");
171+
172+
return errorMsg.toString();
173+
}
174+
}

tis-incr/tis-realtime-flink/src/main/java/com/qlangtech/plugins/incr/flink/launch/FlinkTaskNodeController.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.qlangtech.tis.coredefine.module.action.IRCController.SupportTriggerSavePointResult;
2727
import com.qlangtech.tis.coredefine.module.action.TargetResName;
2828
import com.qlangtech.tis.coredefine.module.action.impl.FlinkJobDeploymentDetails;
29+
import com.qlangtech.tis.datax.DataXName;
2930
import com.qlangtech.tis.datax.job.ServerLaunchToken;
3031
import com.qlangtech.tis.lang.TisException;
3132
import com.qlangtech.tis.manage.common.incr.UberJarUtil;
@@ -38,7 +39,6 @@
3839
import org.apache.commons.lang3.tuple.Pair;
3940
import org.apache.flink.api.common.JobID;
4041
import org.apache.flink.api.common.JobStatus;
41-
import org.apache.flink.client.program.ClusterClient;
4242
import org.apache.flink.client.program.rest.RestClusterClient;
4343
import org.apache.flink.core.execution.SavepointFormatType;
4444
import org.apache.flink.runtime.messages.Acknowledge;
@@ -220,7 +220,8 @@ public IDeploymentDetail getRCDeployment(TargetResName collection) {
220220
// };
221221
if (incrJobStatus.getState() == IFlinkIncrJobStatus.State.NONE) {
222222
// stop 或者压根没有创建
223-
return FlinkJobDeploymentDetails.noneState(factory.getClusterCfg(), incrJobStatus);
223+
return FlinkJobDeploymentDetails.noneState(
224+
DataXName.createDataXPipeline(collection.getName()), factory.getClusterCfg(), incrJobStatus);
224225
}
225226
JobID launchJobID = incrJobStatus.getLaunchJobID();
226227
try {
@@ -229,7 +230,7 @@ public IDeploymentDetail getRCDeployment(TargetResName collection) {
229230
JobStatus status = jobStatus.get(5, TimeUnit.SECONDS);
230231
if (status == null || status.isTerminalState()) {
231232
incrJobStatus.setState(convertTerminalFlinkJobStatus(status));
232-
return FlinkJobDeploymentDetails.noneState(factory.getClusterCfg(), incrJobStatus);
233+
return FlinkJobDeploymentDetails.noneState(DataXName.createDataXPipeline(collection.getName()), factory.getClusterCfg(), incrJobStatus);
233234
}
234235

235236
CompletableFuture<JobDetailsInfo> jobDetails = restClient.getJobDetails(launchJobID);
@@ -246,7 +247,7 @@ public IDeploymentDetail getRCDeployment(TargetResName collection) {
246247
if (isNotFoundException(cause)) {
247248
logger.warn(e.getMessage(), e);
248249
incrJobStatus.setState(State.FAILED);
249-
return FlinkJobDeploymentDetails.noneState(factory.getClusterCfg(), incrJobStatus);
250+
return FlinkJobDeploymentDetails.noneState(DataXName.createDataXPipeline(collection.getName()), factory.getClusterCfg(), incrJobStatus);
250251
}
251252
throw new RuntimeException(e);
252253
} catch (Exception e) {

tis-incr/tis-realtime-flink/src/main/java/com/qlangtech/plugins/incr/flink/launch/TISFlinkCDCStreamFactory.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,8 @@ private FlinkTaskNodeController getIncrSync() {
200200
* ==========================================================================
201201
*/
202202
@Override
203-
public void checkUseable(TargetResName collection) {
204-
this.getClusterCfg().checkUseable(collection);
203+
public void checkUseable(TargetResName collection, boolean deploying) {
204+
this.getClusterCfg().checkUseable(collection, deploying, parallelism);
205205
}
206206

207207
@Override

0 commit comments

Comments
 (0)