package org.ovirt.engine.core.bll.scheduling.policyunits; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import javax.inject.Inject; import org.ovirt.engine.core.bll.scheduling.PolicyUnitImpl; import org.ovirt.engine.core.bll.scheduling.PolicyUnitParameter; import org.ovirt.engine.core.bll.scheduling.external.BalanceResult; import org.ovirt.engine.core.bll.scheduling.pending.PendingResourceManager; import org.ovirt.engine.core.bll.scheduling.utils.FindVmAndDestinations; import org.ovirt.engine.core.bll.scheduling.utils.VdsCpuUsageComparator; import org.ovirt.engine.core.common.businessentities.Cluster; import org.ovirt.engine.core.common.businessentities.VDS; import org.ovirt.engine.core.common.businessentities.VdsSpmStatus; import org.ovirt.engine.core.common.config.Config; import org.ovirt.engine.core.common.config.ConfigValues; import org.ovirt.engine.core.common.scheduling.PolicyUnit; import org.ovirt.engine.core.dal.dbbroker.DbFacade; import org.ovirt.engine.core.dao.ClusterDao; import org.ovirt.engine.core.dao.VdsDao; import org.ovirt.engine.core.dao.VmDao; import org.ovirt.engine.core.dao.VmStatisticsDao; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public abstract class CpuAndMemoryBalancingPolicyUnit extends PolicyUnitImpl { private static final Logger log = LoggerFactory.getLogger(CpuAndMemoryBalancingPolicyUnit.class); @Inject private VmDao vmDao; @Inject private VmStatisticsDao vmStatisticsDao; @Override protected Set<PolicyUnitParameter> getParameters() { Set<PolicyUnitParameter> params = super.getParameters(); params.add(PolicyUnitParameter.LOW_MEMORY_LIMIT_FOR_OVER_UTILIZED); params.add(PolicyUnitParameter.HIGH_MEMORY_LIMIT_FOR_UNDER_UTILIZED); return params; } public CpuAndMemoryBalancingPolicyUnit(PolicyUnit policyUnit, PendingResourceManager pendingResourceManager) { super(policyUnit, pendingResourceManager); } @Override public Optional<BalanceResult> balance(final Cluster cluster, List<VDS> hosts, Map<String, String> parameters, ArrayList<String> messages) { Objects.requireNonNull(hosts); Objects.requireNonNull(cluster); if (hosts.size() < 2) { log.debug("No balancing for cluster '{}', contains only {} host(s)", cluster.getName(), hosts.size()); return Optional.empty(); } final List<VDS> overUtilizedPrimaryHosts = getPrimarySources(cluster, hosts, parameters); final List<VDS> overUtilizedSecondaryHosts = getSecondarySources(cluster, hosts, parameters); // if there aren't any overutilized hosts, then there is nothing to balance... if ((overUtilizedPrimaryHosts == null || overUtilizedPrimaryHosts.size() == 0) && (overUtilizedSecondaryHosts == null || overUtilizedSecondaryHosts.size() == 0)) { log.debug("There is no over-utilized host in cluster '{}'", cluster.getName()); return Optional.empty(); } FindVmAndDestinations findVmAndDestinations = getFindVmAndDestinations(cluster, parameters); Optional<BalanceResult> result = Optional.empty(); // try balancing based on CPU first if (overUtilizedPrimaryHosts != null && overUtilizedPrimaryHosts.size() > 0) { // returns hosts with utilization lower than the specified threshold List<VDS> underUtilizedHosts = getPrimaryDestinations(cluster, hosts, parameters); /* if no host has a spare power, then there is nothing we can do to balance it here, try the secondary aporoach */ if (underUtilizedHosts == null || underUtilizedHosts.size() == 0) { log.warn("All candidate hosts have been filtered, can't balance the cluster '{}'" + " based on the CPU usage, will try memory based approach", cluster.getName()); } else { result = getBalance(findVmAndDestinations, overUtilizedPrimaryHosts, underUtilizedHosts); } } // if it is not possible (or necessary) to balance based on CPU, try with memory if (!result.isPresent() && (overUtilizedSecondaryHosts != null && overUtilizedSecondaryHosts.size() > 0)) { // returns hosts with more free memory than the specified threshold List<VDS> underUtilizedHosts = getSecondaryDestinations(cluster, hosts, parameters); // if no host has memory to spare, then there is nothing we can do to balance it.. if (underUtilizedHosts == null || underUtilizedHosts.size() == 0) { log.warn("All candidate hosts have been filtered, can't balance the cluster '{}'" + " using memory based approach", cluster.getName()); return Optional.empty(); } result = getBalance(findVmAndDestinations, overUtilizedSecondaryHosts, underUtilizedHosts); } // add the current host, it is possible it is the best host after all, // because the balancer does not know about affinity for example Optional<BalanceResult> finalResult = result; result.map(BalanceResult::getCurrentHost) .filter(Objects::nonNull) .ifPresent(h -> finalResult.ifPresent(res -> res.getCandidateHosts().add(h))); return result; } private Optional<BalanceResult> getBalance(FindVmAndDestinations findVmAndDestinations, final List<VDS> overUtilizedHosts, final List<VDS> underUtilizedHosts) { return findVmAndDestinations.invoke(overUtilizedHosts, underUtilizedHosts, getVmDao(), getVmStatisticsDao()) .map(res -> new BalanceResult(res.getVmToMigrate().getId(), res.getDestinationHosts().stream() .map(VDS::getId) .collect(Collectors.toList()), res.getVmToMigrate().getRunOnVds()) ); } /** * Get all hosts that have more free memory than minFreeMemory, but less free memory than maxFreeMemory. * * @param hosts - candidate hosts * @param minFreeMemory - minimum amount of free memory required (MiBĂș * @param maxFreeMemory - maximum amount of free memory allowed (MiB) * @return - normally utilized hosts */ protected List<VDS> getNormallyUtilizedMemoryHosts(Collection<VDS> hosts, long minFreeMemory, long maxFreeMemory) { List<VDS> result = new ArrayList<>(); for (VDS h: hosts) { if (h.getMaxSchedulingMemory() >= minFreeMemory && h.getMaxSchedulingMemory() <= maxFreeMemory) { result.add(h); } } return result; } /** * Return a list of hosts that have more free memory than lowFreeMemoryLimit and more * VMs than minVmCount. * * minVmCount is useful for using this method to find a source of VMs for migration * (we do not care about hosts that have no VMs in that case). If you are looking * for a destination candidates, pass 0 there. * * @param hosts - candidate hosts * @param lowFreeMemoryLimit - minimal amount of free memory to be considered under utilized (MiB) * @param minVmCount - minimal number of VMs that need to be present on a host * @return - under-utilized hosts */ protected List<VDS> getUnderUtilizedMemoryHosts(Collection<VDS> hosts, long lowFreeMemoryLimit, int minVmCount) { List<VDS> result = new ArrayList<>(); for (VDS h: hosts) { if (h.getMaxSchedulingMemory() > lowFreeMemoryLimit && h.getVmCount() >= minVmCount) { result.add(h); } } return result; } /** * Compute a set of hosts where less than a configured amount of memory is available * and there is more VMs than one. * * @param hosts A list of all hosts to consider * @param highFreeMemoryLimit The amount of free memory that has to be available for * the host to NOT be overutilized (in MB) * @return A list of hosts with a memory pressure situation */ protected List<VDS> getOverUtilizedMemoryHosts(Collection<VDS> hosts, long highFreeMemoryLimit) { List<VDS> result = new ArrayList<>(); for (VDS h: hosts) { if (h.getMaxSchedulingMemory() < highFreeMemoryLimit && h.getVmCount() > 1) { result.add(h); } } return result; } /** * Get all hosts that are neither over- or under-utilized in terms of CPU power. * See getOverUtilizedCpuHosts and getUnderUtilizedCpuHosts for details. */ protected List<VDS> getNormallyUtilizedCPUHosts(Cluster cluster, List<VDS> relevantHosts, final int highUtilization, final int cpuOverCommitDurationMinutes, final int highVdsCount) { Set<VDS> remainingHosts = new HashSet<>(relevantHosts); remainingHosts.removeAll(getOverUtilizedCPUHosts(remainingHosts, highUtilization, cpuOverCommitDurationMinutes)); remainingHosts.removeAll(getUnderUtilizedCPUHosts(remainingHosts, highVdsCount, 0, cpuOverCommitDurationMinutes)); return new ArrayList<>(remainingHosts); } /** * Get hosts where the CPU has been utilized to more than highUtilization percentage for * more than cpuOverCommitDurationMinutes minutes. * * @param relevantHosts - candidate hosts * @param highUtilization - threshold cpu usage in percents * @param cpuOverCommitDurationMinutes - time limit in minutes * @return - over utilized hosts */ protected List<VDS> getOverUtilizedCPUHosts(Collection<VDS> relevantHosts, final int highUtilization, final int cpuOverCommitDurationMinutes) { List<VDS> overUtilizedHosts = relevantHosts.stream() .filter(p -> (p.getUsageCpuPercent() + calcSpmCpuConsumption(p)) >= highUtilization && p.getCpuOverCommitTimestamp() != null && (getTime().getTime() - p.getCpuOverCommitTimestamp().getTime()) >= TimeUnit.MINUTES.toMillis(cpuOverCommitDurationMinutes) && p.getVmCount() > 0) .collect(Collectors.toList()); if (overUtilizedHosts.size() > 1) { // Assume all hosts belong to the same cluster Cluster cluster = getClusterDao().get(overUtilizedHosts.get(0).getClusterId()); Collections.sort(overUtilizedHosts, new VdsCpuUsageComparator( cluster != null && cluster.getCountThreadsAsCores()).reversed()); } return overUtilizedHosts; } /** * Get hosts where the CPU is currently loaded to less than lowUtilization percents, * and which were over-utilized (in average) for more than cpuOverCommitDurationMinutes. * * Also filter out hosts with less than minVmCount VMs. * * minVmCount is useful for using this method to find a source of VMs for migration * (we do not care about hosts that have no VMs in that case). If you are looking * for a destination candidates, pass 0 there. * * @param relevantHosts - candidate hosts * @param lowUtilization - load threshold in percent * @param minVmCount - minimal number of VMs on a host * @param cpuOverCommitDurationMinutes - time limit in minutes */ protected List<VDS> getUnderUtilizedCPUHosts(Collection<VDS> relevantHosts, final int lowUtilization, final int minVmCount, final int cpuOverCommitDurationMinutes) { List<VDS> underUtilizedHosts = relevantHosts.stream() .filter(p -> (p.getUsageCpuPercent() + calcSpmCpuConsumption(p)) < lowUtilization && p.getVmCount() >= minVmCount && (p.getCpuOverCommitTimestamp() == null || (getTime().getTime() - p.getCpuOverCommitTimestamp().getTime()) >= TimeUnit.MINUTES.toMillis(cpuOverCommitDurationMinutes))) .collect(Collectors.toList()); if (underUtilizedHosts.size() > 1) { // Assume all hosts belong to the same cluster Cluster cluster = getClusterDao().get(underUtilizedHosts.get(0).getClusterId()); Collections.sort(underUtilizedHosts, new VdsCpuUsageComparator( cluster != null && cluster.getCountThreadsAsCores())); } return underUtilizedHosts; } public static int calcSpmCpuConsumption(VDS vds) { return (vds.getSpmStatus() == VdsSpmStatus.None) ? 0 : Config .<Integer> getValue(ConfigValues.SpmVCpuConsumption) * Config.<Integer> getValue(ConfigValues.VcpuConsumptionPercentage) / vds.getCpuCores(); } protected VdsDao getVdsDao() { return DbFacade.getInstance().getVdsDao(); } protected VmDao getVmDao() { return vmDao; } protected VmStatisticsDao getVmStatisticsDao() { return vmStatisticsDao; } protected ClusterDao getClusterDao() { return DbFacade.getInstance().getClusterDao(); } protected Date getTime() { return new Date(); } protected int tryParseWithDefault(String candidate, int defaultValue) { if (candidate != null) { try { return Integer.parseInt(candidate); } catch (Exception e) { // do nothing } } return defaultValue; } /** * This method should return a configured FindVmAndDestinations objects for * cluster using the provided parameters. * * @param cluster - cluster instance for scheduling * @param parameters - scheduling parameters * @return - a configured FindVmAndDestinations instance */ protected abstract FindVmAndDestinations getFindVmAndDestinations(Cluster cluster, Map<String, String> parameters); /** * Return a list of hosts that should be used as VM "donors" during the first * attempt of migration planning. * * @param cluster - cluster instance for scheduling * @param candidateHosts - all available hosts for this scheduling round * @param parameters - scheduling parameters * @return - subset of hosts from candidateHosts to be used as migration sources */ protected abstract List<VDS> getPrimarySources(Cluster cluster, List<VDS> candidateHosts, Map<String, String> parameters); /** * Return a list of hosts that should be used as VM "receivers" during the first * attempt of migration planning. * * @param cluster - cluster instance for scheduling * @param candidateHosts - all available hosts for this scheduling round * @param parameters - scheduling parameters * @return - subset of hosts from candidateHosts to be used as migration destination */ protected abstract List<VDS> getPrimaryDestinations(Cluster cluster, List<VDS> candidateHosts, Map<String, String> parameters); /** * Return a list of hosts that should be used as VM "donors" during the second * attempt of migration planning. The second attempt is used when there is no * possible (or needed) migration during the first attempt. * * @param cluster - cluster instance for scheduling * @param candidateHosts - all available hosts for this scheduling round * @param parameters - scheduling parameters * @return - subset of hosts from candidateHosts to be used as migration sources */ protected abstract List<VDS> getSecondarySources(Cluster cluster, List<VDS> candidateHosts, Map<String, String> parameters); /** * Return a list of hosts that should be used as VM "receivers" during the second * attempt of migration planning. The second attempt is used when there is no * possible (or needed) migration during the first attempt. * * @param cluster - cluster instance for scheduling * @param candidateHosts - all available hosts for this scheduling round * @param parameters - scheduling parameters * @return - subset of hosts from candidateHosts to be used as migration destination */ protected abstract List<VDS> getSecondaryDestinations(Cluster cluster, List<VDS> candidateHosts, Map<String, String> parameters); }