This is an automated email from the ASF dual-hosted git repository.

brandonwilliams pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra.git


The following commit(s) were added to refs/heads/trunk by this push:
     new c9c022e  Improve the algorithmic token allocation in case racks = RF
c9c022e is described below

commit c9c022e07fd4fc6afc59cd1149fd18bc84d3fb39
Author: Ekaterina Dimitrova <[email protected]>
AuthorDate: Thu Feb 27 18:47:33 2020 -0500

    Improve the algorithmic token allocation in case racks = RF
    
    Patch by Ekaterina Dimitrova, reviewed by brandonwilliams for 
CASSANDRA-15600
---
 CHANGES.txt                                        |  1 +
 .../NoReplicationTokenAllocator.java               | 44 ++++++++--------
 .../ReplicationAwareTokenAllocator.java            | 12 ++++-
 .../dht/tokenallocator/TokenAllocation.java        | 28 +++++++++-
 .../dht/tokenallocator/TokenAllocatorBase.java     | 60 ++++++++++++++++++++++
 .../org/apache/cassandra/dht/BootStrapperTest.java | 57 +++++++++++++++++++-
 6 files changed, 176 insertions(+), 26 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 9cd6040..5858c19 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 4.0-alpha4
+ * Improve the algorithmic token allocation in case racks = RF 
(CASSANDRA-15600)
  * Include finalized pending sstables in preview repair (CASSANDRA-15553)
  * Reverted to the original behavior of CLUSTERING ORDER on CREATE TABLE 
(CASSANDRA-15271)
  * Correct inaccurate logging message (CASSANDRA-15549)
diff --git 
a/src/java/org/apache/cassandra/dht/tokenallocator/NoReplicationTokenAllocator.java
 
b/src/java/org/apache/cassandra/dht/tokenallocator/NoReplicationTokenAllocator.java
index f6a1592..0ac8951 100644
--- 
a/src/java/org/apache/cassandra/dht/tokenallocator/NoReplicationTokenAllocator.java
+++ 
b/src/java/org/apache/cassandra/dht/tokenallocator/NoReplicationTokenAllocator.java
@@ -114,24 +114,6 @@ public class NoReplicationTokenAllocator<Unit> extends 
TokenAllocatorBase<Unit>
         unitTokens.add(new Weighted<TokenInfo>(token.replicatedOwnership, 
token));
     }
 
-    private Collection<Token> generateRandomTokens(UnitInfo<Unit> newUnit, int 
numTokens, Map<Unit, UnitInfo<Unit>> unitInfos)
-    {
-        Set<Token> tokens = new HashSet<>(numTokens);
-        while (tokens.size() < numTokens)
-        {
-            Token token = partitioner.getRandomToken();
-            if (!sortedTokens.containsKey(token))
-            {
-                tokens.add(token);
-                sortedTokens.put(token, newUnit.unit);
-            }
-        }
-        unitInfos.put(newUnit.unit, newUnit);
-        createTokenInfos(unitInfos);
-        TokenAllocatorDiagnostics.randomTokensGenerated(this, numTokens, 
sortedUnits, sortedTokens, newUnit.unit, tokens);
-        return tokens;
-    }
-
     public Collection<Token> addUnit(Unit newUnit, int numTokens)
     {
         assert !tokensInUnits.containsKey(newUnit);
@@ -141,10 +123,10 @@ public class NoReplicationTokenAllocator<Unit> extends 
TokenAllocatorBase<Unit>
         Map<Unit, UnitInfo<Unit>> unitInfos = createUnitInfos(groups);
 
         if (unitInfos.isEmpty())
-            return generateRandomTokens(newUnitInfo, numTokens, unitInfos);
+            return generateSplits(newUnit, numTokens);
 
         if (numTokens > sortedTokens.size())
-            return generateRandomTokens(newUnitInfo, numTokens, unitInfos);
+            return generateSplits(newUnit, numTokens);
 
         TokenInfo<Unit> head = createTokenInfos(unitInfos);
 
@@ -172,7 +154,19 @@ public class NoReplicationTokenAllocator<Unit> extends 
TokenAllocatorBase<Unit>
         }
 
         List<Token> newTokens = Lists.newArrayListWithCapacity(numTokens);
