Use clamped requirements for broker

Go through the resource broker for node candidates again.

Ask for >= x, <= 2*x when we have requirement x (for cpu, memory).  This
gives us some flexibility in case no image precisely fulfills the
requirements, but rules out nodes that are outrageously big.

Change-Id: I35d0b4207b2b76b212f5e584f932b8aaf579a0e9
This commit is contained in:
Rudi Schlatte
2024-04-04 13:54:08 +02:00
parent 7798bf3a38
commit da7805282c
3 changed files with 186 additions and 65 deletions

View File

@@ -75,18 +75,99 @@ public class KubevelaAnalyzer {
return getNodeCount(parseKubevela(kubevela)); return getNodeCount(parseKubevela(kubevela));
} }
/**
* Add frequirements for Ubuntu version 22.04. Also add requirement for
* 2GB of RAM for now until we know more about the size / cpu requirements
* of the nebulous runtime.
*
* @param reqs The list of requirements to add to.
*/
private static void addNebulousRequirements(List<Requirement> reqs) {
reqs.add(new AttributeRequirement("image", "operatingSystem.family",
RequirementOperator.IN, OperatingSystemFamily.UBUNTU.toString()));
reqs.add(new AttributeRequirement("image", "name", RequirementOperator.INC, "22"));
reqs.add(new AttributeRequirement("hardware", "ram", RequirementOperator.GEQ, "2048"));
}
/**
* Get cpu requirement, taken from "cpu" resource requirement in KubeVela
* and rounding up to nearest whole number.
*
* @param c A Component branch of the parsed KubeVela file.
* @param componentName the component name, used only for logging.
* @return an integer of number of cores required, or -1 in case of no
* requirement.
*/
private static long getCpuRequirement(JsonNode c, String componentName) {
JsonNode cpu = c.at("/properties/cpu");
if (cpu.isMissingNode()) cpu = c.at("/properties/resources/requests/cpu");
if (!cpu.isMissingNode()) {
// KubeVela has fractional core /cpu requirements, and the
// value might be given as a string instead of a number, so
// parse string in all cases.
double kubevela_cpu = -1;
try {
kubevela_cpu = Double.parseDouble(cpu.asText());
} catch (NumberFormatException e) {
log.warn("CPU spec in " + componentName + " is not a number, value seen is " + cpu.asText());
return -1;
}
long sal_cores = Math.round(Math.ceil(kubevela_cpu));
if (sal_cores > 0) {
return sal_cores;
} else {
// floatValue returns 0.0 if node is not numeric
log.warn("CPU spec in " + componentName + " is not a number, value seen is " + cpu.asText());
return -1;
}
} else {
// no spec given
return -1;
}
}
/**
* Get memory requirement, taken from "memory" resource requirement in KubeVela
* and converted to Megabytes. We currently handle the "Mi" and "Gi"
* suffixes that KubeVela uses.
*
* @param c A Component branch of the parsed KubeVela file.
* @param componentName the component name, used only for logging.
* @return an integer of memory required in Mb, or -1 in case of no
* requirement.
*/
private static long getMemoryRequirement(JsonNode c, String componentName) {
JsonNode memory = c.at("/properties/memory");
if (memory.isMissingNode()) memory = c.at("/properties/resources/requests/memory");
if (!memory.isMissingNode()) {
long sal_memory = -1;
String sal_memory_str = memory.asText();
if (sal_memory_str.endsWith("Mi")) {
sal_memory = Long.parseLong(sal_memory_str.substring(0, sal_memory_str.length() - 2));
} else if (sal_memory_str.endsWith("Gi")) {
sal_memory = Long.parseLong(sal_memory_str.substring(0, sal_memory_str.length() - 2)) * 1024;
} else {
log.warn("Unsupported memory specification in component " + componentName + " : " + memory.asText() + " (wanted 'Mi' or 'Gi') ");
}
return sal_memory;
} else {
return -1;
}
}
/** /**
* Extract node requirements from a KubeVela file in a form we can send to * Extract node requirements from a KubeVela file in a form we can send to
* the SAL `findNodeCandidates` endpoint. <p> * the SAL `findNodeCandidates` endpoint. <p>
* *
* We read the following attributes for each component: * We read the following attributes for each component:
* *
* - `properties.cpu`, `properties.requests.cpu`: round up to next integer * - `properties.cpu`, `properties.resources.requests.cpu`: round up to
* and generate requirement `hardware.cores` * next integer and generate requirement `hardware.cores`
* *
* - `properties.memory`, `properties.requests.memory`: Handle "200Mi", * - `properties.memory`, `properties.resources.requests.memory`: Handle
* "0.2Gi" and bare number, convert to MB and generate requirement * "200Mi", "0.2Gi" and bare number, convert to MB and generate
* `hardware.memory` * requirement `hardware.memory`
* *
* Notes:<p> * Notes:<p>
* *
@@ -111,60 +192,24 @@ public class KubevelaAnalyzer {
* family) list of requirements for that component. No requirements mean * family) list of requirements for that component. No requirements mean
* any node will suffice. * any node will suffice.
*/ */
public static Map<String, List<Requirement>> getRequirements(JsonNode kubevela, boolean includeNebulousRequirements) { public static Map<String, List<Requirement>> getBoundedRequirements(JsonNode kubevela, boolean includeNebulousRequirements) {
Map<String, List<Requirement>> result = new HashMap<>(); Map<String, List<Requirement>> result = new HashMap<>();
ArrayNode components = kubevela.withArray("/spec/components"); ArrayNode components = kubevela.withArray("/spec/components");
for (final JsonNode c : components) { for (final JsonNode c : components) {
String componentName = c.get("name").asText(); String componentName = c.get("name").asText();
ArrayList<Requirement> reqs = new ArrayList<>(); ArrayList<Requirement> reqs = new ArrayList<>();
if (includeNebulousRequirements) { if (includeNebulousRequirements) {
// We want Ubuntu, version 22.04, and 2GB of RAM until we know addNebulousRequirements(reqs);
// more about the size / cpu requirements of the nebulous
// runtime.
reqs.add(new AttributeRequirement("image", "operatingSystem.family",
RequirementOperator.IN, OperatingSystemFamily.UBUNTU.toString()));
reqs.add(new AttributeRequirement("image", "name", RequirementOperator.INC, "22"));
reqs.add(new AttributeRequirement("hardware", "ram", RequirementOperator.GEQ, "2048"));
} }
JsonNode cpu = c.at("/properties/cpu"); long cores = getCpuRequirement(c, componentName);
if (cpu.isMissingNode()) cpu = c.at("/properties/resources/requests/cpu"); if (cores > 0) {
if (!cpu.isMissingNode()) { reqs.add(new AttributeRequirement("hardware", "cores",
// KubeVela has fractional core /cpu requirements, and the RequirementOperator.GEQ, Long.toString(cores)));
// value might be given as a string instead of a number, so
// parse string in all cases.
double kubevela_cpu = -1;
try {
kubevela_cpu = Double.parseDouble(cpu.asText());
} catch (NumberFormatException e) {
log.warn("CPU spec in " + componentName + " is not a number, value seen is " + cpu.asText());
}
long sal_cores = Math.round(Math.ceil(kubevela_cpu));
if (sal_cores > 0) {
reqs.add(new AttributeRequirement("hardware", "cores",
RequirementOperator.GEQ, Long.toString(sal_cores)));
} else {
// floatValue returns 0.0 if node is not numeric
log.warn("CPU spec in " + componentName + " is not a number, value seen is " + cpu.asText());
}
} }
JsonNode memory = c.at("/properties/memory"); long memory = getMemoryRequirement(c, componentName);
if (memory.isMissingNode()) cpu = c.at("/properties/resources/requests/memory"); if (memory > 0) {
if (!memory.isMissingNode()) {
String sal_memory = memory.asText();
if (sal_memory.endsWith("Mi")) {
sal_memory = sal_memory.substring(0, sal_memory.length() - 2);
} else if (sal_memory.endsWith("Gi")) {
sal_memory = String.valueOf(Integer.parseInt(sal_memory.substring(0, sal_memory.length() - 2)) * 1024);
} else if (!memory.isNumber()) {
log.warn("Unsupported memory specification in component " + componentName + " : " + memory.asText() + " (wanted 'Mi' or 'Gi') ");
sal_memory = null;
}
// Fall-through: we rewrote the KubeVela file and didn't add
// the "Mi" suffix, but it's a number
if (sal_memory != null) {
reqs.add(new AttributeRequirement("hardware", "ram", reqs.add(new AttributeRequirement("hardware", "ram",
RequirementOperator.GEQ, sal_memory)); RequirementOperator.GEQ, Long.toString(memory)));
}
} }
for (final JsonNode t : c.withArray("/traits")) { for (final JsonNode t : c.withArray("/traits")) {
// TODO: Check for node affinity / geoLocation / country / // TODO: Check for node affinity / geoLocation / country /
@@ -178,27 +223,103 @@ public class KubevelaAnalyzer {
/** /**
* Get node requirements for app components, including nebulous-specific * Get node requirements for app components, including nebulous-specific
* requirements. This method calls {@link #getRequirements(JsonNode, * requirements. This method calls {@link #getBoundedRequirements(JsonNode,
* boolean)} with second parameter {@code true}. * boolean)} with second parameter {@code true}.
* *
* @see #getRequirements(JsonNode, boolean) * @see #getBoundedRequirements(JsonNode, boolean)
*/ */
public static Map<String, List<Requirement>> getRequirements(JsonNode kubevela) { public static Map<String, List<Requirement>> getBoundedRequirements(JsonNode kubevela) {
return getRequirements(kubevela, true); return getBoundedRequirements(kubevela, true);
}
/**
* Get node requirements for app components, including nebulous-specific
* requirements. Like {@link #getBoundedRequirements} but also include an
* upper bound of twice the requirement size. I.e., for cpu=2, we ask for
* cpu >= 2, cpu <= 4. Take care to not ask for less than 2048Mb of
* memory since that's the minimum Nebulous requirement for now.
*/
public static Map<String, List<Requirement>> getClampedRequirements(JsonNode kubevela) {
Map<String, List<Requirement>> result = new HashMap<>();
ArrayNode components = kubevela.withArray("/spec/components");
for (final JsonNode c : components) {
String componentName = c.get("name").asText();
ArrayList<Requirement> reqs = new ArrayList<>();
addNebulousRequirements(reqs);
long cores = getCpuRequirement(c, componentName);
if (cores > 0) {
reqs.add(new AttributeRequirement("hardware", "cores",
RequirementOperator.GEQ, Long.toString(cores)));
reqs.add(new AttributeRequirement("hardware", "cores",
RequirementOperator.LEQ, Long.toString(cores * 2)));
}
long memory = getMemoryRequirement(c, componentName);
if (memory > 0) {
reqs.add(new AttributeRequirement("hardware", "ram",
RequirementOperator.GEQ, Long.toString(memory)));
reqs.add(new AttributeRequirement("hardware", "ram",
// See addNebulousRequirements(), don't ask for both more
// and less than 2048
RequirementOperator.LEQ, Long.toString(Math.max(memory * 2, 2048))));
}
for (final JsonNode t : c.withArray("/traits")) {
// TODO: Check for node affinity / geoLocation / country /
// node type (edge or cloud)
}
// Finally, add requirements for this job to the map
result.put(componentName, reqs);
}
return result;
}
/**
* Get node requirements for app components, including nebulous-specific
* requirements. Like {@link #getBoundedRequirements} but require precise
* amounts, i.e., ask for precisely cpu == 2, memory == 2048 instead of
* asking for >= or <=. Note that we still ask for >= 2048 Mb since
* that's the nebulous lower bound for now.
*/
public static Map<String, List<Requirement>> getPreciseRequirements(JsonNode kubevela) {
Map<String, List<Requirement>> result = new HashMap<>();
ArrayNode components = kubevela.withArray("/spec/components");
for (final JsonNode c : components) {
String componentName = c.get("name").asText();
ArrayList<Requirement> reqs = new ArrayList<>();
addNebulousRequirements(reqs);
long cores = getCpuRequirement(c, componentName);
if (cores > 0) {
reqs.add(new AttributeRequirement("hardware", "cores",
RequirementOperator.EQ, Long.toString(cores)));
}
long memory = getMemoryRequirement(c, componentName);
if (memory > 0) {
reqs.add(new AttributeRequirement("hardware", "ram",
// See addNebulousRequirements; don't ask for less than
// the other constraint allows
RequirementOperator.EQ, Long.toString(Math.max(memory, 2048))));
}
for (final JsonNode t : c.withArray("/traits")) {
// TODO: Check for node affinity / geoLocation / country /
// node type (edge or cloud)
}
// Finally, add requirements for this job to the map
result.put(componentName, reqs);
}
return result;
} }
/** /**
* Extract node requirements from a KubeVela file. * Extract node requirements from a KubeVela file.
* *
* @see #getRequirements(JsonNode) * @see #getBoundedRequirements(JsonNode)
* @param kubevela The KubeVela file, as a YAML string. * @param kubevela The KubeVela file, as a YAML string.
* @return a map of component name to (potentially empty, except for OS * @return a map of component name to (potentially empty, except for OS
* family) list of requirements for that component. No requirements mean * family) list of requirements for that component. No requirements mean
* any node will suffice. * any node will suffice.
* @throws JsonProcessingException if kubevela does not contain valid YAML. * @throws JsonProcessingException if kubevela does not contain valid YAML.
*/ */
public static Map<String, List<Requirement>> getRequirements(String kubevela) throws JsonProcessingException { public static Map<String, List<Requirement>> getBoundedRequirements(String kubevela) throws JsonProcessingException {
return getRequirements(parseKubevela(kubevela)); return getBoundedRequirements(parseKubevela(kubevela));
} }
/** /**

View File

@@ -165,7 +165,7 @@ public class NebulousAppDeployer {
// ------------------------------------------------------------ // ------------------------------------------------------------
// 1. Extract node requirements // 1. Extract node requirements
Map<String, List<Requirement>> componentRequirements = KubevelaAnalyzer.getRequirements(kubevela); Map<String, List<Requirement>> componentRequirements = KubevelaAnalyzer.getClampedRequirements(kubevela);
Map<String, Integer> nodeCounts = KubevelaAnalyzer.getNodeCount(kubevela); Map<String, Integer> nodeCounts = KubevelaAnalyzer.getNodeCount(kubevela);
List<Requirement> controllerRequirements = getControllerRequirements(appUUID); List<Requirement> controllerRequirements = getControllerRequirements(appUUID);
@@ -177,7 +177,7 @@ public class NebulousAppDeployer {
// 2. Find node candidates // 2. Find node candidates
// TODO: filter by app resources (check enabled: true in resources array) // TODO: filter by app resources (check enabled: true in resources array)
List<NodeCandidate> controllerCandidates = conn.findNodeCandidatesFromSal(controllerRequirements, appUUID); List<NodeCandidate> controllerCandidates = conn.findNodeCandidates(controllerRequirements, appUUID);
if (controllerCandidates.isEmpty()) { if (controllerCandidates.isEmpty()) {
log.error("Could not find node candidates for requirements: {}", log.error("Could not find node candidates for requirements: {}",
controllerRequirements, keyValue("appId", appUUID), keyValue("clusterName", clusterName)); controllerRequirements, keyValue("appId", appUUID), keyValue("clusterName", clusterName));
@@ -189,7 +189,7 @@ public class NebulousAppDeployer {
String nodeName = e.getKey(); String nodeName = e.getKey();
List<Requirement> requirements = e.getValue(); List<Requirement> requirements = e.getValue();
// TODO: filter by app resources (check enabled: true in resources array) // TODO: filter by app resources (check enabled: true in resources array)
List<NodeCandidate> candidates = conn.findNodeCandidatesFromSal(requirements, appUUID); List<NodeCandidate> candidates = conn.findNodeCandidates(requirements, appUUID);
if (candidates.isEmpty()) { if (candidates.isEmpty()) {
log.error("Could not find node candidates for for node {}, requirements: {}", nodeName, requirements, log.error("Could not find node candidates for for node {}, requirements: {}", nodeName, requirements,
keyValue("appId", appUUID), keyValue("clusterName", clusterName)); keyValue("appId", appUUID), keyValue("clusterName", clusterName));
@@ -415,7 +415,7 @@ public class NebulousAppDeployer {
// ------------------------------------------------------------ // ------------------------------------------------------------
// 1. Extract node requirements // 1. Extract node requirements
Map<String, List<Requirement>> componentRequirements = KubevelaAnalyzer.getRequirements(kubevela); Map<String, List<Requirement>> componentRequirements = KubevelaAnalyzer.getClampedRequirements(kubevela);
Map<String, Integer> componentReplicaCounts = KubevelaAnalyzer.getNodeCount(kubevela); Map<String, Integer> componentReplicaCounts = KubevelaAnalyzer.getNodeCount(kubevela);
Map<String, List<Requirement>> oldComponentRequirements = app.getComponentRequirements(); Map<String, List<Requirement>> oldComponentRequirements = app.getComponentRequirements();
@@ -444,7 +444,7 @@ public class NebulousAppDeployer {
log.debug("Adding {} nodes to component {}", nAdd, componentName, log.debug("Adding {} nodes to component {}", nAdd, componentName,
keyValue("appId", appUUID), keyValue("clusterName", clusterName)); keyValue("appId", appUUID), keyValue("clusterName", clusterName));
// TODO: filter by app resources (check enabled: true in resources array) // TODO: filter by app resources (check enabled: true in resources array)
List<NodeCandidate> candidates = conn.findNodeCandidatesFromSal(newR, appUUID); List<NodeCandidate> candidates = conn.findNodeCandidates(newR, appUUID);
if (candidates.isEmpty()) { if (candidates.isEmpty()) {
log.error("Could not find node candidates for requirements: {}", log.error("Could not find node candidates for requirements: {}",
newR, keyValue("appId", appUUID), keyValue("clusterName", clusterName)); newR, keyValue("appId", appUUID), keyValue("clusterName", clusterName));
@@ -500,7 +500,7 @@ public class NebulousAppDeployer {
log.debug("Redeploying all nodes of component {}", componentName, log.debug("Redeploying all nodes of component {}", componentName,
keyValue("appId", appUUID), keyValue("clusterName", clusterName)); keyValue("appId", appUUID), keyValue("clusterName", clusterName));
// TODO: filter by app resources (check enabled: true in resources array) // TODO: filter by app resources (check enabled: true in resources array)
List<NodeCandidate> candidates = conn.findNodeCandidatesFromSal(newR, appUUID); List<NodeCandidate> candidates = conn.findNodeCandidates(newR, appUUID);
if (candidates.size() == 0) { if (candidates.size() == 0) {
log.error("Empty node candidate list for component {}, continuing without creating node", componentName, log.error("Empty node candidate list for component {}, continuing without creating node", componentName,
keyValue("appId", appUUID), keyValue("clusterName", clusterName)); keyValue("appId", appUUID), keyValue("clusterName", clusterName));

View File

@@ -91,7 +91,7 @@ public class NebulousAppTests {
String kubevela_str = Files.readString(getResourcePath("vela-deployment-v2.yml"), String kubevela_str = Files.readString(getResourcePath("vela-deployment-v2.yml"),
StandardCharsets.UTF_8); StandardCharsets.UTF_8);
JsonNode kubevela = yaml_mapper.readTree(kubevela_str); JsonNode kubevela = yaml_mapper.readTree(kubevela_str);
Map<String, List<Requirement>> requirements = KubevelaAnalyzer.getRequirements(kubevela); Map<String, List<Requirement>> requirements = KubevelaAnalyzer.getBoundedRequirements(kubevela);
// We could compare the requirements with what is contained in // We could compare the requirements with what is contained in
// KubeVela, or compare keys with component names, but this would // KubeVela, or compare keys with component names, but this would
// essentially duplicate the method code--so we just make sure the // essentially duplicate the method code--so we just make sure the
@@ -111,7 +111,7 @@ public class NebulousAppTests {
ObjectNode replacements = solutions.withObject("VariableValues"); ObjectNode replacements = solutions.withObject("VariableValues");
ObjectNode kubevela1 = app.rewriteKubevelaWithSolution(replacements); ObjectNode kubevela1 = app.rewriteKubevelaWithSolution(replacements);
Map<String, List<Requirement>> requirements = KubevelaAnalyzer.getRequirements(kubevela1); Map<String, List<Requirement>> requirements = KubevelaAnalyzer.getBoundedRequirements(kubevela1);
// We could compare the requirements with what is contained in // We could compare the requirements with what is contained in
// KubeVela, or compare keys with component names, but this would // KubeVela, or compare keys with component names, but this would
// essentially duplicate the method code--so we just make sure the // essentially duplicate the method code--so we just make sure the