org.apache.distributedlog.client.routing.ConsistentHashRoutingService Maven / Gradle / Ivy
The newest version!
/**
* 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.distributedlog.client.routing;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.twitter.common.zookeeper.ServerSet;
import org.apache.distributedlog.service.DLSocketAddress;
import com.twitter.finagle.ChannelException;
import com.twitter.finagle.NoBrokersAvailableException;
import com.twitter.finagle.stats.Counter;
import com.twitter.finagle.stats.Gauge;
import com.twitter.finagle.stats.NullStatsReceiver;
import com.twitter.finagle.stats.StatsReceiver;
import com.twitter.util.Function0;
import java.net.SocketAddress;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.lang3.tuple.Pair;
import org.jboss.netty.util.HashedWheelTimer;
import org.jboss.netty.util.Timeout;
import org.jboss.netty.util.TimerTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import scala.collection.Seq;
/**
* Consistent Hashing Based {@link RoutingService}.
*/
public class ConsistentHashRoutingService extends ServerSetRoutingService {
private static final Logger logger = LoggerFactory.getLogger(ConsistentHashRoutingService.class);
@Deprecated
public static ConsistentHashRoutingService of(ServerSetWatcher serverSetWatcher, int numReplicas) {
return new ConsistentHashRoutingService(serverSetWatcher, numReplicas, 300, NullStatsReceiver.get());
}
/**
* Builder helper class to build a consistent hash bashed {@link RoutingService}.
*
* @return builder to build a consistent hash based {@link RoutingService}.
*/
public static Builder newBuilder() {
return new Builder();
}
/**
* Builder for building consistent hash based routing service.
*/
public static class Builder implements RoutingService.Builder {
private ServerSet serverSet;
private boolean resolveFromName = false;
private int numReplicas;
private int blackoutSeconds = 300;
private StatsReceiver statsReceiver = NullStatsReceiver.get();
private Builder() {}
public Builder serverSet(ServerSet serverSet) {
this.serverSet = serverSet;
return this;
}
public Builder resolveFromName(boolean enabled) {
this.resolveFromName = enabled;
return this;
}
public Builder numReplicas(int numReplicas) {
this.numReplicas = numReplicas;
return this;
}
public Builder blackoutSeconds(int seconds) {
this.blackoutSeconds = seconds;
return this;
}
public Builder statsReceiver(StatsReceiver statsReceiver) {
this.statsReceiver = statsReceiver;
return this;
}
@Override
public RoutingService build() {
checkNotNull(serverSet, "No serverset provided.");
checkNotNull(statsReceiver, "No stats receiver provided.");
checkArgument(numReplicas > 0, "Invalid number of replicas : " + numReplicas);
return new ConsistentHashRoutingService(new TwitterServerSetWatcher(serverSet, resolveFromName),
numReplicas, blackoutSeconds, statsReceiver);
}
}
static class ConsistentHash {
private final HashFunction hashFunction;
private final int numOfReplicas;
private final SortedMap circle;
// Stats
protected final Counter hostAddedCounter;
protected final Counter hostRemovedCounter;
ConsistentHash(HashFunction hashFunction,
int numOfReplicas,
StatsReceiver statsReceiver) {
this.hashFunction = hashFunction;
this.numOfReplicas = numOfReplicas;
this.circle = new TreeMap();
this.hostAddedCounter = statsReceiver.counter0("adds");
this.hostRemovedCounter = statsReceiver.counter0("removes");
}
private String replicaName(int shardId, int replica, String address) {
if (shardId < 0) {
shardId = UNKNOWN_SHARD_ID;
}
StringBuilder sb = new StringBuilder(100);
sb.append("shard-");
sb.append(shardId);
sb.append('-');
sb.append(replica);
sb.append('-');
sb.append(address);
return sb.toString();
}
private Long replicaHash(int shardId, int replica, String address) {
return hashFunction.hashUnencodedChars(replicaName(shardId, replica, address)).asLong();
}
private Long replicaHash(int shardId, int replica, SocketAddress address) {
return replicaHash(shardId, replica, address.toString());
}
public synchronized void add(int shardId, SocketAddress address) {
String addressStr = address.toString();
for (int i = 0; i < numOfReplicas; i++) {
Long hash = replicaHash(shardId, i, addressStr);
circle.put(hash, address);
}
hostAddedCounter.incr();
}
public synchronized void remove(int shardId, SocketAddress address) {
for (int i = 0; i < numOfReplicas; i++) {
long hash = replicaHash(shardId, i, address);
SocketAddress oldAddress = circle.get(hash);
if (null != oldAddress && oldAddress.equals(address)) {
circle.remove(hash);
}
}
hostRemovedCounter.incr();
}
public SocketAddress get(String key, RoutingContext rContext) {
long hash = hashFunction.hashUnencodedChars(key).asLong();
return find(hash, rContext);
}
private synchronized SocketAddress find(long hash, RoutingContext rContext) {
if (circle.isEmpty()) {
return null;
}
Iterator> iterator =
circle.tailMap(hash).entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry entry = iterator.next();
if (!rContext.isTriedHost(entry.getValue())) {
return entry.getValue();
}
}
// the tail map has been checked
iterator = circle.headMap(hash).entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry entry = iterator.next();
if (!rContext.isTriedHost(entry.getValue())) {
return entry.getValue();
}
}
return null;
}
private synchronized Pair get(long hash) {
if (circle.isEmpty()) {
return null;
}
if (!circle.containsKey(hash)) {
SortedMap tailMap = circle.tailMap(hash);
hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
}
return Pair.of(hash, circle.get(hash));
}
synchronized void dumpHashRing() {
for (Map.Entry entry : circle.entrySet()) {
logger.info(entry.getKey() + " : " + entry.getValue());
}
}
}
class BlackoutHost implements TimerTask {
final int shardId;
final SocketAddress address;
BlackoutHost(int shardId, SocketAddress address) {
this.shardId = shardId;
this.address = address;
numBlackoutHosts.incrementAndGet();
}
@Override
public void run(Timeout timeout) throws Exception {
numBlackoutHosts.decrementAndGet();
if (!timeout.isExpired()) {
return;
}
Set removedList = new HashSet();
boolean joined;
// add the shard back
synchronized (shardId2Address) {
SocketAddress curHost = shardId2Address.get(shardId);
if (null != curHost) {
// there is already new shard joint, so drop the host.
logger.info("Blackout Shard {} ({}) was already replaced by {} permanently.",
new Object[] { shardId, address, curHost });
joined = false;
} else {
join(shardId, address, removedList);
joined = true;
}
}
if (joined) {
for (RoutingListener listener : listeners) {
listener.onServerJoin(address);
}
} else {
for (RoutingListener listener : listeners) {
listener.onServerLeft(address);
}
}
}
}
protected final HashedWheelTimer hashedWheelTimer;
protected final HashFunction hashFunction = Hashing.md5();
protected final ConsistentHash circle;
protected final Map shardId2Address =
new HashMap();
protected final Map address2ShardId =
new HashMap();
// blackout period
protected final int blackoutSeconds;
// stats
protected final StatsReceiver statsReceiver;
protected final AtomicInteger numBlackoutHosts;
protected final Gauge numBlackoutHostsGauge;
protected final Gauge numHostsGauge;
private static final int UNKNOWN_SHARD_ID = -1;
ConsistentHashRoutingService(ServerSetWatcher serverSetWatcher,
int numReplicas,
int blackoutSeconds,
StatsReceiver statsReceiver) {
super(serverSetWatcher);
this.circle = new ConsistentHash(hashFunction, numReplicas, statsReceiver.scope("ring"));
this.hashedWheelTimer = new HashedWheelTimer(new ThreadFactoryBuilder()
.setNameFormat("ConsistentHashRoutingService-Timer-%d").build());
this.blackoutSeconds = blackoutSeconds;
// stats
this.statsReceiver = statsReceiver;
this.numBlackoutHosts = new AtomicInteger(0);
this.numBlackoutHostsGauge = this.statsReceiver.addGauge(gaugeName("num_blackout_hosts"),
new Function0
© 2015 - 2025 Weber Informatics LLC | Privacy Policy