-
+        // Generate different size nodes, at most at 2/(numTokens*2+1) 
difference,
+        // but tighten the spread as the number of nodes grows (since it 
increases the time until we need to use nodes
+        // we have just split).
+        double sizeCorrection = Math.min(1.0, (numTokens + 1.0) / 
(unitInfos.size() + 1.0));
+        double spread = targetAverage * sizeCorrection * 2.0 / (2 * numTokens 
+ 1);
+
+        // The biggest target is assigned to the biggest existing node. This 
should result in better balance in
+        // the amount of data that needs to be streamed from the different 
sources to the new node.
+        double target = targetAverage + spread / 2;
+
+        // This step intentionally divides by the count (rather than count - 
1) because we also need to count the new
+        // node. This leaves the last position in the spread (i.e. the 
smallest size, least data to stream) for it.
+        double step = spread / unitsToChange.size();
         int nr = 0;
         // calculate the tokens
         for (Weighted<UnitInfo> unit : unitsToChange)
@@ -193,7 +187,7 @@ public class NoReplicationTokenAllocator<Unit> extends 
TokenAllocatorBase<Unit>
                 unit.value.ownership -= wt.weight;
             }
 
-            double toTakeOver = unit.weight - targetAverage;
+            double toTakeOver = unit.weight - target;
             // Split toTakeOver proportionally between the vnodes.
             for (Weighted<TokenInfo> wt : tokens)
             {
@@ -230,6 +224,7 @@ public class NoReplicationTokenAllocator<Unit> extends 
TokenAllocatorBase<Unit>
 
             // adjust the weight for current unit
             sortedUnits.add(new Weighted<>(unit.value.ownership, unit.value));
+            target -= step;
             ++nr;
         }
         sortedUnits.add(new Weighted<>(newUnitInfo.ownership, newUnitInfo));
@@ -267,4 +262,9 @@ public class NoReplicationTokenAllocator<Unit> extends 
TokenAllocatorBase<Unit>
     {
         return 1;
     }
+
+    public String toString()
+    {
+        return getClass().getSimpleName();
+    }
 }
diff --git 
a/src/java/org/apache/cassandra/dht/tokenallocator/ReplicationAwareTokenAllocator.java
 
b/src/java/org/apache/cassandra/dht/tokenallocator/ReplicationAwareTokenAllocator.java
index 36fc8c2..eb498ce 100644
--- 
a/src/java/org/apache/cassandra/dht/tokenallocator/ReplicationAwareTokenAllocator.java
+++ 
b/src/java/org/apache/cassandra/dht/tokenallocator/ReplicationAwareTokenAllocator.java
@@ -61,9 +61,12 @@ class ReplicationAwareTokenAllocator<Unit> extends 
TokenAllocatorBase<Unit>
 
         if (unitCount() < replicas)
             // Allocation does not matter; everything replicates everywhere.
+            //However, at this point it is
+            // important to start the cluster/datacenter with suitably varied 
token range sizes so that the algorithm
+            // can maintain good balance for any number of nodes.
             return generateRandomTokens(newUnit, numTokens);
         if (numTokens > sortedTokens.size())
-            // Some of the heuristics below can't deal with this case. Use 
random for now, later allocations can fix any problems this may cause.
+            // Some of the heuristics below can't deal with this very unlikely 
case. Use splits for now, later allocations can fix any problems this may cause.
             return generateRandomTokens(newUnit, numTokens);
 
         // ============= construct our initial token ring state =============
@@ -154,6 +157,13 @@ class ReplicationAwareTokenAllocator<Unit> extends 
TokenAllocatorBase<Unit>
         return tokens;
     }
 
