blob: 7bbb773fa98a6cbdf8b2e304aaf7903caa6e5d22 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.zookeeper.server;
import java.io.File;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.zip.CheckedInputStream;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.jute.BinaryInputArchive;
import org.apache.jute.InputArchive;
import org.apache.zookeeper.server.persistence.FileSnap;
import org.apache.zookeeper.server.persistence.SnapStream;
import org.apache.zookeeper.util.ServiceUtils;
/**
* SnapshotComparer is a tool that loads and compares two snapshots with configurable threshold and various filters, and outputs information about the delta.
* The delta includes specific znode paths added, updated, deleted comparing one snapshot to another.
* It's useful in use cases that involve snapshot analysis, such as offline data consistency checking, and data trending analysis (e.g. what's growing under which zNode path during when).
* Only outputs information about permanent nodes, ignoring both sessions and ephemeral nodes.
*/
public class SnapshotComparer {
private final Options options;
private static final String leftOption = "left";
private static final String rightOption = "right";
private static final String byteThresholdOption = "bytes";
private static final String nodeThresholdOption = "nodes";
private static final String debugOption = "debug";
private static final String interactiveOption = "interactive";
@SuppressWarnings("static")
private SnapshotComparer() {
options = new Options();
options.addOption(
Option.builder("l")
.hasArg()
.required(true)
.longOpt(leftOption)
.desc("(Required) The left snapshot file.")
.argName("LEFT")
.type(File.class)
.build());
options.addOption(
Option.builder("r")
.hasArg()
.required(true)
.longOpt(rightOption)
.desc("(Required) The right snapshot file.")
.argName("RIGHT")
.type(File.class)
.build());
options.addOption(
Option.builder("b")
.hasArg()
.required(true)
.longOpt(byteThresholdOption)
.desc("(Required) The node data delta size threshold, in bytes, for printing the node.")
.argName("BYTETHRESHOLD")
.type(String.class)
.build());
options.addOption(
Option.builder("n")
.hasArg()
.required(true)
.longOpt(nodeThresholdOption)
.desc("(Required) The descendant node delta size threshold, in nodes, for printing the node.")
.argName("NODETHRESHOLD")
.type(String.class)
.build());
options.addOption("d", debugOption, false, "Use debug output.");
options.addOption("i", interactiveOption, false, "Enter interactive mode.");
}
private void usage() {
HelpFormatter help = new HelpFormatter();
help.printHelp(
120,
"java -cp <classPath> " + SnapshotComparer.class.getName(),
"",
options,
"");
}
public static void main(String[] args) throws Exception {
SnapshotComparer app = new SnapshotComparer();
app.compareSnapshots(args);
}
private void compareSnapshots(String[] args) throws Exception {
CommandLine parsedOptions;
try {
parsedOptions = new DefaultParser().parse(options, args);
} catch (ParseException e) {
System.err.println(e.getMessage());
usage();
ServiceUtils.requestSystemExit(ExitCode.INVALID_INVOCATION.getValue());
return;
}
File left = (File) parsedOptions.getParsedOptionValue(leftOption);
File right = (File) parsedOptions.getParsedOptionValue(rightOption);
int byteThreshold = Integer.parseInt((String) parsedOptions.getParsedOptionValue(byteThresholdOption));
int nodeThreshold = Integer.parseInt((String) parsedOptions.getParsedOptionValue(nodeThresholdOption));
boolean debug = parsedOptions.hasOption(debugOption);
boolean interactive = parsedOptions.hasOption(interactiveOption);
System.out.println("Successfully parsed options!");
TreeInfo leftTree = new TreeInfo(left);
TreeInfo rightTree = new TreeInfo(right);
System.out.println(leftTree.toString());
System.out.println(rightTree.toString());
compareTrees(leftTree, rightTree, byteThreshold, nodeThreshold, debug, interactive);
}
private static class TreeInfo {
public static class TreeNode {
final String label;
final long size;
final List<TreeNode> children;
long descendantSize;
long descendantCount;
public static class AlphabeticComparator implements Comparator<TreeNode>, Serializable {
private static final long serialVersionUID = 2601197766392565593L;
public int compare(TreeNode left, TreeNode right) {
if (left == right) {
return 0;
}
if (left == null) {
return -1;
}
if (right == null) {
return 1;
}
return left.label.compareTo(right.label);
}
}
public TreeNode(String label, long size) {
this.label = label;
this.size = size;
this.children = new ArrayList<>();
}
void populateChildren(String path, DataTree dataTree, TreeInfo treeInfo) throws Exception {
populateChildren(path, dataTree, treeInfo, 1);
}
void populateChildren(String path, DataTree dataTree, TreeInfo treeInfo, int currentDepth) throws Exception {
List<String> childLabels = null;
childLabels = dataTree.getChildren(path, null, null);
if (childLabels != null && !childLabels.isEmpty()) {
for (String childName : childLabels){
String childPath = path + "/" + childName;
DataNode childNode = dataTree.getNode(childPath);
long size;
synchronized (childNode) {
size = childNode.data == null ? 0 : childNode.data.length;
}
TreeNode childTreeNode = new TreeNode(childPath, size);
childTreeNode.populateChildren(childPath, dataTree, treeInfo, currentDepth + 1);
children.add(childTreeNode);
}
}
descendantSize = 0;
descendantCount = 0;
for (TreeNode child : children) {
descendantSize += child.descendantSize;
descendantCount += child.descendantCount;
}
descendantSize += this.size;
descendantCount += this.children.size();
treeInfo.registerNode(this, currentDepth);
}
}
final TreeNode root;
long count;
List<ArrayList<TreeNode>> nodesAtDepths = new ArrayList<>();
Map<String, TreeNode> nodesByName = new HashMap<>();
TreeInfo(File snapshot) throws Exception {
DataTree dataTree = getSnapshot(snapshot);
count = 0;
long beginning = System.nanoTime();
DataNode root = dataTree.getNode("");
long size = root.data == null ? 0 : root.data.length;
this.root = new TreeNode("", size);
// Construct TreeInfo tree from DataTree
this.root.populateChildren("", dataTree, this);
long end = System.nanoTime();
System.out.println(String.format("Processed data tree in %f seconds",
((((double) end - beginning) / 1000000)) / 1000));
}
void registerNode(TreeNode node, int depth) {
while (depth > nodesAtDepths.size()) {
nodesAtDepths.add(new ArrayList<>());
}
nodesAtDepths.get(depth - 1).add(node);
nodesByName.put(node.label, node);
this.count++;
}
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(String.format("Node count: %d%n", count));
builder.append(String.format("Total size: %d%n", root.descendantSize));
builder.append(String.format("Max depth: %d%n", nodesAtDepths.size()));
for (int i = 0; i < nodesAtDepths.size(); i++) {
builder.append(String.format("Count of nodes at depth %d: %d%n", i, nodesAtDepths.get(i).size()));
}
return builder.toString();
}
public static Comparator<TreeNode> MakeAlphabeticComparator() {
return new TreeNode.AlphabeticComparator();
}
}
/**
* Parse a Zookeeper snapshot file to DataTree
* @param file the snapshot file
* @throws Exception
*/
private static DataTree getSnapshot(File file) throws Exception {
DataTree dataTree = new DataTree();
Map<Long, Integer> sessions = new HashMap<>();
CheckedInputStream snapIS = SnapStream.getInputStream(file);
long beginning = System.nanoTime();
InputArchive ia = BinaryInputArchive.getArchive(snapIS);
FileSnap.deserialize(dataTree, sessions, ia);
long end = System.nanoTime();
System.out.println(String.format("Deserialized snapshot in %s in %f seconds", file.getName(),
(((double) (end - beginning) / 1000000)) / 1000));
return dataTree;
}
private static void printThresholdInfo(int byteThreshold, int nodeThreshold) {
System.out.println(String.format("Printing analysis for nodes difference larger than %d bytes or node count difference larger than %d.", byteThreshold, nodeThreshold));
}
private static void compareTrees(TreeInfo left, TreeInfo right, int byteThreshold, int nodeThreshold, boolean debug, boolean interactive) {
int maxDepth = Math.max(left.nodesAtDepths.size(), right.nodesAtDepths.size());
if (!interactive) {
printThresholdInfo(byteThreshold, nodeThreshold);
for (int i = 0; i < maxDepth; i++) {
System.out.println(String.format("Analysis for depth %d", i));
compareLine(left, right, i, byteThreshold, nodeThreshold, debug, interactive);
}
} else {
// interactive mode
Scanner scanner = new Scanner(System.in);
int currentDepth = 0;
while (currentDepth < maxDepth) {
System.out.println(String.format("Current depth is %d", currentDepth));
System.out.println("- Press enter to move to print current depth layer;\n- Type a number to jump to and print all nodes at a given depth;\n- Enter an ABSOLUTE path to print the immediate subtree of a node. Path must start with '/'.");
String input = scanner.nextLine();
printThresholdInfo(byteThreshold, nodeThreshold);
if (input.isEmpty()) {
// input is Enter
System.out.println(String.format("Analysis for depth %d", currentDepth));
compareLine(left, right, currentDepth, byteThreshold, nodeThreshold, debug, interactive);
currentDepth++;
} else {
// input is a path
if (input.startsWith("/")){
System.out.println(String.format("Analysis for node %s", input));
compareSubtree(left, right, input, byteThreshold, nodeThreshold, debug, interactive);
} else {
// input is a number
try {
int depth = Integer.parseInt(input);
if (depth < 0 || depth >= maxDepth) {
System.out.println(String.format("Depth must be in range [%d, %d]", 0, maxDepth - 1));
continue;
}
currentDepth = depth;
System.out.println(String.format("Analysis for depth %d", currentDepth));
compareLine(left, right, currentDepth, byteThreshold, nodeThreshold, debug, interactive);
} catch (NumberFormatException ex) {
// input is invalid
System.out.println(String.format("Input %s is not valid. Depth must be in range [%d, %d]. Path must be an absolute path which starts with '/'.", input, 0, maxDepth - 1));
}
}
}
System.out.println("");
}
}
System.out.println("All layers compared.");
}
private static void compareSubtree(TreeInfo left, TreeInfo right, String path, int byteThreshold, int nodeThreshold, boolean debug, boolean interactive) {
TreeInfo.TreeNode leftRoot = left.nodesByName.get(path);
TreeInfo.TreeNode rightRoot = right.nodesByName.get(path);
List<TreeInfo.TreeNode> leftList = leftRoot == null ? new ArrayList<TreeInfo.TreeNode>() : leftRoot.children;
List<TreeInfo.TreeNode> rightList = rightRoot == null ? new ArrayList<TreeInfo.TreeNode>() : rightRoot.children;
if (leftRoot == null && rightRoot == null) {
System.out.println(String.format("Path %s is neither found in left tree nor right tree.", path));
} else {
compareNodes(leftList, rightList, byteThreshold, nodeThreshold, debug, interactive);
}
}
/**
* Compare left tree and right tree at the same depth.
* @param left the left data tree
* @param right the right data tree
* @param depth the depth of the data tree to be compared at
* @param byteThreshold the node data delta size threshold, in bytes, for printing the node
* @param nodeThreshold the descendant node delta size threshold, in nodes, for printing the node
* @param debug If true, print more detailed debug information
* @param interactive If true, enter interactive mode
*/
private static void compareLine(TreeInfo left, TreeInfo right, int depth, int byteThreshold, int nodeThreshold, boolean debug, boolean interactive) {
List<TreeInfo.TreeNode> leftList = depth >= left.nodesAtDepths.size() ? new ArrayList<>() : left.nodesAtDepths.get(depth);
List<TreeInfo.TreeNode> rightList = depth >= right.nodesAtDepths.size() ? new ArrayList<>() : right.nodesAtDepths.get(depth);
compareNodes(leftList, rightList, byteThreshold, nodeThreshold, debug, interactive);
}
private static void compareNodes(List<TreeInfo.TreeNode> leftList, List<TreeInfo.TreeNode> rightList, int byteThreshold, int nodeThreshold, boolean debug, boolean interactive) {
Comparator<TreeInfo.TreeNode> alphabeticComparator = TreeInfo.MakeAlphabeticComparator();
Collections.sort(leftList, alphabeticComparator);
Collections.sort(rightList, alphabeticComparator);
int leftIndex = 0;
int rightIndex = 0;
boolean leftRemaining = leftList.size() > leftIndex;
boolean rightRemaining = rightList.size() > rightIndex;
while (leftRemaining || rightRemaining) {
TreeInfo.TreeNode leftNode = null;
if (leftRemaining) {
leftNode = leftList.get(leftIndex);
}
TreeInfo.TreeNode rightNode = null;
if (rightRemaining) {
rightNode = rightList.get(rightIndex);
}
if (leftNode != null && rightNode != null) {
if (debug) {
System.out.println(String.format("Comparing %s to %s", leftNode.label, rightNode.label));
}
int result = leftNode.label.compareTo(rightNode.label);
if (result < 0) {
if (debug) {
System.out.println("left is less");
}
printLeftOnly(leftNode, byteThreshold, nodeThreshold, debug, interactive);
leftIndex++;
} else if (result > 0) {
if (debug) {
System.out.println("right is less");
}
printRightOnly(rightNode, byteThreshold, nodeThreshold, debug, interactive);
rightIndex++;
} else {
if (debug) {
System.out.println("same");
}
printBoth(leftNode, rightNode, byteThreshold, nodeThreshold, debug, interactive);
leftIndex++;
rightIndex++;
}
} else if (leftNode != null) {
printLeftOnly(leftNode, byteThreshold, nodeThreshold, debug, interactive);
leftIndex++;
} else {
printRightOnly(rightNode, byteThreshold, nodeThreshold, debug, interactive);
rightIndex++;
}
leftRemaining = leftList.size() > leftIndex;
rightRemaining = rightList.size() > rightIndex;
}
}
static void printLeftOnly(TreeInfo.TreeNode node, int byteThreshold, int nodeThreshold, boolean debug, boolean interactive) {
if (node.descendantSize > byteThreshold || node.descendantCount > nodeThreshold) {
StringBuilder builder = new StringBuilder();
builder.append(String.format("Node %s found only in left tree. ", node.label));
printNode(node, builder);
System.out.println(builder.toString());
} else if (debug || interactive) {
System.out.println(String.format("Filtered left node %s of size %d", node.label, node.descendantSize));
}
}
static void printRightOnly(TreeInfo.TreeNode node, int byteThreshold, int nodeThreshold, boolean debug, boolean interactive) {
if (node.descendantSize > byteThreshold || node.descendantCount > nodeThreshold) {
StringBuilder builder = new StringBuilder();
builder.append(String.format("Node %s found only in right tree. ", node.label));
printNode(node, builder);
System.out.println(builder.toString());
} else if (debug || interactive) {
System.out.println(String.format("Filtered right node %s of size %d", node.label, node.descendantSize));
}
}
static void printBoth(TreeInfo.TreeNode leftNode, TreeInfo.TreeNode rightNode, int byteThreshold, int nodeThreshold, boolean debug, boolean interactive) {
if (Math.abs(rightNode.descendantSize - leftNode.descendantSize) > byteThreshold
|| Math.abs(rightNode.descendantCount - leftNode.descendantCount) > nodeThreshold) {
System.out.println(String.format(
"Node %s found in both trees. Delta: %d bytes, %d descendants",
leftNode.label,
rightNode.descendantSize - leftNode.descendantSize,
rightNode.descendantCount - leftNode.descendantCount));
} else if (debug || interactive) {
System.out.println(String.format("Filtered node %s of left size %d, right size %d", leftNode.label, leftNode.descendantSize, rightNode.descendantSize));
}
}
static void printNode(TreeInfo.TreeNode node, StringBuilder builder) {
builder.append(String.format("Descendant size: %d. Descendant count: %d", node.descendantSize, node.descendantCount));
}
}