Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide pluggable hashing and lookup strategy for NodeLocator #154

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions evcache-core/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
plugins {
id "me.champeau.jmh" version "0.6.6"
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'me.champeau.jmh'

sourceSets.main.java.srcDir 'src/main/java'
sourceSets.main.resources.srcDir 'src/main/resources'
Expand Down Expand Up @@ -39,6 +44,7 @@ dependencies {
compile group:"joda-time", name:"joda-time", version:"latest.release"
compile group:"javax.annotation", name:"javax.annotation-api", version:"latest.release"
compile group:"com.github.fzakaria", name:"ascii85", version:"latest.release"
compile group:"net.openhft", name:"zero-allocation-hashing", version:"latest.release"

testCompile group:"org.testng", name:"testng", version:"7.5"
testCompile group:"com.beust", name:"jcommander", version:"1.72"
Expand All @@ -49,3 +55,12 @@ dependencies {
javadoc {
failOnError = false
}

jmh {
jmhVersion = '1.35'
warmupIterations = 2
iterations = 2
fork = 1
zip64 = true
profilers = ['gc', 'stack']
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.netflix.evcache.pool;

import net.spy.memcached.DefaultHashAlgorithm;
import net.spy.memcached.MemcachedNode;
import net.spy.memcached.NodeLocator;
import net.spy.memcached.util.DefaultKetamaNodeLocatorConfiguration;
import net.spy.memcached.util.KetamaNodeLocatorConfiguration;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.infra.Blackhole;

import com.netflix.evcache.pool.NodeLocatorLookup.ArrayNodeLocatorLookup;
import com.netflix.evcache.pool.NodeLocatorLookup.EytzingerNodeLocatorLookup;
import com.netflix.evcache.pool.NodeLocatorLookup.TreeMapNodeLocatorLookup;
import com.netflix.evcache.pool.NodeLocatorLookup.DirectApproximateNodeLocatorLookup;

import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeMap;
import java.util.function.Function;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@State(Scope.Benchmark)
public class NodeLocatorBenchmark {
@Param({"10", "120"})
int nodeCount;

@Param({"2000"})
int keyCount;

@Param({"0", "50"})
int keyTailLength;

@Param({"legacy", "array", "eytzinger", "direct"})
String locator;

@Param({"ketama-md5", "simple-fnv1a", "ketama-murmur3"})
String hashRing;

NodeLocator impl;

List<MemcachedNode> nodes;

String[] keys;

static Function<TreeMap<Long, MemcachedNode>, NodeLocatorLookup<MemcachedNode>> findLookupFactory(String locator) {
if (locator.equals("legacy")) {
return TreeMapNodeLocatorLookup::new;
} else if (locator.equals("array")) {
return ArrayNodeLocatorLookup::new;
} else if (locator.equals("eytzinger")) {
return EytzingerNodeLocatorLookup::new;
} else if (locator.equals("direct")) {
return DirectApproximateNodeLocatorLookup::new;
} else {
throw new RuntimeException("Unknown locator: " + locator);
}
}

static HashRingAlgorithm findHashRingAlgorithm(String hashRing) {
if (hashRing.equals("ketama-md5")) {
return new HashRingAlgorithm.KetamaMd5HashRingAlgorithm();
} else if (hashRing.equals("simple-fnv1a")) {
return new HashRingAlgorithm.SimpleHashRingAlgorithm(DefaultHashAlgorithm.FNV1A_64_HASH);
} else if (hashRing.equals("ketama-murmur3")) {
return new HashRingAlgorithm.KetamaMurmur3HashRingAlgorithm();
} else {
throw new RuntimeException("Unknown hash ring: " + hashRing);
}
}

@Setup
public void setup() {
nodes = new ArrayList<>();
for (int i = 1; i <= nodeCount; i++) {
MemcachedNode node = mock(MemcachedNode.class);
when(node.getSocketAddress()).thenReturn(new InetSocketAddress("100.94.221." + i, 11211));
nodes.add(node);
}

EVCacheClient client = mock(EVCacheClient.class);
when(client.getAppName()).thenReturn("mockApp");
when(client.getServerGroupName()).thenReturn("mockAppServerGroup");
KetamaNodeLocatorConfiguration conf = new DefaultKetamaNodeLocatorConfiguration();

HashRingAlgorithm hashRingAlgorithm = findHashRingAlgorithm(hashRing);
Function<TreeMap<Long, MemcachedNode>, NodeLocatorLookup<MemcachedNode>> lookupFactory = findLookupFactory(locator);

impl = new EVCacheNodeLocator(client, nodes, hashRingAlgorithm, conf, lookupFactory);

keys = new String[keyCount];
for (int i = 0; i < keyCount; i++) {
keys[i] = "key_" + i;
for (int j = 0; j < keyTailLength; j++) {
keys[i] += (char)('a' + (i % 26));
}
}
}

@Benchmark
public void testGetPrimary(Blackhole bh) {
for (int i = 0; i < keyCount; i++) {
bh.consume(impl.getPrimary(keys[i]));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Function;

import com.netflix.archaius.api.Property;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.netflix.evcache.pool.HashRingAlgorithm.SimpleHashRingAlgorithm;
import com.netflix.evcache.pool.HashRingAlgorithm.KetamaMd5HashRingAlgorithm;
import com.netflix.evcache.pool.NodeLocatorLookup.TreeMapNodeLocatorLookup;
import com.netflix.evcache.util.EVCacheConfig;

import net.spy.memcached.DefaultHashAlgorithm;
Expand All @@ -24,15 +28,17 @@
public class EVCacheNodeLocator implements NodeLocator {

private static final Logger log = LoggerFactory.getLogger(EVCacheNodeLocator.class);
private TreeMap<Long, MemcachedNode> ketamaNodes;
private TreeMap<Long, MemcachedNode> ketamaNodesTreeMap;
private NodeLocatorLookup<MemcachedNode> ketamaNodes;
protected final EVCacheClient client;
private final Function<TreeMap<Long, MemcachedNode>, NodeLocatorLookup<MemcachedNode>> lookupFactory;

private final Property<Boolean> partialStringHash;
private final Property<String> hashDelimiter;

private final Collection<MemcachedNode> allNodes;

private final HashAlgorithm hashingAlgorithm;
private final HashRingAlgorithm hashRingAlgorithm;
private final KetamaNodeLocatorConfiguration config;

/**
Expand All @@ -47,12 +53,13 @@ public class EVCacheNodeLocator implements NodeLocator {
* consistent hash continuum
* @param conf
*/
public EVCacheNodeLocator(EVCacheClient client, List<MemcachedNode> nodes, HashAlgorithm alg, KetamaNodeLocatorConfiguration conf) {
public EVCacheNodeLocator(EVCacheClient client, List<MemcachedNode> nodes, HashRingAlgorithm hashRingAlgorithm, KetamaNodeLocatorConfiguration conf, Function<TreeMap<Long, MemcachedNode>, NodeLocatorLookup<MemcachedNode>> lookupFactory) {
super();
this.allNodes = nodes;
this.hashingAlgorithm = alg;
this.hashRingAlgorithm = hashRingAlgorithm;
this.config = conf;
this.client = client;
this.lookupFactory = lookupFactory;

this.partialStringHash = EVCacheConfig.getInstance().getPropertyRepository().get(client.getAppName() + "." + client.getServerGroupName() + ".hash.on.partial.key", Boolean.class)
.orElseGet(client.getAppName()+ ".hash.on.partial.key").orElse(false);
Expand All @@ -63,11 +70,22 @@ public EVCacheNodeLocator(EVCacheClient client, List<MemcachedNode> nodes, HashA
setKetamaNodes(nodes);
}

private EVCacheNodeLocator(EVCacheClient client, TreeMap<Long, MemcachedNode> smn, Collection<MemcachedNode> an, HashAlgorithm alg, KetamaNodeLocatorConfiguration conf) {
public EVCacheNodeLocator(EVCacheClient client, List<MemcachedNode> nodes, HashAlgorithm alg, KetamaNodeLocatorConfiguration conf) {
this(client,
nodes,
alg == DefaultHashAlgorithm.KETAMA_HASH ? new KetamaMd5HashRingAlgorithm()
: new SimpleHashRingAlgorithm(alg),
conf,
TreeMapNodeLocatorLookup::new);
}

private EVCacheNodeLocator(EVCacheClient client, TreeMap<Long, MemcachedNode> smn, Collection<MemcachedNode> an, HashRingAlgorithm hashRingAlgorithm, KetamaNodeLocatorConfiguration conf, Function<TreeMap<Long, MemcachedNode>, NodeLocatorLookup<MemcachedNode>> lookupFactory) {
super();
this.ketamaNodes = smn;
this.ketamaNodes = lookupFactory.apply(smn);
this.lookupFactory = lookupFactory;
this.ketamaNodesTreeMap = smn;
this.allNodes = an;
this.hashingAlgorithm = alg;
this.hashRingAlgorithm = hashRingAlgorithm;
this.config = conf;
this.client = client;

Expand All @@ -88,20 +106,17 @@ public Collection<MemcachedNode> getAll() {
* @see net.spy.memcached.NodeLocator#getPrimary
*/
public MemcachedNode getPrimary(String k) {
CharSequence key = k;
if (partialStringHash.get()) {
final int index = k.indexOf(hashDelimiter.get());
if (index > 0) {
k = k.substring(0, index);
key = k.subSequence(0, index);
}
}

final long hash = hashingAlgorithm.hash(k);
final long hash = hashRingAlgorithm.hash(key);

Map.Entry<Long, MemcachedNode> entry = ketamaNodes.ceilingEntry(hash);
if (entry == null) {
entry = ketamaNodes.firstEntry();
}
return entry.getValue();
return ketamaNodes.wrappingCeilingValue(hash);
}

/*
Expand All @@ -114,12 +129,7 @@ public long getMaxKey() {
public MemcachedNode getNodeForKey(long _hash) {
long start = (log.isDebugEnabled()) ? System.nanoTime() : 0;
try {
Long hash = Long.valueOf(_hash);
hash = ketamaNodes.ceilingKey(hash);
if (hash == null) {
hash = ketamaNodes.firstKey();
}
return ketamaNodes.get(hash);
return ketamaNodes.wrappingCeilingValue(_hash);
} finally {
if (log.isDebugEnabled()) {
final long end = System.nanoTime();
Expand Down Expand Up @@ -147,22 +157,22 @@ public NodeLocator getReadonlyCopy() {
aNodes.add(new EVCacheMemcachedNodeROImpl(n));
}

return new EVCacheNodeLocator(client, ketamaNaodes, aNodes, hashingAlgorithm, config);
return new EVCacheNodeLocator(client, ketamaNaodes, aNodes, hashRingAlgorithm, config, lookupFactory);
}

/**
* @return the ketamaNodes
*/
protected TreeMap<Long, MemcachedNode> getKetamaNodes() {
return ketamaNodes;
return ketamaNodesTreeMap;
}

/**
* @return the readonly view of ketamaNodes. This is mailnly for admin
* purposes
*/
public Map<Long, MemcachedNode> getKetamaNodeMap() {
return Collections.<Long, MemcachedNode> unmodifiableMap(ketamaNodes);
return Collections.<Long, MemcachedNode> unmodifiableMap(ketamaNodesTreeMap);
}

/**
Expand All @@ -175,26 +185,13 @@ public Map<Long, MemcachedNode> getKetamaNodeMap() {
protected final void setKetamaNodes(List<MemcachedNode> nodes) {
TreeMap<Long, MemcachedNode> newNodeMap = new TreeMap<Long, MemcachedNode>();
final int numReps = config.getNodeRepetitions();
long[] parts = new long[hashRingAlgorithm.getCountHashParts()];
for (MemcachedNode node : nodes) {
// Ketama does some special work with md5 where it reuses chunks.
if (hashingAlgorithm == DefaultHashAlgorithm.KETAMA_HASH) {
for (int i = 0; i < numReps / 4; i++) {
final String hashString = config.getKeyForNode(node, i);
byte[] digest = DefaultHashAlgorithm.computeMd5(hashString);
if (log.isDebugEnabled()) log.debug("digest : " + digest);
for (int h = 0; h < 4; h++) {
long k = ((long) (digest[3 + h * 4] & 0xFF) << 24)
| ((long) (digest[2 + h * 4] & 0xFF) << 16)
| ((long) (digest[1 + h * 4] & 0xFF) << 8)
| (digest[h * 4] & 0xFF);
newNodeMap.put(Long.valueOf(k), node);
if (log.isDebugEnabled()) log.debug("Key : " + hashString + " ; hash : " + k + "; node " + node );
}
}
} else {
for (int i = 0; i < numReps; i++) {
final Long hashL = Long.valueOf(hashingAlgorithm.hash(config.getKeyForNode(node, i)));
newNodeMap.put(hashL, node);
for (int i = 0; i < numReps / 4; i++) {
final String hashString = config.getKeyForNode(node, i);
hashRingAlgorithm.getHashPartsInto(hashString, parts);
for (int h = 0; h < parts.length; h++) {
newNodeMap.put(Long.valueOf(parts[h]), node);
}
}
}
Expand All @@ -204,7 +201,8 @@ protected final void setKetamaNodes(List<MemcachedNode> nodes) {
log.trace("Hash : " + key + "; Node : " + newNodeMap.get(key));
}
}
ketamaNodes = newNodeMap;
ketamaNodes = lookupFactory.apply(newNodeMap);
ketamaNodesTreeMap = newNodeMap;
}

@Override
Expand All @@ -215,7 +213,7 @@ public void updateLocator(List<MemcachedNode> nodes) {
@Override
public String toString() {
return "EVCacheNodeLocator [ketamaNodes=" + ketamaNodes + ", EVCacheClient=" + client + ", partialStringHash=" + partialStringHash
+ ", hashDelimiter=" + hashDelimiter + ", allNodes=" + allNodes + ", hashingAlgorithm=" + hashingAlgorithm + ", config=" + config + "]";
+ ", hashDelimiter=" + hashDelimiter + ", allNodes=" + allNodes + ", hashRingAlgorithm=" + hashRingAlgorithm + ", config=" + config + "]";
}

}
Loading
Loading