+    Collection<Token> generateSplits(Unit newUnit, int numTokens)
+    {
+        Collection<Token> tokens = super.generateSplits(newUnit, numTokens);
+        unitToTokens.putAll(newUnit, tokens);
+        return tokens;
+    }
+
     /**
      * Construct the token ring as a CircularList of TokenInfo,
      * and populate the ownership of the UnitInfo's provided
diff --git 
a/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocation.java 
b/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocation.java
index bfa281e..bd6d980 100644
--- a/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocation.java
+++ b/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocation.java
@@ -255,7 +255,7 @@ public class TokenAllocation
                 ? topology.getDatacenterRacks().get(dc).asMap().size()
                 : 1;
 
-        if (racks >= replicas)
+        if (racks > replicas)
         {
             return new StrategyAdapter()
             {
@@ -278,6 +278,32 @@ public class TokenAllocation
                 }
             };
         }
+        else if (racks == replicas)
+        {
+            // When the number of racks is the same as the replication factor, 
everything must replicate exactly once
+            // in each rack. This is the same as having independent rings from 
each rack.
+            final String rack = snitch.getRack(endpoint);
+            return new StrategyAdapter()
+            {
+                @Override
+                public int replicas()
+                {
+                    return 1;
+                }
+
+                @Override
+                public Object getGroup(InetAddressAndPort unit)
+                {
+                    return unit;
+                }
+
+                @Override
+                public boolean inAllocationRing(InetAddressAndPort other)
+                {
+                    return dc.equals(snitch.getDatacenter(other)) && 
rack.equals(snitch.getRack(other));
+                }
+            };
+        }
         else if (racks == 1)
         {
             // One rack, each node treated as separate.
diff --git 
a/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocatorBase.java 
b/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocatorBase.java
index f59bfd4..3d7e6b9 100644
--- a/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocatorBase.java
+++ b/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocatorBase.java
@@ -18,9 +18,13 @@
 
 package org.apache.cassandra.dht.tokenallocator;
 
+import java.util.Collection;
+import java.util.List;
 import java.util.Map;
 import java.util.NavigableMap;
+import java.util.Random;
 
+import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 
 import org.apache.cassandra.dht.IPartitioner;
@@ -28,6 +32,9 @@ import org.apache.cassandra.dht.Token;
 
 public abstract class TokenAllocatorBase<Unit> implements TokenAllocator<Unit>
 {
+    static final double MIN_INITIAL_SPLITS_RATIO = 1.0 - 1.0 / Math.sqrt(5.0);
+    static final double MAX_INITIAL_SPLITS_RATIO = MIN_INITIAL_SPLITS_RATIO + 
0.075;
+
     final NavigableMap<Token, Unit> sortedTokens;
     final ReplicationStrategy<Unit> strategy;
     final IPartitioner partitioner;
@@ -79,6 +86,59 @@ public abstract class TokenAllocatorBase<Unit> implements 
TokenAllocator<Unit>
         return group;
     }
 
+    Collection<Token> generateSplits(Unit newUnit, int numTokens)
+    {
+        return generateSplits(newUnit, numTokens, MIN_INITIAL_SPLITS_RATIO, 
MAX_INITIAL_SPLITS_RATIO);
+    }
+    /**
+     * Selects tokens by repeatedly splitting the largest range in the ring at 
the given ratio.
+     *
+     * This is used to choose tokens for the first nodes in the ring where the 
algorithm cannot be applied (e.g. when
+     * number of nodes < RF). It generates a reasonably chaotic initial token 
split, after which the algorithm behaves
+     * well for an unbounded number of nodes.
+     */
+    Collection<Token> generateSplits(Unit newUnit, int numTokens, double 
minRatio, double maxRatio)
+    {
+        Random random = new Random(sortedTokens.size());
+
+        double potentialRatioGrowth = maxRatio - minRatio;
+
+        List<Token> tokens = Lists.newArrayListWithExpectedSize(numTokens);
+
+        if (sortedTokens.isEmpty())
+        {
+            // Select a random start token. This has no effect on 
distribution, only on where the local ring is "centered".
+            // Using a random start decreases the chances of clash with the 
tokens of other datacenters in the ring.
+            Token t = partitioner.getRandomToken();
+            tokens.add(t);
+            sortedTokens.put(t, newUnit);
+        }
+
+        while (tokens.size() < numTokens)
+        {
+            // split max span using given ratio
+            Token prev = sortedTokens.lastKey();
+            double maxsz = 0;
+            Token t1 = null;
+            Token t2 = null;
+            for (Token curr : sortedTokens.keySet())
+            {
+                double sz = prev.size(curr);
+                if (sz > maxsz)
+                {
+                    maxsz = sz;
+                    t1 = prev; t2 = curr;
+                }
+                prev = curr;
+            }
+            assert t1 != null;
+            Token t = partitioner.split(t1, t2, minRatio + 
potentialRatioGrowth * random.nextDouble());
+            tokens.add(t);
+            sortedTokens.put(t, newUnit);
+        }
+        return tokens;
+    }
+
     /**
      * Unique group object that one or more UnitInfo objects link to.
      */
