com.netflix.fenzo.AutoScaler Maven / Gradle / Ivy
/*
* Copyright 2015 Netflix, Inc.
*
* 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.netflix.fenzo;
import com.netflix.fenzo.functions.Action1;
import com.netflix.fenzo.functions.Func1;
import com.netflix.fenzo.queues.QueuableTask;
import org.apache.mesos.Protos;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
class AutoScaler {
private static class HostAttributeGroup {
String name;
List idleHosts;
int shortFall;
AutoScaleRule rule;
private HostAttributeGroup(String name, AutoScaleRule rule) {
this.name = name;
this.rule = rule;
this.idleHosts = new ArrayList<>();
this.shortFall=0;
}
}
private static class ScalingActivity {
private long scaleUpAt;
private long scaleUpRequestedAt;
private long scaleDownAt;
private long scaleDownRequestedAt;
private int shortfall;
private int scaledNumInstances;
private AutoScaleAction.Type type;
private ScalingActivity(long scaleUpAt, long scaleDownAt, int shortfall, int scaledNumInstances, AutoScaleAction.Type type) {
this.scaleUpAt = scaleUpAt;
scaleUpRequestedAt = 0L;
this.scaleDownAt = scaleDownAt;
scaleDownRequestedAt = 0L;
this.shortfall = shortfall;
this.scaledNumInstances = scaledNumInstances;
this.type = type;
}
}
private static final Logger logger = LoggerFactory.getLogger(AutoScaler.class);
private volatile Action1 callback=null;
private final String mapHostnameAttributeName;
private final String scaleDownBalancedByAttributeName;
private ShortfallEvaluator shortfallEvaluator;
private final ActiveVmGroups activeVmGroups;
private final AutoScaleRules autoScaleRules;
private final boolean disableShortfallEvaluation;
private final String attributeName;
private final AssignableVMs assignableVMs;
private long delayScaleUpBySecs =0L;
private long delayScaleDownBySecs =0L;
private volatile Func1> taskToClustersGetter = null;
private final ThreadPoolExecutor executor =
new ThreadPoolExecutor(1, 1, Long.MAX_VALUE, TimeUnit.MILLISECONDS, new ArrayBlockingQueue(100),
new ThreadPoolExecutor.DiscardOldestPolicy());
private final AtomicBoolean isShutdown = new AtomicBoolean();
private final ConcurrentMap scalingActivityMap = new ConcurrentHashMap<>();
final VMCollection vmCollection;
private final ScaleDownConstraintExecutor scaleDownConstraintExecutor;
AutoScaler(final String attributeName, String mapHostnameAttributeName, String scaleDownBalancedByAttributeName,
final List autoScaleRules, final AssignableVMs assignableVMs,
final boolean disableShortfallEvaluation, ActiveVmGroups activeVmGroups, VMCollection vmCollection,
ScaleDownConstraintExecutor scaleDownConstraintExecutor) {
this.mapHostnameAttributeName = mapHostnameAttributeName;
this.scaleDownBalancedByAttributeName = scaleDownBalancedByAttributeName;
this.shortfallEvaluator = new NaiveShortfallEvaluator();
this.attributeName = attributeName;
this.autoScaleRules = new AutoScaleRules(autoScaleRules);
this.assignableVMs = assignableVMs;
this.disableShortfallEvaluation = disableShortfallEvaluation;
this.activeVmGroups = activeVmGroups;
this.vmCollection = vmCollection;
this.scaleDownConstraintExecutor = scaleDownConstraintExecutor;
}
/* package */ void useOptimizingShortfallAnalyzer() {
shortfallEvaluator = new OptimizingShortfallEvaluator();
}
/* package */ void setSchedulingService(TaskSchedulingService service) {
shortfallEvaluator.setTaskSchedulingService(service);
}
Collection getRules() {
return Collections.unmodifiableCollection(autoScaleRules.getRules());
}
void replaceRule(AutoScaleRule rule) {
if(rule == null)
throw new NullPointerException("Can't add null rule");
autoScaleRules.replaceRule(rule);
}
AutoScaleRule getRule(String name) {
return autoScaleRules.get(name);
}
void removeRule(String ruleName) {
if(ruleName != null)
autoScaleRules.remRule(ruleName);
}
void setDelayScaleUpBySecs(long secs) {
delayScaleUpBySecs = secs;
}
void setDelayScaleDownBySecs(long secs) {
delayScaleDownBySecs = secs;
}
void setTaskToClustersGetter(Func1> getter) {
this.taskToClustersGetter = getter;
}
void scheduleAutoscale(final AutoScalerInput autoScalerInput) {
if(isShutdown.get())
return;
try {
executor.submit(() -> {
if(isShutdown.get())
return;
shortfallEvaluator.setTaskToClustersGetter(taskToClustersGetter);
autoScaleRules.prepare();
Map hostAttributeGroupMap = setupHostAttributeGroupMap(autoScaleRules, scalingActivityMap);
if (!disableShortfallEvaluation) {
Map shortfall = shortfallEvaluator.getShortfall(hostAttributeGroupMap.keySet(), autoScalerInput.getFailures(), autoScaleRules);
for (Map.Entry entry : shortfall.entrySet()) {
hostAttributeGroupMap.get(entry.getKey()).shortFall = entry.getValue() == null ? 0 : entry.getValue();
}
}
populateIdleResources(autoScalerInput.getIdleResourcesList(), hostAttributeGroupMap, attributeName);
for (HostAttributeGroup hostAttributeGroup : hostAttributeGroupMap.values()) {
processScalingNeeds(hostAttributeGroup, scalingActivityMap, assignableVMs);
}
});
}
catch (RejectedExecutionException e) {
logger.warn("Autoscaler execution request rejected: " + e.getMessage());
}
}
private boolean shouldScaleNow(boolean scaleUp, long now, ScalingActivity prevScalingActivity, AutoScaleRule rule) {
return scaleUp?
now > (Math.max(activeVmGroups.getLastSetAt(), prevScalingActivity.scaleUpAt) + rule.getCoolDownSecs() * 1000) :
now > (Math.max(activeVmGroups.getLastSetAt(), Math.max(prevScalingActivity.scaleDownAt, prevScalingActivity.scaleUpAt))
+ rule.getCoolDownSecs() * 1000);
}
private boolean shouldScaleUp(long now, ScalingActivity prevScalingActivity, AutoScaleRule rule) {
return shouldScaleNow(true, now, prevScalingActivity, rule);
}
private boolean shouldScaleDown(long now, ScalingActivity prevScalingActivity, AutoScaleRule rule) {
return shouldScaleNow(false, now, prevScalingActivity, rule);
}
private void processScalingNeeds(HostAttributeGroup hostAttributeGroup, ConcurrentMap scalingActivityMap, AssignableVMs assignableVMs) {
AutoScaleRule rule = hostAttributeGroup.rule;
long now = System.currentTimeMillis();
ScalingActivity prevScalingActivity= scalingActivityMap.get(rule.getRuleName());
int excess = hostAttributeGroup.shortFall>0? 0 : hostAttributeGroup.idleHosts.size() - rule.getMaxIdleHostsToKeep();
if (excess > 0 && shouldScaleDown(now, prevScalingActivity, rule)) {
ScalingActivity scalingActivity = scalingActivityMap.get(rule.getRuleName());
long lastReqstAge = (now - scalingActivity.scaleDownRequestedAt) / 1000L;
if(delayScaleDownBySecs>0L && lastReqstAge > 2 * delayScaleDownBySecs) { // reset the request at time
scalingActivity.scaleDownRequestedAt = now;
}
else if(delayScaleDownBySecs == 0L || lastReqstAge > delayScaleDownBySecs) {
final int size = vmCollection.size(rule.getRuleName());
if (rule.getMinSize() > (size - excess))
excess = Math.max(0, size - rule.getMinSize());
if (excess > 0) {
scalingActivity.scaleDownRequestedAt = 0L;
scalingActivity.scaleDownAt = now;
scalingActivity.shortfall = hostAttributeGroup.shortFall;
Map hostsToTerminate = getHostsToTerminate(hostAttributeGroup.idleHosts, excess);
scalingActivity.scaledNumInstances = hostsToTerminate.size();
scalingActivity.type = AutoScaleAction.Type.Down;
StringBuilder sBuilder = new StringBuilder();
for (String host : hostsToTerminate.keySet()) {
sBuilder.append(host).append(", ");
assignableVMs.disableUntil(host, now + rule.getCoolDownSecs() * 1000);
}
logger.info("Scaling down " + rule.getRuleName() + " by "
+ excess + " hosts (" + sBuilder.toString() + ")");
callback.call(
new ScaleDownAction(rule.getRuleName(), hostsToTerminate.values())
);
}
}
} else if(hostAttributeGroup.shortFall>0 || (excess<=0 && shouldScaleUp(now, prevScalingActivity, rule))) {
if (hostAttributeGroup.shortFall>0 || rule.getMinIdleHostsToKeep() > hostAttributeGroup.idleHosts.size()) {
// scale up to rule.getMaxIdleHostsToKeep() instead of just until rule.getMinIdleHostsToKeep()
// but, if not shouldScaleUp(), then, scale up due to shortfall
ScalingActivity scalingActivity = scalingActivityMap.get(rule.getRuleName());
long lastReqstAge = (now - scalingActivity.scaleUpRequestedAt) / 1000L;
if(delayScaleUpBySecs >0L && lastReqstAge > 2 * delayScaleUpBySecs) { // reset the request at time
scalingActivity.scaleUpRequestedAt = now;
}
else if(delayScaleUpBySecs ==0L || lastReqstAge > delayScaleUpBySecs) {
int shortage = (excess<=0 && shouldScaleUp(now, prevScalingActivity, rule))?
rule.getMaxIdleHostsToKeep() - hostAttributeGroup.idleHosts.size() :
0;
shortage = Math.max(shortage, hostAttributeGroup.shortFall);
final int size = vmCollection.size(rule.getRuleName());
if (shortage + size > rule.getMaxSize())
shortage = Math.max(0, rule.getMaxSize() - size);
if (shortage > 0) {
scalingActivity.scaleUpRequestedAt = 0L;
scalingActivity.scaleUpAt = now;
scalingActivity.shortfall = hostAttributeGroup.shortFall;
scalingActivity.scaledNumInstances = shortage;
scalingActivity.type = AutoScaleAction.Type.Up;
logger.info("Scaling up " + rule.getRuleName() + " by "
+ shortage + " hosts");
callback.call(
new ScaleUpAction(rule.getRuleName(), shortage)
);
}
}
}
}
}
private void populateIdleResources(List idleResources, Map leasesMap, String attributeName) {
for (VirtualMachineLease l : idleResources) {
if (l.getAttributeMap() != null && l.getAttributeMap().get(attributeName) != null &&
l.getAttributeMap().get(attributeName).getText().hasValue()) {
String attrValue = l.getAttributeMap().get(attributeName).getText().getValue();
if (leasesMap.containsKey(attrValue)) {
if (!leasesMap.get(attrValue).rule.idleMachineTooSmall(l))
leasesMap.get(attrValue).idleHosts.add(l);
}
}
}
}
private Map setupHostAttributeGroupMap(AutoScaleRules autoScaleRules, ConcurrentMap lastScalingAt) {
Map leasesMap = new HashMap<>();
for (AutoScaleRule rule : autoScaleRules.getRules()) {
leasesMap.put(rule.getRuleName(),
new HostAttributeGroup(rule.getRuleName(), rule));
long initialCoolDown = getInitialCoolDown(rule.getCoolDownSecs());
lastScalingAt.putIfAbsent(rule.getRuleName(), new ScalingActivity(initialCoolDown, initialCoolDown, 0, 0, null));
}
return leasesMap;
}
// make scaling activity happen after a fixed delayed time for the first time encountered (e.g., server start)
private long getInitialCoolDown(long coolDownSecs) {
long initialCoolDownInPastSecs=120;
initialCoolDownInPastSecs = Math.min(coolDownSecs, initialCoolDownInPastSecs);
return System.currentTimeMillis()- coolDownSecs*1000 + initialCoolDownInPastSecs*1000;
}
private Map getHostsToTerminate(List hosts, int excess) {
if(scaleDownConstraintExecutor == null) {
return getHostsToTerminateLegacy(hosts, excess);
} else {
return getHostsToTerminateUsingCriteria(hosts, excess);
}
}
private Map getHostsToTerminateUsingCriteria(List hosts, int excess) {
Map hostsMap = new HashMap<>();
List result = scaleDownConstraintExecutor.evaluate(hosts);
int removeLimit = Math.min(result.size(), excess);
for(int i = 0; i < removeLimit; i++) {
VirtualMachineLease lease = result.get(i);
hostsMap.put(lease.hostname(), getMappedHostname(lease));
}
return hostsMap;
}
private Map getHostsToTerminateLegacy(List hosts, int excess) {
Map result = new HashMap<>();
final Map> hostsMap = new HashMap<>();
final String defaultAttributeName = "default";
for(VirtualMachineLease host: hosts) {
final Protos.Attribute attribute = host.getAttributeMap().get(scaleDownBalancedByAttributeName);
String val = (attribute!=null && attribute.hasText())? attribute.getText().getValue() : defaultAttributeName;
if(hostsMap.get(val) == null)
hostsMap.put(val, new ArrayList());
hostsMap.get(val).add(host);
}
final List> lists = new ArrayList<>();
for(List l: hostsMap.values())
lists.add(l);
int taken=0;
while(taken takeFrom=null;
int max=0;
for(List l: lists) {
if(l.size()>max) {
max = l.size();
takeFrom = l;
}
}
final VirtualMachineLease removed = takeFrom.remove(0);
result.put(removed.hostname(), getMappedHostname(removed));
taken++;
}
return result;
}
private String getMappedHostname(VirtualMachineLease lease) {
if(mapHostnameAttributeName==null || mapHostnameAttributeName.isEmpty())
return lease.hostname();
Protos.Attribute attribute = lease.getAttributeMap().get(mapHostnameAttributeName);
if(attribute==null) {
logger.error("Didn't find attribute " + mapHostnameAttributeName + " for host " + lease.hostname());
return lease.hostname();
}
return attribute.getText().getValue();
}
public void setCallback(Action1 callback) {
this.callback = callback;
}
void shutdown() {
if(isShutdown.compareAndSet(false, true))
executor.shutdown();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy