Skip to content

Commit 84cf7c2

Browse files
frankmutengfmu
authored andcommitted
Make StickyRebalanceStrategy topology aware (#2944)
* Make StickyRebalanceStrategy topology aware * move canAdd() to a separate function for extendability * add todo * address comment and add test * constructor change for CapacityNode --------- Co-authored-by: Tengfei Mu <tmu@linkedin.com>
1 parent a52c9a6 commit 84cf7c2

6 files changed

Lines changed: 227 additions & 76 deletions

File tree

helix-core/src/main/java/org/apache/helix/controller/common/CapacityNode.java

Lines changed: 92 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,55 @@
2121

2222
import java.util.HashMap;
2323
import java.util.HashSet;
24+
import java.util.LinkedHashMap;
2425
import java.util.Map;
2526
import java.util.Set;
2627

28+
import org.apache.helix.controller.rebalancer.topology.Topology;
29+
import org.apache.helix.model.ClusterConfig;
30+
import org.apache.helix.model.ClusterTopologyConfig;
31+
import org.apache.helix.model.InstanceConfig;
32+
2733
/**
2834
* A Node is an entity that can serve capacity recording purpose. It has a capacity and knowledge
2935
* of partitions assigned to it, so it can decide if it can receive additional partitions.
3036
*/
31-
public class CapacityNode {
37+
public class CapacityNode implements Comparable<CapacityNode> {
3238
private int _currentlyAssigned;
3339
private int _capacity;
34-
private final String _id;
40+
private final String _instanceName;
41+
private final String _logicaId;
42+
private final String _faultZone;
3543
private final Map<String, Set<String>> _partitionMap;
3644

37-
public CapacityNode(String id) {
38-
_partitionMap = new HashMap<>();
39-
_currentlyAssigned = 0;
40-
this._id = id;
45+
/**
46+
* Constructor used for non-topology-aware use case
47+
* @param instanceName The instance name of this node
48+
* @param capacity The capacity of this node
49+
*/
50+
public CapacityNode(String instanceName, int capacity) {
51+
this(instanceName, null, null, null);
52+
this._capacity = capacity;
53+
}
54+
55+
/**
56+
* Constructor used for topology-aware use case
57+
* @param instanceName The instance name of this node
58+
* @param clusterConfig The cluster config for current helix cluster
59+
* @param clusterTopologyConfig The cluster topology config for current helix cluster
60+
* @param instanceConfig The instance config for current instance
61+
*/
62+
public CapacityNode(String instanceName, ClusterConfig clusterConfig,
63+
ClusterTopologyConfig clusterTopologyConfig, InstanceConfig instanceConfig) {
64+
this._instanceName = instanceName;
65+
this._logicaId = clusterTopologyConfig != null ? instanceConfig.getLogicalId(
66+
clusterTopologyConfig.getEndNodeType()) : instanceName;
67+
this._faultZone =
68+
clusterConfig != null ? computeFaultZone(clusterConfig, instanceConfig) : null;
69+
this._partitionMap = new HashMap<>();
70+
this._capacity =
71+
clusterConfig != null ? clusterConfig.getGlobalMaxPartitionAllowedPerInstance() : 0;
72+
this._currentlyAssigned = 0;
4173
}
4274

4375
/**
@@ -80,11 +112,27 @@ public void setCapacity(int capacity) {
80112
}
81113

82114
/**
83-
* Get the ID of this node
84-
* @return The ID of this node
115+
* Get the instance name of this node
116+
* @return The instance name of this node
85117
*/
86-
public String getId() {
87-
return _id;
118+
public String getInstanceName() {
119+
return _instanceName;
120+
}
121+
122+
/**
123+
* Get the logical id of this node
124+
* @return The logical id of this node
125+
*/
126+
public String getLogicalId() {
127+
return _logicaId;
128+
}
129+
130+
/**
131+
* Get the fault zone of this node
132+
* @return The fault zone of this node
133+
*/
134+
public String getFaultZone() {
135+
return _faultZone;
88136
}
89137

90138
/**
@@ -98,8 +146,40 @@ public int getCurrentlyAssigned() {
98146
@Override
99147
public String toString() {
100148
StringBuilder sb = new StringBuilder();
101-
sb.append("##########\nname=").append(_id).append("\nassigned:").append(_currentlyAssigned)
102-
.append("\ncapacity:").append(_capacity);
149+
sb.append("##########\nname=").append(_instanceName).append("\nassigned:")
150+
.append(_currentlyAssigned).append("\ncapacity:").append(_capacity).append("\nlogicalId:")
151+
.append(_logicaId).append("\nfaultZone:").append(_faultZone);
103152
return sb.toString();
104153
}
154+
155+
@Override
156+
public int compareTo(CapacityNode o) {
157+
if (_logicaId != null) {
158+
return _logicaId.compareTo(o.getLogicalId());
159+
}
160+
return _instanceName.compareTo(o.getInstanceName());
161+
}
162+
163+
/**
164+
* Computes the fault zone id based on the domain and fault zone type when topology is enabled.
165+
* For example, when
166+
* the domain is "zone=2, instance=testInstance" and the fault zone type is "zone", this function
167+
* returns "2".
168+
* If cannot find the fault zone type, this function leaves the fault zone id as the instance name.
169+
* TODO: change the return value to logical id when no fault zone type found. Also do the same for
170+
* waged rebalancer in helix-core/src/main/java/org/apache/helix/controller/rebalancer/waged/model/AssignableNode.java
171+
*/
172+
private String computeFaultZone(ClusterConfig clusterConfig, InstanceConfig instanceConfig) {
173+
LinkedHashMap<String, String> instanceTopologyMap =
174+
Topology.computeInstanceTopologyMap(clusterConfig, instanceConfig.getInstanceName(),
175+
instanceConfig, true /*earlyQuitTillFaultZone*/);
176+
177+
StringBuilder faultZoneStringBuilder = new StringBuilder();
178+
for (Map.Entry<String, String> entry : instanceTopologyMap.entrySet()) {
179+
faultZoneStringBuilder.append(entry.getValue());
180+
faultZoneStringBuilder.append('/');
181+
}
182+
faultZoneStringBuilder.setLength(faultZoneStringBuilder.length() - 1);
183+
return faultZoneStringBuilder.toString();
184+
}
105185
}

helix-core/src/main/java/org/apache/helix/controller/dataproviders/ResourceControllerDataProvider.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
import org.apache.helix.controller.rebalancer.waged.WagedResourceWeightsProvider;
4747
import org.apache.helix.controller.stages.CurrentStateOutput;
4848
import org.apache.helix.controller.stages.MissingTopStateRecord;
49+
import org.apache.helix.model.ClusterConfig;
50+
import org.apache.helix.model.ClusterTopologyConfig;
4951
import org.apache.helix.model.CustomizedState;
5052
import org.apache.helix.model.CustomizedStateConfig;
5153
import org.apache.helix.model.CustomizedView;
@@ -190,7 +192,7 @@ public synchronized void refresh(HelixDataAccessor accessor) {
190192

191193
if (getClusterConfig() != null
192194
&& getClusterConfig().getGlobalMaxPartitionAllowedPerInstance() != -1) {
193-
buildSimpleCapacityMap(getClusterConfig().getGlobalMaxPartitionAllowedPerInstance());
195+
buildSimpleCapacityMap();
194196
// Remove all cached IdealState because it is a global computation cannot partially be
195197
// performed for some resources. The computation is simple as well not taking too much resource
196198
// to recompute the assignments.
@@ -581,11 +583,16 @@ public WagedInstanceCapacity getWagedInstanceCapacity() {
581583
return _wagedInstanceCapacity;
582584
}
583585

584-
private void buildSimpleCapacityMap(int globalMaxPartitionAllowedPerInstance) {
586+
private void buildSimpleCapacityMap() {
587+
ClusterConfig clusterConfig = getClusterConfig();
588+
ClusterTopologyConfig clusterTopologyConfig =
589+
ClusterTopologyConfig.createFromClusterConfig(clusterConfig);
590+
Map<String, InstanceConfig> instanceConfigMap = getAssignableInstanceConfigMap();
585591
_simpleCapacitySet = new HashSet<>();
586-
for (String instance : getEnabledLiveInstances()) {
587-
CapacityNode capacityNode = new CapacityNode(instance);
588-
capacityNode.setCapacity(globalMaxPartitionAllowedPerInstance);
592+
for (String instanceName : getAssignableInstances()) {
593+
CapacityNode capacityNode =
594+
new CapacityNode(instanceName, clusterConfig, clusterTopologyConfig,
595+
instanceConfigMap.getOrDefault(instanceName, new InstanceConfig(instanceName)));
589596
_simpleCapacitySet.add(capacityNode);
590597
}
591598
}
@@ -599,7 +606,7 @@ public void populateSimpleCapacitySetUsage(final Set<String> resourceNameSet,
599606
// Convert the assignableNodes to map for quick lookup
600607
Map<String, CapacityNode> simpleCapacityMap = new HashMap<>();
601608
for (CapacityNode node : _simpleCapacitySet) {
602-
simpleCapacityMap.put(node.getId(), node);
609+
simpleCapacityMap.put(node.getInstanceName(), node);
603610
}
604611
for (String resourceName : resourceNameSet) {
605612
// Process current state mapping

helix-core/src/main/java/org/apache/helix/controller/rebalancer/strategy/StickyRebalanceStrategy.java

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
*/
2121

2222
import java.util.ArrayList;
23-
import java.util.Comparator;
2423
import java.util.HashMap;
2524
import java.util.HashSet;
2625
import java.util.LinkedHashMap;
@@ -74,34 +73,49 @@ public ZNRecord computePartitionAssignment(final List<String> allNodes,
7473
// Note the liveNodes parameter here might be processed within the rebalancer, e.g. filter based on tags
7574
Set<CapacityNode> assignableNodeSet = new HashSet<>(clusterData.getSimpleCapacitySet());
7675
Set<String> liveNodesSet = new HashSet<>(liveNodes);
77-
assignableNodeSet.removeIf(n -> !liveNodesSet.contains(n.getId()));
76+
assignableNodeSet.removeIf(n -> !liveNodesSet.contains(n.getInstanceName()));
77+
78+
// Convert the assignableNodes to map for quick lookup
79+
Map<String, CapacityNode> assignableNodeMap = assignableNodeSet.stream()
80+
.collect(Collectors.toMap(CapacityNode::getInstanceName, node -> node));
7881

7982
// Populate valid state map given current mapping
8083
Map<String, Set<String>> stateMap =
81-
populateValidAssignmentMapFromCurrentMapping(currentMapping, assignableNodeSet);
84+
populateValidAssignmentMapFromCurrentMapping(currentMapping, assignableNodeMap);
8285

8386
if (logger.isDebugEnabled()) {
8487
logger.debug("currentMapping: {}", currentMapping);
8588
logger.debug("stateMap: {}", stateMap);
8689
}
8790

8891
// Sort the assignable nodes by id
89-
List<CapacityNode> assignableNodeList =
90-
assignableNodeSet.stream().sorted(Comparator.comparing(CapacityNode::getId))
92+
List<CapacityNode> assignableNodeList = assignableNodeSet.stream().sorted()
9193
.collect(Collectors.toList());
9294

9395
// Assign partitions to node by order.
9496
for (int i = 0, index = 0; i < _partitions.size(); i++) {
9597
int startIndex = index;
98+
Map<String, Integer> currentFaultZoneCountMap = new HashMap<>();
9699
int remainingReplica = _statesReplicaCount;
97100
if (stateMap.containsKey(_partitions.get(i))) {
98-
remainingReplica = remainingReplica - stateMap.get(_partitions.get(i)).size();
101+
Set<String> existingReplicas = stateMap.get(_partitions.get(i));
102+
remainingReplica = remainingReplica - existingReplicas.size();
103+
for (String instanceName : existingReplicas) {
104+
String faultZone = assignableNodeMap.get(instanceName).getFaultZone();
105+
currentFaultZoneCountMap.put(faultZone,
106+
currentFaultZoneCountMap.getOrDefault(faultZone, 0) + 1);
107+
}
99108
}
100109
for (int j = 0; j < remainingReplica; j++) {
101110
while (index - startIndex < assignableNodeList.size()) {
102111
CapacityNode node = assignableNodeList.get(index++ % assignableNodeList.size());
103-
if (node.canAdd(_resourceName, _partitions.get(i))) {
104-
stateMap.computeIfAbsent(_partitions.get(i), m -> new HashSet<>()).add(node.getId());
112+
if (this.canAdd(node, _partitions.get(i), currentFaultZoneCountMap)) {
113+
stateMap.computeIfAbsent(_partitions.get(i), m -> new HashSet<>())
114+
.add(node.getInstanceName());
115+
if (node.getFaultZone() != null) {
116+
currentFaultZoneCountMap.put(node.getFaultZone(),
117+
currentFaultZoneCountMap.getOrDefault(node.getFaultZone(), 0) + 1);
118+
}
105119
break;
106120
}
107121
}
@@ -126,16 +140,13 @@ public ZNRecord computePartitionAssignment(final List<String> allNodes,
126140
* Populates a valid state map from the current mapping, filtering out invalid nodes.
127141
*
128142
* @param currentMapping the current mapping of partitions to node states
129-
* @param assignableNodes the list of nodes that can be assigned
143+
* @param assignableNodeMap the map of instance name -> nodes that can be assigned
130144
* @return a map of partitions to valid nodes
131145
*/
132146
private Map<String, Set<String>> populateValidAssignmentMapFromCurrentMapping(
133147
final Map<String, Map<String, String>> currentMapping,
134-
final Set<CapacityNode> assignableNodes) {
148+
final Map<String, CapacityNode> assignableNodeMap) {
135149
Map<String, Set<String>> validAssignmentMap = new HashMap<>();
136-
// Convert the assignableNodes to map for quick lookup
137-
Map<String, CapacityNode> assignableNodeMap =
138-
assignableNodes.stream().collect(Collectors.toMap(CapacityNode::getId, node -> node));
139150
if (currentMapping != null) {
140151
for (Map.Entry<String, Map<String, String>> entry : currentMapping.entrySet()) {
141152
String partition = entry.getKey();
@@ -167,4 +178,22 @@ private boolean isValidNodeAssignment(final String partition, final String nodeI
167178
return node != null && (node.hasPartition(_resourceName, partition) || node.canAdd(
168179
_resourceName, partition));
169180
}
181+
182+
/**
183+
* Checks if it's valid to assign the partition to node
184+
*
185+
* @param node node to assign partition
186+
* @param partition partition name
187+
* @param currentFaultZoneCountMap the map of fault zones -> count
188+
* @return true if it's valid to assign the partition to node, false otherwise
189+
*/
190+
protected boolean canAdd(CapacityNode node, String partition,
191+
Map<String, Integer> currentFaultZoneCountMap) {
192+
// Valid assignment when following conditions match:
193+
// 1. Replica is not within the same fault zones of other replicas
194+
// 2. Node has capacity to hold the replica
195+
return !currentFaultZoneCountMap.containsKey(node.getFaultZone()) && node.canAdd(_resourceName,
196+
partition);
197+
}
170198
}
199+

helix-core/src/test/java/org/apache/helix/common/ZkTestBase.java

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -367,14 +367,6 @@ protected void setLastOnDemandRebalanceTimeInCluster(HelixZkClient zkClient,
367367
configAccessor.setClusterConfig(clusterName, clusterConfig);
368368
}
369369

370-
protected void setGlobalMaxPartitionAllowedPerInstanceInCluster(HelixZkClient zkClient,
371-
String clusterName, int maxPartitionAllowed) {
372-
ConfigAccessor configAccessor = new ConfigAccessor(zkClient);
373-
ClusterConfig clusterConfig = configAccessor.getClusterConfig(clusterName);
374-
clusterConfig.setGlobalMaxPartitionAllowedPerInstance(maxPartitionAllowed);
375-
configAccessor.setClusterConfig(clusterName, clusterConfig);
376-
}
377-
378370
protected IdealState createResourceWithDelayedRebalance(String clusterName, String db,
379371
String stateModel, int numPartition, int replica, int minActiveReplica, long delay) {
380372
return createResourceWithDelayedRebalance(clusterName, db, stateModel, numPartition, replica,

helix-core/src/test/java/org/apache/helix/controller/rebalancer/TestStickyRebalanceStrategy.java

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,12 @@ public void testAssignmentWithGlobalPartitionLimit() {
5353

5454
Set<CapacityNode> capacityNodeSet = new HashSet<>();
5555
for (int i = 0; i < 5; i++) {
56-
CapacityNode capacityNode = new CapacityNode("Node-" + i);
57-
capacityNode.setCapacity(1);
56+
CapacityNode capacityNode = new CapacityNode("Node-" + i, 1);
5857
capacityNodeSet.add(capacityNode);
5958
}
6059

6160
List<String> liveNodes =
62-
capacityNodeSet.stream().map(CapacityNode::getId).collect(Collectors.toList());
61+
capacityNodeSet.stream().map(CapacityNode::getInstanceName).collect(Collectors.toList());
6362

6463
List<String> partitions = new ArrayList<>();
6564
for (int i = 0; i < 3; i++) {
@@ -97,13 +96,12 @@ public void testStickyAssignment() {
9796

9897
Set<CapacityNode> capacityNodeSet = new HashSet<>();
9998
for (int i = 0; i < nNode; i++) {
100-
CapacityNode capacityNode = new CapacityNode("Node-" + i);
101-
capacityNode.setCapacity(1);
99+
CapacityNode capacityNode = new CapacityNode("Node-" + i, 1);
102100
capacityNodeSet.add(capacityNode);
103101
}
104102

105103
List<String> liveNodes =
106-
capacityNodeSet.stream().map(CapacityNode::getId).collect(Collectors.toList());
104+
capacityNodeSet.stream().map(CapacityNode::getInstanceName).collect(Collectors.toList());
107105

108106
List<String> partitions = new ArrayList<>();
109107
for (int i = 0; i < nPartitions; i++) {
@@ -150,40 +148,39 @@ public void testStickyAssignmentMultipleTimes() {
150148

151149
Set<CapacityNode> capacityNodeSet = new HashSet<>();
152150
for (int i = 0; i < nNode; i++) {
153-
CapacityNode capacityNode = new CapacityNode("Node-" + i);
154-
capacityNode.setCapacity(1);
151+
CapacityNode capacityNode = new CapacityNode("Node-" + i, 1);
155152
capacityNodeSet.add(capacityNode);
156153
}
157154

158155
List<String> liveNodes =
159-
capacityNodeSet.stream().map(CapacityNode::getId).collect(Collectors.toList());
156+
capacityNodeSet.stream().map(CapacityNode::getInstanceName).collect(Collectors.toList());
160157

161158
List<String> partitions = new ArrayList<>();
162159
for (int i = 0; i < nPartitions; i++) {
163160
partitions.add(TEST_RESOURCE_PREFIX + i);
164161
}
165162
when(clusterDataCache.getSimpleCapacitySet()).thenReturn(capacityNodeSet);
166163

167-
StickyRebalanceStrategy greedyRebalanceStrategy = new StickyRebalanceStrategy();
168-
greedyRebalanceStrategy.init(TEST_RESOURCE_PREFIX + 0, partitions, states, 1);
164+
StickyRebalanceStrategy stickyRebalanceStrategy = new StickyRebalanceStrategy();
165+
stickyRebalanceStrategy.init(TEST_RESOURCE_PREFIX + 0, partitions, states, 1);
169166
// First round assignment computation:
170167
// 1. Without previous assignment (currentMapping is null)
171168
// 2. Without enough assignable nodes
172169
ZNRecord firstRoundShardAssignment =
173-
greedyRebalanceStrategy.computePartitionAssignment(null, liveNodes, null, clusterDataCache);
170+
stickyRebalanceStrategy.computePartitionAssignment(null, liveNodes, null, clusterDataCache);
174171

175172
// Assert only 3 partitions are fulfilled with assignment
176173
Assert.assertEquals(firstRoundShardAssignment.getListFields().entrySet().stream()
177174
.filter(e -> e.getValue().size() == nReplicas).count(), 3);
178175

179176
// Assign 4 more nodes which is used in second round assignment computation
180177
for (int i = nNode; i < nNode + 4; i++) {
181-
CapacityNode capacityNode = new CapacityNode("Node-" + i);
182-
capacityNode.setCapacity(1);
178+
CapacityNode capacityNode = new CapacityNode("Node-" + i, 1);
183179
capacityNodeSet.add(capacityNode);
184180
}
185181

186-
liveNodes = capacityNodeSet.stream().map(CapacityNode::getId).collect(Collectors.toList());
182+
liveNodes =
183+
capacityNodeSet.stream().map(CapacityNode::getInstanceName).collect(Collectors.toList());
187184

188185
// Populate previous assignment (currentMapping) with first round assignment computation result
189186
Map<String, Map<String, String>> currentMapping = new HashMap<>();
@@ -199,7 +196,7 @@ public void testStickyAssignmentMultipleTimes() {
199196
// 1. With previous assignment (currentMapping)
200197
// 2. With enough assignable nodes
201198
ZNRecord secondRoundShardAssignment =
202-
greedyRebalanceStrategy.computePartitionAssignment(null, liveNodes, currentMapping,
199+
stickyRebalanceStrategy.computePartitionAssignment(null, liveNodes, currentMapping,
203200
clusterDataCache);
204201

205202
// Assert all partitions have been assigned with enough replica

0 commit comments

Comments
 (0)