/*
* Copyright 2013 LinkedIn, 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 voldemort.utils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import voldemort.cluster.Cluster;
import voldemort.cluster.Node;
import voldemort.cluster.Zone;
import voldemort.routing.StoreRoutingPlan;
import voldemort.store.StoreDefinition;
import voldemort.store.StoreUtils;
import com.google.common.collect.Maps;
/**
* PartitionBalanceUtils provides helper methods for interpreting, analyzing,
* and printing partition information.
*
* Most of these helper methods take one Cluster object, and possibly some other
* minor, simple arguments. The Cluster object defines the partition layout
* which is being interpreted/analzyed/printed.
*/
public class PartitionBalanceUtils {
/**
* Compress contiguous partitions into format "e-i" instead of
* "e, f, g, h, i". This helps illustrate contiguous partitions within a
* zone.
*
* @param cluster
* @param zoneId
* @return pretty string of partitions per zone
*/
public static String compressedListOfPartitionsInZone(final Cluster cluster, int zoneId) {
Map<Integer, Integer> idToRunLength = PartitionBalanceUtils.getMapOfContiguousPartitions(cluster,
zoneId);
StringBuilder sb = new StringBuilder();
sb.append("[");
boolean first = true;
Set<Integer> sortedInitPartitionIds = new TreeSet<Integer>(idToRunLength.keySet());
for(int initPartitionId: sortedInitPartitionIds) {
if(!first) {
sb.append(", ");
} else {
first = false;
}
int runLength = idToRunLength.get(initPartitionId);
if(runLength == 1) {
sb.append(initPartitionId);
} else {
int endPartitionId = (initPartitionId + runLength - 1)
% cluster.getNumberOfPartitions();
sb.append(initPartitionId).append("-").append(endPartitionId);
}
}
sb.append("]");
return sb.toString();
}
/**
* Determines run length for each 'initial' partition ID. Note that a
* contiguous run may "wrap around" the end of the ring.
*
* @param cluster
* @param zoneId
* @return map of initial partition Id to length of contiguous run of
* partition IDs within the same zone..
*/
public static Map<Integer, Integer> getMapOfContiguousPartitions(final Cluster cluster,
int zoneId) {
List<Integer> partitionIds = new ArrayList<Integer>(cluster.getPartitionIdsInZone(zoneId));
Map<Integer, Integer> partitionIdToRunLength = Maps.newHashMap();
if(partitionIds.isEmpty()) {
return partitionIdToRunLength;
}
int lastPartitionId = partitionIds.get(0);
int initPartitionId = lastPartitionId;
for(int offset = 1; offset < partitionIds.size(); offset++) {
int partitionId = partitionIds.get(offset);
if(partitionId == lastPartitionId + 1) {
lastPartitionId = partitionId;
continue;
}
int runLength = lastPartitionId - initPartitionId + 1;
partitionIdToRunLength.put(initPartitionId, runLength);
initPartitionId = partitionId;
lastPartitionId = initPartitionId;
}
int runLength = lastPartitionId - initPartitionId + 1;
if(lastPartitionId == cluster.getNumberOfPartitions() - 1
&& partitionIdToRunLength.containsKey(0)) {
// special case of contiguity that wraps around the ring.
partitionIdToRunLength.put(initPartitionId, runLength + partitionIdToRunLength.get(0));
partitionIdToRunLength.remove(0);
} else {
partitionIdToRunLength.put(initPartitionId, runLength);
}
return partitionIdToRunLength;
}
/**
* Determines a histogram of contiguous runs of partitions within a zone.
* I.e., for each run length of contiguous partitions, how many such runs
* are there.
*
* Does not correctly address "wrap around" of partition IDs (i.e., the fact
* that partition ID 0 is "next" to partition ID 'max')
*
* @param cluster
* @param zoneId
* @return map of length of contiguous run of partitions to count of number
* of such runs.
*/
public static Map<Integer, Integer>
getMapOfContiguousPartitionRunLengths(final Cluster cluster, int zoneId) {
Map<Integer, Integer> idToRunLength = getMapOfContiguousPartitions(cluster, zoneId);
Map<Integer, Integer> runLengthToCount = Maps.newHashMap();
if(idToRunLength.isEmpty()) {
return runLengthToCount;
}
for(int runLength: idToRunLength.values()) {
if(!runLengthToCount.containsKey(runLength)) {
runLengthToCount.put(runLength, 0);
}
runLengthToCount.put(runLength, runLengthToCount.get(runLength) + 1);
}
return runLengthToCount;
}
/**
* Pretty prints the output of getMapOfContiguousPartitionRunLengths
*
* @param cluster
* @param zoneId
* @return pretty string of contiguous run lengths
*/
public static String getPrettyMapOfContiguousPartitionRunLengths(final Cluster cluster,
int zoneId) {
Map<Integer, Integer> runLengthToCount = getMapOfContiguousPartitionRunLengths(cluster,
zoneId);
String prettyHistogram = "[";
boolean first = true;
Set<Integer> runLengths = new TreeSet<Integer>(runLengthToCount.keySet());
for(int runLength: runLengths) {
if(first) {
first = false;
} else {
prettyHistogram += ", ";
}
prettyHistogram += "{" + runLength + " : " + runLengthToCount.get(runLength) + "}";
}
prettyHistogram += "]";
return prettyHistogram;
}
/**
* Returns a pretty printed string of nodes that host specific "hot"
* partitions, where hot is defined as following a contiguous run of
* partitions of some length in another zone.
*
* @param cluster The cluster to analyze
* @param hotContiguityCutoff cutoff below which a contiguous run is not
* hot.
* @return pretty string of hot partitions
*/
public static String getHotPartitionsDueToContiguity(final Cluster cluster,
int hotContiguityCutoff) {
StringBuilder sb = new StringBuilder();
for(int zoneId: cluster.getZoneIds()) {
Map<Integer, Integer> idToRunLength = getMapOfContiguousPartitions(cluster, zoneId);
for(Integer initialPartitionId: idToRunLength.keySet()) {
int runLength = idToRunLength.get(initialPartitionId);
if(runLength < hotContiguityCutoff)
continue;
int hotPartitionId = (initialPartitionId + runLength)
% cluster.getNumberOfPartitions();
Node hotNode = cluster.getNodeForPartitionId(hotPartitionId);
sb.append("\tNode " + hotNode.getId() + " (" + hotNode.getHost()
+ ") has hot primary partition " + hotPartitionId
+ " that follows contiguous run of length " + runLength + Utils.NEWLINE);
}
}
return sb.toString();
}
/**
* Prints the details of cluster xml in various formats. Some information is
* repeated in different forms. This is intentional so that it is easy to
* find the specific view of the cluster xml that you want.
*
* @param cluster
* @return pretty string of cluster balance
*/
public static String verboseClusterDump(final Cluster cluster) {
StringBuilder builder = new StringBuilder();
builder.append("CLUSTER XML SUMMARY\n");
Map<Integer, Integer> zoneIdToPartitionCount = Maps.newHashMap();
Map<Integer, Integer> zoneIdToNodeCount = Maps.newHashMap();
for(Zone zone: cluster.getZones()) {
zoneIdToPartitionCount.put(zone.getId(), 0);
zoneIdToNodeCount.put(zone.getId(), 0);
}
for(Node node: cluster.getNodes()) {
zoneIdToPartitionCount.put(node.getZoneId(),
zoneIdToPartitionCount.get(node.getZoneId())
+ node.getNumberOfPartitions());
zoneIdToNodeCount.put(node.getZoneId(), zoneIdToNodeCount.get(node.getZoneId()) + 1);
}
builder.append("\n");
builder.append("Number of partitions per zone:\n");
for(Zone zone: cluster.getZones()) {
builder.append("\tZone: " + zone.getId() + " - "
+ zoneIdToPartitionCount.get(zone.getId()) + "\n");
}
builder.append("\n");
builder.append("Number of nodes per zone:\n");
for(Zone zone: cluster.getZones()) {
builder.append("\tZone: " + zone.getId() + " - " + zoneIdToNodeCount.get(zone.getId())
+ "\n");
}
builder.append("\n");
builder.append("Nodes in each zone:\n");
for(Zone zone: cluster.getZones()) {
builder.append("\tZone: " + zone.getId() + " - "
+ cluster.getNodeIdsInZone(zone.getId()) + "\n");
}
builder.append("\n");
builder.append("Number of partitions per node:\n");
for(Node node: cluster.getNodes()) {
builder.append("\tNode ID: " + node.getId() + " - " + node.getNumberOfPartitions()
+ " (" + node.getHost() + ")\n");
}
builder.append("\n");
if(cluster.getZones().size() > 1) {
builder.append("ZONE-PARTITION SUMMARY:\n");
builder.append("\n");
builder.append("Partitions in each zone:\n");
for(Zone zone: cluster.getZones()) {
builder.append("\tZone: " + zone.getId() + " - "
+ compressedListOfPartitionsInZone(cluster, zone.getId()) + "\n");
}
builder.append("\n");
builder.append("Contiguous partition run lengths in each zone ('{run length : count}'):\n");
for(Zone zone: cluster.getZones()) {
builder.append("\tZone: " + zone.getId() + " - "
+ getPrettyMapOfContiguousPartitionRunLengths(cluster, zone.getId())
+ "\n");
}
builder.append("\n");
builder.append("The following nodes have hot partitions:\n");
builder.append(getHotPartitionsDueToContiguity(cluster, 5));
builder.append("\n");
}
return builder.toString();
}
// TODO: (refactor) separate analysis from pretty printing and add a unit
// test for the analysis sub-method.
/**
* Compares current cluster with final cluster. Uses pertinent store defs
* for each cluster to determine if a node that hosts a zone-primary in the
* current cluster will no longer host any zone-nary in the final cluster.
* This check is the precondition for a server returning an invalid metadata
* exception to a client on a normal-case put or get. Normal-case being that
* the zone-primary receives the pseudo-master put or the get operation.
*
* @param currentCluster
* @param currentStoreDefs
* @param finalCluster
* @param finalStoreDefs
* @return pretty-printed string documenting invalid metadata rates for each
* zone.
*/
public static String analyzeInvalidMetadataRate(final Cluster currentCluster,
List<StoreDefinition> currentStoreDefs,
final Cluster finalCluster,
List<StoreDefinition> finalStoreDefs) {
StringBuilder sb = new StringBuilder();
sb.append("Dump of invalid metadata rates per zone").append(Utils.NEWLINE);
HashMap<StoreDefinition, Integer> uniqueStores = StoreDefinitionUtils.getUniqueStoreDefinitionsWithCounts(currentStoreDefs);
for(StoreDefinition currentStoreDef: uniqueStores.keySet()) {
sb.append("Store exemplar: " + currentStoreDef.getName())
.append(Utils.NEWLINE)
.append("\tThere are " + uniqueStores.get(currentStoreDef) + " other similar stores.")
.append(Utils.NEWLINE);
StoreRoutingPlan currentSRP = new StoreRoutingPlan(currentCluster, currentStoreDef);
StoreDefinition finalStoreDef = StoreUtils.getStoreDef(finalStoreDefs,
currentStoreDef.getName());
StoreRoutingPlan finalSRP = new StoreRoutingPlan(finalCluster, finalStoreDef);
// Only care about existing zones
for(int zoneId: currentCluster.getZoneIds()) {
int zonePrimariesCount = 0;
int invalidMetadata = 0;
// Examine nodes in current cluster in existing zone.
for(int nodeId: currentCluster.getNodeIdsInZone(zoneId)) {
// For every zone-primary in current cluster
for(int zonePrimaryPartitionId: currentSRP.getZonePrimaryPartitionIds(nodeId)) {
zonePrimariesCount++;
// Determine if original zone-primary node is still some
// form of n-ary in final cluster. If not,
// InvalidMetadataException will fire.
if(!finalSRP.getZoneNAryPartitionIds(nodeId)
.contains(zonePrimaryPartitionId)) {
invalidMetadata++;
}
}
}
float rate = invalidMetadata / (float) zonePrimariesCount;
sb.append("\tZone " + zoneId)
.append(" : total zone primaries " + zonePrimariesCount)
.append(", # that trigger invalid metadata " + invalidMetadata)
.append(" => " + rate)
.append(Utils.NEWLINE);
}
}
return sb.toString();
}
}