diff --git a/test/unit/org/apache/cassandra/dht/BootStrapperTest.java 
b/test/unit/org/apache/cassandra/dht/BootStrapperTest.java
index c0b6d5c..c5cce58 100644
--- a/test/unit/org/apache/cassandra/dht/BootStrapperTest.java
+++ b/test/unit/org/apache/cassandra/dht/BootStrapperTest.java
@@ -20,10 +20,14 @@ package org.apache.cassandra.dht;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
+import org.junit.Assert;
 
+import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
+import java.util.Random;
 import java.util.UUID;
 
 import com.google.common.base.Predicate;
@@ -147,6 +151,8 @@ public class BootStrapperTest
         generateFakeEndpoints(tmd, numOldNodes, numVNodes, "0", "0");
     }
 
+    Random rand = new Random(1);
+
     private void generateFakeEndpoints(TokenMetadata tmd, int numOldNodes, int 
numVNodes, String dc, String rack) throws UnknownHostException
     {
         IPartitioner p = tmd.partitioner;
@@ -157,7 +163,7 @@ public class BootStrapperTest
             InetAddressAndPort addr = InetAddressAndPort.getByName("127." + dc 
+ "." + rack + "." + (i + 1));
             List<Token> tokens = Lists.newArrayListWithCapacity(numVNodes);
             for (int j = 0; j < numVNodes; ++j)
-                tokens.add(p.getRandomToken());
+                tokens.add(p.getRandomToken(rand));
             
             tmd.updateNormalTokens(tokens, addr);
         }
@@ -270,7 +276,54 @@ public class BootStrapperTest
         }
     }
 
-    
+    @Test
+    public void testAllocateTokensRfEqRacks() throws UnknownHostException
+    {
+        IEndpointSnitch oldSnitch = DatabaseDescriptor.getEndpointSnitch();
+        try
+        {
+            DatabaseDescriptor.setEndpointSnitch(new RackInferringSnitch());
+            int vn = 8;
+            int replicas = 3;
+            int rackCount = replicas;
+            String ks = "BootStrapperTestNTSKeyspaceRfEqRacks";
+            String dc = "1";
+
+            TokenMetadata metadata = 
StorageService.instance.getTokenMetadata();
+            metadata.clearUnsafe();
+            metadata.updateHostId(UUID.randomUUID(), 
InetAddressAndPort.getByName("127.1.0.99"));
+            metadata.updateHostId(UUID.randomUUID(), 
InetAddressAndPort.getByName("127.15.0.99"));
+
+            SchemaLoader.createKeyspace(ks, KeyspaceParams.nts(dc, replicas, 
"15", 15), SchemaLoader.standardCFMD(ks, "Standard1"));
+            int base = 5;
+            for (int i = 0; i < rackCount; ++i)
+                generateFakeEndpoints(metadata, base << i, vn, dc, 
Integer.toString(i));     // unbalanced racks
+
+            int cnt = 5;
+            for (int i = 0; i < cnt; ++i)
+                allocateTokensForNode(vn, ks, metadata, 
InetAddressAndPort.getByName("127." + dc + ".0." + (99 + i)));
+
+            double target = 1.0 / (base + cnt);
+            double permittedOver = 1.0 / (2 * vn + 1) + 0.01;
+
+            Map<InetAddress, Float> ownership = 
StorageService.instance.effectiveOwnership(ks);
+            boolean failed = false;
+            for (Map.Entry<InetAddress, Float> o : ownership.entrySet())
+            {
+                int rack = o.getKey().getAddress()[2];
+                if (rack != 0)
+                    continue;
+
+                System.out.format("Node %s owns %f ratio to optimal %.2f\n", 
o.getKey(), o.getValue(), o.getValue() / target);
+                if (o.getValue()/target > 1 + permittedOver)
+                    failed = true;
+            }
+            Assert.assertFalse(String.format("One of the nodes in the rack has 
over %.2f%% overutilization.", permittedOver * 100), failed);
+        } finally {
+            DatabaseDescriptor.setEndpointSnitch(oldSnitch);
+        }
+    }
+
     @Test
     public void testAllocateTokensMultipleKeyspaces() throws 
UnknownHostException
     {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to