// Copyright 2016 Twitter. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.twitter.heron.packing;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import com.twitter.heron.api.generated.TopologyAPI;
import com.twitter.heron.spi.common.Constants;
import com.twitter.heron.spi.packing.InstanceId;
import com.twitter.heron.spi.packing.PackingPlan;
import com.twitter.heron.spi.packing.Resource;
import com.twitter.heron.spi.utils.TopologyUtils;
/**
* Shared utilities for packing algorithms
*/
public final class PackingUtils {
private static final Logger LOG = Logger.getLogger(PackingUtils.class.getName());
private PackingUtils() {
}
/**
* Check whether the Instance has enough RAM and whether it can fit within the container limits.
*
* @param instanceResources The resources allocated to the instance
* @return true if the instance is valid, false otherwise
*/
public static boolean isValidInstance(Resource instanceResources,
long minInstanceRam,
Resource maxContainerResources,
int paddingPercentage) {
if (instanceResources.getRam() < minInstanceRam) {
LOG.severe(String.format(
"Instance requires %d MB ram which is less than the minimum %d MB ram per instance",
instanceResources.getRam(), minInstanceRam / Constants.MB));
return false;
}
long instanceRam = PackingUtils.increaseBy(instanceResources.getRam(), paddingPercentage);
if (instanceRam > maxContainerResources.getRam()) {
LOG.severe(String.format(
"This instance requires containers of at least %d MB ram. The current max container"
+ "size is %d MB",
instanceRam, maxContainerResources.getRam()));
return false;
}
double instanceCpu = Math.round(PackingUtils.increaseBy(
instanceResources.getCpu(), paddingPercentage));
if (instanceCpu > maxContainerResources.getCpu()) {
LOG.severe(String.format(
"This instance requires containers with at least %s cpu cores. The current max container"
+ "size is %s cores",
instanceCpu > maxContainerResources.getCpu(), maxContainerResources.getCpu()));
return false;
}
long instanceDisk = PackingUtils.increaseBy(instanceResources.getDisk(), paddingPercentage);
if (instanceDisk > maxContainerResources.getDisk()) {
LOG.severe(String.format(
"This instance requires containers of at least %d MB disk. The current max container"
+ "size is %d MB",
instanceDisk, maxContainerResources.getDisk()));
return false;
}
return true;
}
/**
* Estimate the per instance and topology resources for the packing plan based on the ramMap,
* instance defaults and paddingPercentage.
*
* @return container plans
*/
public static Set<PackingPlan.ContainerPlan> buildContainerPlans(
Map<Integer, List<InstanceId>> containerInstances,
Map<String, Long> ramMap,
Resource instanceDefaults,
double paddingPercentage) {
Set<PackingPlan.ContainerPlan> containerPlans = new HashSet<>();
for (Integer containerId : containerInstances.keySet()) {
List<InstanceId> instanceList = containerInstances.get(containerId);
long containerRam = 0;
long containerDiskInBytes = 0;
double containerCpu = 0;
// Calculate the resource required for single instance
Set<PackingPlan.InstancePlan> instancePlans = new HashSet<>();
for (InstanceId instanceId : instanceList) {
long instanceRam = 0;
if (ramMap.containsKey(instanceId.getComponentName())) {
instanceRam = ramMap.get(instanceId.getComponentName());
} else {
instanceRam = instanceDefaults.getRam();
}
containerRam += instanceRam;
// Currently not yet support disk or cpu config for different components,
// so just use the default value.
long instanceDisk = instanceDefaults.getDisk();
containerDiskInBytes += instanceDisk;
double instanceCpu = instanceDefaults.getCpu();
containerCpu += instanceCpu;
// Insert it into the map
instancePlans.add(new PackingPlan.InstancePlan(instanceId,
new Resource(instanceCpu, instanceRam, instanceDisk)));
}
containerCpu += (paddingPercentage * containerCpu) / 100;
containerRam += (paddingPercentage * containerRam) / 100;
containerDiskInBytes += (paddingPercentage * containerDiskInBytes) / 100;
Resource resource =
new Resource(Math.round(containerCpu), containerRam, containerDiskInBytes);
PackingPlan.ContainerPlan containerPlan =
new PackingPlan.ContainerPlan(containerId, instancePlans, resource);
containerPlans.add(containerPlan);
}
return containerPlans;
}
/**
* Sort the container plans based on the container Ids
*
* @return sorted array of container plans
*/
public static PackingPlan.ContainerPlan[] sortOnContainerId(
Set<PackingPlan.ContainerPlan> containers) {
ArrayList<Integer> containerIds = new ArrayList<>();
PackingPlan.ContainerPlan[] currentContainers =
new PackingPlan.ContainerPlan[containers.size()];
for (PackingPlan.ContainerPlan container : containers) {
containerIds.add(container.getId());
}
Collections.sort(containerIds);
for (PackingPlan.ContainerPlan container : containers) {
int position = containerIds.indexOf(container.getId());
currentContainers[position] = container;
}
return currentContainers;
}
public static long increaseBy(long value, int paddingPercentage) {
return value + (paddingPercentage * value) / 100;
}
public static double increaseBy(double value, int paddingPercentage) {
return value + (paddingPercentage * value) / 100;
}
/**
* Allocate a new container of a given capacity
*
* @return the number of containers
*/
public static int allocateNewContainer(ArrayList<Container> containers, Resource capacity,
int paddingPercentage) {
containers.add(new Container(capacity, paddingPercentage));
return containers.size();
}
/**
* Identifies which components need to be scaled given specific scaling direction
*
* @return Map < component name, scale factor >
*/
public static Map<String, Integer> getComponentsToScale(Map<String,
Integer> componentChanges, ScalingDirection scalingDirection) {
Map<String, Integer> componentsToScale = new HashMap<String, Integer>();
for (String component : componentChanges.keySet()) {
int parallelismChange = componentChanges.get(component);
if (scalingDirection.includes(parallelismChange)) {
componentsToScale.put(component, parallelismChange);
}
}
return componentsToScale;
}
/**
* Identifies the resources reclaimed by the components that will be scaled down
*
* @return Total resources reclaimed
*/
public static Resource computeTotalResourceChange(TopologyAPI.Topology topology,
Map<String, Integer> componentChanges,
Resource defaultInstanceResources,
ScalingDirection scalingDirection) {
double cpu = 0;
long ram = 0;
long disk = 0;
Map<String, Long> ramMap = TopologyUtils.getComponentRamMapConfig(topology);
Map<String, Integer> componentsToScale = PackingUtils.getComponentsToScale(
componentChanges, scalingDirection);
for (String component : componentsToScale.keySet()) {
int parallelismChange = Math.abs(componentChanges.get(component));
cpu += parallelismChange * defaultInstanceResources.getCpu();
disk += parallelismChange * defaultInstanceResources.getDisk();
if (ramMap.containsKey(component)) {
ram += parallelismChange * ramMap.get(component);
} else {
ram += parallelismChange * defaultInstanceResources.getRam();
}
}
return new Resource(cpu, ram, disk);
}
/**
* Removes containers from tha allocation that do not contain any instances
*/
public static void removeEmptyContainers(Map<Integer, List<InstanceId>> allocation) {
Iterator<Integer> containerIds = allocation.keySet().iterator();
while (containerIds.hasNext()) {
Integer containerId = containerIds.next();
if (allocation.get(containerId).isEmpty()) {
containerIds.remove();
}
}
}
/**
* Generates the containers that correspond to the current packing plan
* along with their associated instances.
*
* @return List of containers for the current packing plan
*/
public static ArrayList<Container> getContainers(PackingPlan currentPackingPlan,
int paddingPercentage) {
ArrayList<Container> containers = new ArrayList<>();
//sort containers based on containerIds;
PackingPlan.ContainerPlan[] currentContainers =
PackingUtils.sortOnContainerId(currentPackingPlan.getContainers());
Resource capacity = currentPackingPlan.getMaxContainerResources();
for (int i = 0; i < currentContainers.length; i++) {
int containerId = PackingUtils.allocateNewContainer(
containers, capacity, paddingPercentage);
for (PackingPlan.InstancePlan instancePlan
: currentContainers[i].getInstances()) {
containers.get(containerId - 1).add(instancePlan);
}
}
return containers;
}
/**
* Generates an instance allocation for the current packing plan
*
* @return Map < containerId, list of InstanceId belonging to this container >
*/
public static Map<Integer, List<InstanceId>> getAllocation(PackingPlan currentPackingPlan) {
Map<Integer, List<InstanceId>> allocation = new HashMap<Integer, List<InstanceId>>();
for (PackingPlan.ContainerPlan containerPlan : currentPackingPlan.getContainers()) {
ArrayList<InstanceId> instances = new ArrayList<InstanceId>();
for (PackingPlan.InstancePlan instance : containerPlan.getInstances()) {
instances.add(new InstanceId(instance.getComponentName(), instance.getTaskId(),
instance.getComponentIndex()));
}
allocation.put(containerPlan.getId(), instances);
}
return allocation;
}
public enum ScalingDirection {
UP,
DOWN;
boolean includes(int parallelismChange) {
switch (this) {
case UP:
return parallelismChange > 0;
case DOWN:
return parallelismChange < 0;
default:
throw new IllegalArgumentException(String.format("Not valid parallelism change: %d",
parallelismChange));
}
}
}
}