Adding ability to handle orphaned devices when balancing mastership.

Change-Id: I01dd7a3074475d79504d516fbd3fd32ef18770ce
This commit is contained in:
Thomas Vachuska 2017-11-06 15:17:06 -08:00
parent b5e6e06634
commit f2e09cb477
2 changed files with 149 additions and 67 deletions

View File

@ -57,6 +57,7 @@ import org.slf4j.Logger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@ -67,6 +68,7 @@ import java.util.concurrent.CompletableFuture;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Lists.newArrayList;
import static java.util.concurrent.CompletableFuture.allOf;
import static org.onlab.metrics.MetricsUtil.startTimer;
import static org.onlab.metrics.MetricsUtil.stopTimer;
import static org.onosproject.net.MastershipRole.MASTER;
@ -76,13 +78,15 @@ import static org.onosproject.security.AppPermission.Type.CLUSTER_WRITE;
import static org.slf4j.LoggerFactory.getLogger;
/**
* Component providing the node-device mastership service.
*/
@Component(immediate = true)
@Service
public class MastershipManager
extends AbstractListenerManager<MastershipEvent, MastershipListener>
implements MastershipService, MastershipAdminService, MastershipTermService,
MetricsHelper {
extends AbstractListenerManager<MastershipEvent, MastershipListener>
implements MastershipService, MastershipAdminService, MastershipTermService,
MetricsHelper {
private static final String NODE_ID_NULL = "Node ID cannot be null";
private static final String DEVICE_ID_NULL = "Device ID cannot be null";
@ -116,7 +120,7 @@ public class MastershipManager
static final boolean DEFAULT_USE_REGION_FOR_BALANCE_ROLES = false;
@Property(name = "useRegionForBalanceRoles", boolValue = DEFAULT_USE_REGION_FOR_BALANCE_ROLES,
label = "Use Regions for balancing roles")
label = "Use Regions for balancing roles")
protected boolean useRegionForBalanceRoles;
private static final boolean DEFAULT_REBALANCE_ROLES_ON_UPGRADE = true;
@ -181,7 +185,7 @@ public class MastershipManager
}
return eventFuture.thenAccept(this::post)
.thenApply(v -> null);
.thenApply(v -> null);
}
@Override
@ -196,8 +200,8 @@ public class MastershipManager
public CompletableFuture<Void> relinquishMastership(DeviceId deviceId) {
checkPermission(CLUSTER_WRITE);
return store.relinquishRole(localNodeId, deviceId)
.thenAccept(this::post)
.thenApply(v -> null);
.thenAccept(this::post)
.thenApply(v -> null);
}
@Override
@ -249,15 +253,20 @@ public class MastershipManager
public void balanceRoles() {
List<ControllerNode> nodes = newArrayList(clusterService.getNodes());
Map<ControllerNode, Set<DeviceId>> controllerDevices = new HashMap<>();
Set<DeviceId> orphanedDevices = Sets.newHashSet();
int deviceCount = 0;
// Create buckets reflecting current ownership.
// Create buckets reflecting current ownership; do this irrespective of
// whether the node is active.
for (ControllerNode node : nodes) {
Set<DeviceId> devicesOf = new HashSet<>(getDevicesOf(node.id()));
if (clusterService.getState(node.id()).isActive()) {
Set<DeviceId> devicesOf = new HashSet<>(getDevicesOf(node.id()));
log.info("Node {} has {} devices.", node.id(), devicesOf.size());
deviceCount += devicesOf.size();
controllerDevices.put(node, devicesOf);
log.info("Node {} has {} devices.", node.id(), devicesOf.size());
} else if (!devicesOf.isEmpty()) {
log.warn("Inactive node {} has {} orphaned devices.", node.id(), devicesOf.size());
orphanedDevices.addAll(getDevicesOf(node.id()));
}
}
@ -265,11 +274,16 @@ public class MastershipManager
return;
}
// Now re-balance the buckets until they are roughly even.
List<CompletableFuture<Void>> balanceBucketsFutures = balanceControllerNodes(controllerDevices, deviceCount);
List<CompletableFuture<Void>> balanceBucketsFutures = Lists.newLinkedList();
CompletableFuture<Void> balanceRolesFuture = CompletableFuture.allOf(
balanceBucketsFutures.toArray(new CompletableFuture[balanceBucketsFutures.size()]));
// First re-balance the buckets until they are roughly even.
balanceControllerNodes(controllerDevices, deviceCount, balanceBucketsFutures);
// Then attempt to distribute any orphaned devices among the buckets.
distributeOrphanedDevices(controllerDevices, orphanedDevices, balanceBucketsFutures);
CompletableFuture<Void> balanceRolesFuture =
allOf(balanceBucketsFutures.toArray(new CompletableFuture[balanceBucketsFutures.size()]));
Futures.getUnchecked(balanceRolesFuture);
}
@ -278,32 +292,51 @@ public class MastershipManager
* Balances the nodes specified in controllerDevices.
*
* @param controllerDevices controller nodes to devices map
* @param deviceCount number of devices mastered by controller nodes
* @return list of setRole futures for "moved" devices
* @param deviceCount number of devices mastered by controller nodes
* @param futures list of setRole futures for "moved" devices
*/
private List<CompletableFuture<Void>> balanceControllerNodes(
Map<ControllerNode, Set<DeviceId>> controllerDevices, int deviceCount) {
private void balanceControllerNodes(Map<ControllerNode, Set<DeviceId>> controllerDevices,
int deviceCount,
List<CompletableFuture<Void>> futures) {
// Now re-balance the buckets until they are roughly even.
List<CompletableFuture<Void>> balanceBucketsFutures = Lists.newLinkedList();
int rounds = controllerDevices.keySet().size();
for (int i = 0; i < rounds; i++) {
// Iterate over the buckets and find the smallest and the largest.
ControllerNode smallest = findBucket(true, controllerDevices);
ControllerNode largest = findBucket(false, controllerDevices);
balanceBucketsFutures.add(balanceBuckets(smallest, largest, controllerDevices, deviceCount));
futures.add(balanceBuckets(smallest, largest, controllerDevices, deviceCount));
}
}
/**
* Uses the set of orphaned devices to even out the load among the controllers.
*
* @param controllerDevices controller nodes to devices map
* @param orphanedDevices set of orphaned devices without an active master
* @param futures list of completable future to track the progress of the balancing operation
*/
private void distributeOrphanedDevices(Map<ControllerNode, Set<DeviceId>> controllerDevices,
Set<DeviceId> orphanedDevices,
List<CompletableFuture<Void>> futures) {
// Now re-distribute the orphaned devices into buckets until they are roughly even.
while (!orphanedDevices.isEmpty()) {
// Iterate over the buckets and find the smallest bucket.
ControllerNode smallest = findBucket(true, controllerDevices);
changeMastership(smallest, controllerDevices.get(smallest),
orphanedDevices, 1, futures);
}
return balanceBucketsFutures;
}
/**
* Finds node with the minimum/maximum devices from a list of nodes.
*
* @param min true: minimum, false: maximum
* @param min true: minimum, false: maximum
* @param controllerDevices controller nodes to devices map
* @return controller node with minimum/maximum devices
*/
private ControllerNode findBucket(boolean min,
Map<ControllerNode, Set<DeviceId>> controllerDevices) {
Map<ControllerNode, Set<DeviceId>> controllerDevices) {
int xSize = min ? Integer.MAX_VALUE : -1;
ControllerNode xNode = null;
for (ControllerNode node : controllerDevices.keySet()) {
@ -319,15 +352,15 @@ public class MastershipManager
/**
* Balance the node buckets by moving devices from largest to smallest node.
*
* @param smallest node that is master of the smallest number of devices
* @param largest node that is master of the largest number of devices
* @param smallest node that is master of the smallest number of devices
* @param largest node that is master of the largest number of devices
* @param controllerDevices controller nodes to devices map
* @param deviceCount number of devices mastered by controller nodes
* @param deviceCount number of devices mastered by controller nodes
* @return list of setRole futures for "moved" devices
*/
private CompletableFuture<Void> balanceBuckets(ControllerNode smallest, ControllerNode largest,
Map<ControllerNode, Set<DeviceId>> controllerDevices,
int deviceCount) {
Map<ControllerNode, Set<DeviceId>> controllerDevices,
int deviceCount) {
Collection<DeviceId> minBucket = controllerDevices.get(smallest);
Collection<DeviceId> maxBucket = controllerDevices.get(largest);
int bucketCount = controllerDevices.keySet().size();
@ -340,20 +373,36 @@ public class MastershipManager
if (delta > 0) {
log.info("Attempting to move {} nodes from {} to {}...", delta,
largest.id(), smallest.id());
int i = 0;
Iterator<DeviceId> it = maxBucket.iterator();
while (it.hasNext() && i < delta) {
DeviceId deviceId = it.next();
log.info("Setting {} as the master for {}", smallest.id(), deviceId);
setRoleFutures.add(setRole(smallest.id(), deviceId, MASTER));
controllerDevices.get(smallest).add(deviceId);
it.remove();
i++;
}
changeMastership(smallest, minBucket, maxBucket, delta, setRoleFutures);
}
return CompletableFuture.allOf(setRoleFutures.toArray(new CompletableFuture[setRoleFutures.size()]));
return allOf(setRoleFutures.toArray(new CompletableFuture[setRoleFutures.size()]));
}
/**
* Changes mastership for the specified number of devices in the given source
* bucket to the specified node and ads those devices to the given target
* bucket. Also adds the futures for tracking the role reassignment progress.
*
* @param toNode target controller node
* @param toBucket target bucket
* @param fromBucket source bucket
* @param count number of devices
* @param futures futures for tracking operation progress
*/
private void changeMastership(ControllerNode toNode, Collection<DeviceId> toBucket,
Collection<DeviceId> fromBucket, int count,
List<CompletableFuture<Void>> futures) {
int i = 0;
Iterator<DeviceId> it = fromBucket.iterator();
while (it.hasNext() && i < count) {
DeviceId deviceId = it.next();
log.info("Setting {} as the master for {}", toNode.id(), deviceId);
futures.add(setRole(toNode.id(), deviceId, MASTER));
toBucket.add(deviceId);
it.remove();
i++;
}
}
/**
@ -368,7 +417,7 @@ public class MastershipManager
return false; // no balancing was done using regions.
}
// handle nodes belonging to regions
// Handle nodes belonging to regions
Set<ControllerNode> nodesInRegions = Sets.newHashSet();
for (Region region : regions) {
Map<ControllerNode, Set<DeviceId>> activeRegionControllers =
@ -376,20 +425,20 @@ public class MastershipManager
nodesInRegions.addAll(activeRegionControllers.keySet());
}
// handle nodes not belonging to any region
// Handle nodes not belonging to any region
Set<ControllerNode> nodesNotInRegions = Sets.difference(allControllerDevices.keySet(), nodesInRegions);
if (!nodesNotInRegions.isEmpty()) {
int deviceCount = 0;
Map<ControllerNode, Set<DeviceId>> controllerDevicesNotInRegions = new HashMap<>();
for (ControllerNode controllerNode: nodesNotInRegions) {
for (ControllerNode controllerNode : nodesNotInRegions) {
controllerDevicesNotInRegions.put(controllerNode, allControllerDevices.get(controllerNode));
deviceCount += allControllerDevices.get(controllerNode).size();
}
// Now re-balance the buckets until they are roughly even.
List<CompletableFuture<Void>> balanceBucketsFutures =
balanceControllerNodes(controllerDevicesNotInRegions, deviceCount);
List<CompletableFuture<Void>> balanceBucketsFutures = Lists.newArrayList();
balanceControllerNodes(controllerDevicesNotInRegions, deviceCount, balanceBucketsFutures);
CompletableFuture<Void> balanceRolesFuture = CompletableFuture.allOf(
CompletableFuture<Void> balanceRolesFuture = allOf(
balanceBucketsFutures.toArray(new CompletableFuture[balanceBucketsFutures.size()]));
Futures.getUnchecked(balanceRolesFuture);
@ -400,14 +449,15 @@ public class MastershipManager
/**
* Balances the nodes in specified region.
*
* @param region region in which nodes are to be balanced
* @param region region in which nodes are to be balanced
* @param allControllerDevices controller nodes to devices map
* @return controller nodes that were balanced
*/
private Map<ControllerNode, Set<DeviceId>> balanceRolesInRegion(Region region,
Map<ControllerNode, Set<DeviceId>> allControllerDevices) {
private Map<ControllerNode, Set<DeviceId>>
balanceRolesInRegion(Region region,
Map<ControllerNode, Set<DeviceId>> allControllerDevices) {
// retrieve all devices associated with specified region
// Retrieve all devices associated with specified region
Set<DeviceId> devicesInRegion = regionService.getRegionDevices(region.id());
log.info("Region {} has {} devices.", region.id(), devicesInRegion.size());
if (devicesInRegion.isEmpty()) {
@ -421,24 +471,22 @@ public class MastershipManager
return new HashMap<>(); // for now just leave devices alone
}
// get the region's preferred set of masters
// Get the region's preferred set of masters
Set<DeviceId> devicesInMasters = Sets.newHashSet();
Map<ControllerNode, Set<DeviceId>> regionalControllerDevices =
getRegionsPreferredMasters(region, devicesInMasters, allControllerDevices);
// Now re-balance the buckets until they are roughly even.
List<CompletableFuture<Void>> balanceBucketsFutures =
balanceControllerNodes(regionalControllerDevices, devicesInMasters.size());
List<CompletableFuture<Void>> balanceBucketsFutures = Lists.newArrayList();
balanceControllerNodes(regionalControllerDevices, devicesInMasters.size(), balanceBucketsFutures);
// handle devices that are not currently mastered by the master node set
// Handle devices that are not currently mastered by the master node set
Set<DeviceId> devicesNotMasteredWithControllers = Sets.difference(devicesInRegion, devicesInMasters);
if (!devicesNotMasteredWithControllers.isEmpty()) {
// active controllers in master node set are already balanced, just
// assign device mastership in sequence
List<ControllerNode> sorted = new ArrayList<>(regionalControllerDevices.keySet());
Collections.sort(sorted, (o1, o2) ->
((Integer) (regionalControllerDevices.get(o1)).size())
.compareTo((Integer) (regionalControllerDevices.get(o2)).size()));
Collections.sort(sorted, Comparator.comparingInt(o -> (regionalControllerDevices.get(o)).size()));
int deviceIndex = 0;
for (DeviceId deviceId : devicesNotMasteredWithControllers) {
ControllerNode cnode = sorted.get(deviceIndex % sorted.size());
@ -448,12 +496,12 @@ public class MastershipManager
}
}
CompletableFuture<Void> balanceRolesFuture = CompletableFuture.allOf(
balanceBucketsFutures.toArray(new CompletableFuture[balanceBucketsFutures.size()]));
CompletableFuture<Void> balanceRolesFuture =
allOf(balanceBucketsFutures.toArray(new CompletableFuture[balanceBucketsFutures.size()]));
Futures.getUnchecked(balanceRolesFuture);
// update the map before returning
// Update the map before returning
regionalControllerDevices.forEach((controllerNode, deviceIds) -> {
regionalControllerDevices.put(controllerNode, new HashSet<>(getDevicesOf(controllerNode.id())));
});
@ -465,17 +513,18 @@ public class MastershipManager
* Get region's preferred set of master nodes - the first master node set that has at
* least one active node.
*
* @param region region for which preferred set of master nodes is requested
* @param devicesInMasters device set to track devices in preferred set of master nodes
* @param region region for which preferred set of master nodes is requested
* @param devicesInMasters device set to track devices in preferred set of master nodes
* @param allControllerDevices controller nodes to devices map
* @return region's preferred master nodes (and devices that use them as masters)
*/
private Map<ControllerNode, Set<DeviceId>> getRegionsPreferredMasters(Region region,
Set<DeviceId> devicesInMasters,
Map<ControllerNode, Set<DeviceId>> allControllerDevices) {
private Map<ControllerNode, Set<DeviceId>>
getRegionsPreferredMasters(Region region,
Set<DeviceId> devicesInMasters,
Map<ControllerNode, Set<DeviceId>> allControllerDevices) {
Map<ControllerNode, Set<DeviceId>> regionalControllerDevices = new HashMap<>();
int listIndex = 0;
for (Set<NodeId> masterSet: region.masters()) {
for (Set<NodeId> masterSet : region.masters()) {
log.info("Region {} masters set {} has {} nodes.",
region.id(), listIndex, masterSet.size());
if (masterSet.isEmpty()) { // nothing on this level

View File

@ -78,6 +78,9 @@ public class MastershipManagerTest {
private static final DeviceId DID1 = DeviceId.deviceId("foo:d1");
private static final DeviceId DID2 = DeviceId.deviceId("foo:d2");
private static final DeviceId DID3 = DeviceId.deviceId("foo:d3");
private static final DeviceId DID4 = DeviceId.deviceId("foo:d4");
private static final DeviceId DID5 = DeviceId.deviceId("foo:d5");
private static final DeviceId DID6 = DeviceId.deviceId("foo:d6");
private static final NodeId NID1 = NodeId.nodeId("n1");
private static final NodeId NID2 = NodeId.nodeId("n2");
private static final NodeId NID3 = NodeId.nodeId("n3");
@ -219,6 +222,36 @@ public class MastershipManagerTest {
assertEquals("inconsistent terms: ", 3, ts.getMastershipTerm(DEV_MASTER).termNumber());
}
@Test
public void balanceWithOrphans() {
// Setup cluster of three nodes
testClusterService.put(CNODE1, ControllerNode.State.ACTIVE);
testClusterService.put(CNODE2, ControllerNode.State.INACTIVE);
testClusterService.put(CNODE3, ControllerNode.State.ACTIVE);
// Pre-assign some devices to each of the node
// Leave some devices as orphans assigned to a downed node
assignRoles(NID1, ImmutableSet.of(DID1, DID2, DID3, DID4));
assignRoles(NID2, ImmutableSet.of(DID5));
assignRoles(NID3, ImmutableSet.of(DID6));
// Trigger load balancing
mgr.balanceRoles();
// Make sure we have a balanced load
// Make sure that we no longer have any orphans
assertEquals("incorrect balance for node 1", 3, mgr.getDevicesOf(NID1).size());
assertEquals("incorrect balance for node 2", 0, mgr.getDevicesOf(NID2).size());
assertEquals("incorrect balance for node 3", 3, mgr.getDevicesOf(NID3).size());
}
private void assignRoles(NodeId nid, Set<DeviceId> deviceIds) {
Set<DeviceId> all = ImmutableSet.of(DID1, DID2, DID3, DID4, DID5, DID6);
for (DeviceId did : all) {
mgr.setRole(nid, did, deviceIds.contains(did) ? MASTER : STANDBY);
}
}
@Test
public void balanceWithRegion1() {
//set up region - 2 sets of masters with 1 node in each