Use standard Amazon naming for datacenter and rack in Ec2Snitch.

patch by Gregory Ramsperger and jasobrown; reviewed by Joey Lynch for CASSANDRA-7839
This commit is contained in:
Gregory Ramsperger 2016-04-16 00:42:03 -05:00 committed by Jason Brown
parent 8a5e1cbe29
commit fd44a69fce
10 changed files with 279 additions and 19 deletions

View File

@ -1,4 +1,5 @@
4.0
* Use standard Amazon naming for datacenter and rack in Ec2Snitch (CASSANDRA-7839)
* Fix junit failure for SSTableReaderTest (CASSANDRA-14387)
* Abstract write path for pluggable storage (CASSANDRA-14118)
* nodetool describecluster should be more informative (CASSANDRA-13853)

View File

@ -141,6 +141,10 @@ Upgrading
storage port or native port at nodes you must first upgrade the entire cluster
and clients to 4.0 so they can handle the port not being consistent across
the cluster.
- Names of AWS regions/availability zones have been cleaned up to more correctly
match the Amazon names. There is now a new option in conf/cassandra-rackdc.properties
that lets users enable the correct names for new clusters, or use the legacy
names for existing clusters. See conf/cassandra-rackdc.properties for details.
Materialized Views
-------------------

View File

@ -25,3 +25,15 @@ rack=rack1
# Uncomment the following line to make this snitch prefer the internal ip when possible, as the Ec2MultiRegionSnitch does.
# prefer_local=true
# Datacenter and rack naming convention used by the Ec2Snitch and Ec2MultiRegionSnitch.
# Options are:
# legacy : datacenter name is the part of the availability zone name preceding the last "-"
# when the zone ends in -1 and includes the number if not -1. Rack is the portion of
# the availability zone name following the last "-".
# Examples: us-west-1a => dc: us-west, rack: 1a; us-west-2b => dc: us-west-2, rack: 2b;
# YOU MUST USE THIS VALUE IF YOU ARE UPGRADING A PRE-4.0 CLUSTER
# standard : Default value. datacenter name is the standard AWS region name, including the number.
# rack name is the region plus the availability zone letter.
# Examples: us-west-1a => dc: us-west-1, rack: us-west-1a; us-west-2b => dc: us-west-2, rack: us-west-2b;
# ec2_naming_scheme=standard

View File

@ -448,4 +448,9 @@ public class DynamicEndpointSnitch extends AbstractEndpointSnitch implements ILa
}
return maxScore;
}
public boolean validate(Set<String> datacenters, Set<String> racks)
{
return subsnitch.validate(datacenters, racks);
}
}

View File

@ -24,7 +24,9 @@ import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Set;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.db.SystemKeyspace;
@ -42,30 +44,63 @@ import org.apache.cassandra.utils.FBUtilities;
public class Ec2Snitch extends AbstractNetworkTopologySnitch
{
protected static final Logger logger = LoggerFactory.getLogger(Ec2Snitch.class);
protected static final String ZONE_NAME_QUERY_URL = "http://169.254.169.254/latest/meta-data/placement/availability-zone";
private static final String SNITCH_PROP_NAMING_SCHEME = "ec2_naming_scheme";
static final String EC2_NAMING_LEGACY = "legacy";
private static final String EC2_NAMING_STANDARD = "standard";
private static final String ZONE_NAME_QUERY_URL = "http://169.254.169.254/latest/meta-data/placement/availability-zone";
private static final String DEFAULT_DC = "UNKNOWN-DC";
private static final String DEFAULT_RACK = "UNKNOWN-RACK";
final String ec2region;
private final String ec2zone;
private final boolean usingLegacyNaming;
private Map<InetAddressAndPort, Map<String, String>> savedEndpoints;
protected String ec2zone;
protected String ec2region;
public Ec2Snitch() throws IOException, ConfigurationException
{
this(new SnitchProperties());
}
public Ec2Snitch(SnitchProperties props) throws IOException, ConfigurationException
{
String az = awsApiCall(ZONE_NAME_QUERY_URL);
// Split "us-east-1a" or "asia-1a" into "us-east"/"1a" and "asia"/"1a".
String[] splits = az.split("-");
ec2zone = splits[splits.length - 1];
// hack for CASSANDRA-4026
ec2region = az.substring(0, az.length() - 1);
if (ec2region.endsWith("1"))
ec2region = az.substring(0, az.length() - 3);
// if using the full naming scheme, region name is created by removing letters from the
// end of the availability zone and zone is the full zone name
usingLegacyNaming = isUsingLegacyNaming(props);
String region;
if (usingLegacyNaming)
{
// Split "us-east-1a" or "asia-1a" into "us-east"/"1a" and "asia"/"1a".
String[] splits = az.split("-");
ec2zone = splits[splits.length - 1];
String datacenterSuffix = (new SnitchProperties()).get("dc_suffix", "");
ec2region = ec2region.concat(datacenterSuffix);
// hack for CASSANDRA-4026
region = az.substring(0, az.length() - 1);
if (region.endsWith("1"))
region = az.substring(0, az.length() - 3);
}
else
{
// grab the region name, which is embedded in the availability zone name.
// thus an AZ of "us-east-1a" yields the region name "us-east-1"
region = az.replaceFirst("[a-z]+$","");
ec2zone = az;
}
String datacenterSuffix = props.get("dc_suffix", "");
ec2region = region.concat(datacenterSuffix);
logger.info("EC2Snitch using region: {}, zone: {}.", ec2region, ec2zone);
}
private static boolean isUsingLegacyNaming(SnitchProperties props)
{
return props.get(SNITCH_PROP_NAMING_SCHEME, EC2_NAMING_STANDARD).equalsIgnoreCase(EC2_NAMING_LEGACY);
}
String awsApiCall(String url) throws IOException, ConfigurationException
{
// Populate the region and zone by introspection, fail if 404 on metadata
@ -122,4 +157,54 @@ public class Ec2Snitch extends AbstractNetworkTopologySnitch
}
return state.getApplicationState(ApplicationState.DC).value;
}
@Override
public boolean validate(Set<String> datacenters, Set<String> racks)
{
return validate(datacenters, racks, usingLegacyNaming);
}
@VisibleForTesting
static boolean validate(Set<String> datacenters, Set<String> racks, boolean usingLegacyNaming)
{
boolean valid = true;
for (String dc : datacenters)
{
// predicated on the late-2017 AWS naming 'convention' that all region names end with a digit.
// Unfortunately, life isn't that simple. Since we allow custom datacenter suffixes (CASSANDRA-5155),
// an operator could conceiveably make the suffix "a", and thus create a region name that looks just like
// one of the region's availability zones. (for example, "us-east-1a").
// Further, we can't make any assumptions of what that suffix might be by looking at this node's
// datacenterSuffix as conceivably their could be many different suffixes in play for a given region.
//
// Thus, the best we can do is make sure the region name follows
// the basic region naming pattern: "us-east-1<custom-suffix>"
boolean dcUsesLegacyFormat = !dc.matches("[a-z]+-[a-z].+-[\\d].*");
if (dcUsesLegacyFormat != usingLegacyNaming)
valid = false;
}
for (String rack : racks)
{
// predicated on late-2017 AWS naming 'convention' that AZs do not have a digit as the first char -
// we had that in our legacy AZ (rack) names. Thus we test to see if the rack is in the legacy format.
//
// NOTE: the allowed custom suffix only applies to datacenter (region) names, not availability zones.
boolean rackUsesLegacyFormat = rack.matches("[\\d][a-z]");
if (rackUsesLegacyFormat != usingLegacyNaming)
valid = false;
}
if (!valid)
{
logger.error("This ec2-enabled snitch appears to be using the {} naming scheme for regions, " +
"but existing nodes in cluster are using the opposite: region(s) = {}, availability zone(s) = {}. " +
"Please check the {} property in the {} configuration file for more details.",
usingLegacyNaming ? "legacy" : "standard", datacenters, racks,
SNITCH_PROP_NAMING_SCHEME, SnitchProperties.RACKDC_PROPERTY_FILENAME);
}
return valid;
}
}

View File

@ -19,6 +19,7 @@ package org.apache.cassandra.locator;
import java.util.Collection;
import java.util.List;
import java.util.Set;
/**
* This interface helps determine location of node in the datacenter relative to another node.
@ -63,4 +64,12 @@ public interface IEndpointSnitch
* to be faster than 2 sequential queries, one against l1 followed by one against l2.
*/
public boolean isWorthMergingForRangeQuery(List<InetAddressAndPort> merged, List<InetAddressAndPort> l1, List<InetAddressAndPort> l2);
/**
* Determine if the datacenter or rack values in the current node's snitch conflict with those passed in parameters.
*/
default boolean validate(Set<String> datacenters, Set<String> racks)
{
return true;
}
}

View File

@ -30,7 +30,7 @@ public class SnitchProperties
private static final Logger logger = LoggerFactory.getLogger(SnitchProperties.class);
public static final String RACKDC_PROPERTY_FILENAME = "cassandra-rackdc.properties";
private Properties properties;
private final Properties properties;
public SnitchProperties()
{

View File

@ -482,6 +482,8 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
if (epStates.get(replaceAddress) == null)
throw new RuntimeException(String.format("Cannot replace_address %s because it doesn't exist in gossip", replaceAddress));
validateEndpointSnitch(epStates.values().iterator());
try
{
VersionedValue tokensVersionedValue = epStates.get(replaceAddress).getApplicationState(ApplicationState.TOKENS);
@ -527,6 +529,8 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
FBUtilities.getBroadcastAddressAndPort()));
}
validateEndpointSnitch(epStates.values().iterator());
if (shouldBootstrap() && useStrictConsistency && !allowSimultaneousMoves())
{
for (Map.Entry<InetAddressAndPort, EndpointState> entry : epStates.entrySet())
@ -550,6 +554,28 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
}
}
private static void validateEndpointSnitch(Iterator<EndpointState> endpointStates)
{
Set<String> datacenters = new HashSet<>();
Set<String> racks = new HashSet<>();
while (endpointStates.hasNext())
{
EndpointState state = endpointStates.next();
VersionedValue val = state.getApplicationState(ApplicationState.DC);
if (val != null)
datacenters.add(val.value);
val = state.getApplicationState(ApplicationState.RACK);
if (val != null)
racks.add(val.value);
}
IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch();
if (!snitch.validate(datacenters, racks))
{
throw new IllegalStateException();
}
}
private boolean allowSimultaneousMoves()
{
return allowSimultaneousMoves && DatabaseDescriptor.getNumTokens() == 1;

View File

@ -22,3 +22,15 @@ rack=RAC1
# Add a suffix to a datacenter name. Used by the Ec2Snitch and Ec2MultiRegionSnitch
# to append a string to the EC2 region name.
#dc_suffix=
# Datacenter and rack naming convention used by the Ec2Snitch and Ec2MultiRegionSnitch.
# Options are:
# legacy : datacenter name is the part of the availability zone name preceding the last "-"
# when the zone ends in -1 and includes the number if not -1. Rack is the portion of
# the availability zone name following the last "-".
# Examples: us-west-1a => dc: us-west, rack: 1a; us-west-2b => dc: us-west-2, rack: 2b;
# standard : datacenter name is the standard AWS region name, including the number. rack name is the
# region plus the availability zone letter.
# Examples: us-west-1a => dc: us-west-1, rack: us-west-1a; us-west-2b => dc: us-west-2, rack: us-west-2b;
# default: standard
ec2_naming_scheme=standard

View File

@ -20,11 +20,14 @@ package org.apache.cassandra.locator;
import java.io.IOException;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
@ -37,12 +40,21 @@ import org.apache.cassandra.gms.Gossiper;
import org.apache.cassandra.gms.VersionedValue;
import org.apache.cassandra.service.StorageService;
import static org.apache.cassandra.locator.Ec2Snitch.EC2_NAMING_LEGACY;
import static org.junit.Assert.assertEquals;
public class EC2SnitchTest
{
private static String az;
private final SnitchProperties legacySnitchProps = new SnitchProperties()
{
public String get(String propertyName, String defaultValue)
{
return propertyName.equals("ec2_naming_scheme") ? EC2_NAMING_LEGACY : super.get(propertyName, defaultValue);
}
};
@BeforeClass
public static void setup() throws Exception
{
@ -60,6 +72,11 @@ public class EC2SnitchTest
super();
}
public TestEC2Snitch(SnitchProperties props) throws IOException, ConfigurationException
{
super(props);
}
@Override
String awsApiCall(String url) throws IOException, ConfigurationException
{
@ -68,10 +85,10 @@ public class EC2SnitchTest
}
@Test
public void testRac() throws IOException, ConfigurationException
public void testLegacyRac() throws IOException, ConfigurationException
{
az = "us-east-1d";
Ec2Snitch snitch = new TestEC2Snitch();
Ec2Snitch snitch = new TestEC2Snitch(legacySnitchProps);
InetAddressAndPort local = InetAddressAndPort.getByName("127.0.0.1");
InetAddressAndPort nonlocal = InetAddressAndPort.getByName("127.0.0.7");
@ -87,17 +104,106 @@ public class EC2SnitchTest
assertEquals("us-east", snitch.getDatacenter(local));
assertEquals("1d", snitch.getRack(local));
}
@Test
public void testNewRegions() throws IOException, ConfigurationException
public void testLegacyNewRegions() throws IOException, ConfigurationException
{
az = "us-east-2d";
Ec2Snitch snitch = new TestEC2Snitch();
Ec2Snitch snitch = new TestEC2Snitch(legacySnitchProps);
InetAddressAndPort local = InetAddressAndPort.getByName("127.0.0.1");
assertEquals("us-east-2", snitch.getDatacenter(local));
assertEquals("2d", snitch.getRack(local));
}
@Test
public void testFullNamingScheme() throws IOException, ConfigurationException
{
InetAddressAndPort local = InetAddressAndPort.getByName("127.0.0.1");
az = "us-east-2d";
Ec2Snitch snitch = new TestEC2Snitch();
assertEquals("us-east-2", snitch.getDatacenter(local));
assertEquals("us-east-2d", snitch.getRack(local));
az = "us-west-1a";
snitch = new TestEC2Snitch();
assertEquals("us-west-1", snitch.getDatacenter(local));
assertEquals("us-west-1a", snitch.getRack(local));
}
@Test
public void validateDatacenter_RequiresLegacy_CorrectAmazonName()
{
Set<String> datacenters = new HashSet<>();
datacenters.add("us-east-1");
Assert.assertFalse(Ec2Snitch.validate(datacenters, Collections.emptySet(), true));
}
@Test
public void validateDatacenter_RequiresLegacy_LegacyName()
{
Set<String> datacenters = new HashSet<>();
datacenters.add("us-east");
Assert.assertTrue(Ec2Snitch.validate(datacenters, Collections.emptySet(), true));
}
@Test
public void validate_RequiresLegacy_HappyPath()
{
Set<String> datacenters = new HashSet<>();
datacenters.add("us-east");
Set<String> racks = new HashSet<>();
racks.add("1a");
Assert.assertTrue(Ec2Snitch.validate(datacenters, racks, true));
}
@Test
public void validate_RequiresLegacy_HappyPathWithDCSuffix()
{
Set<String> datacenters = new HashSet<>();
datacenters.add("us-east_CUSTOM_SUFFIX");
Set<String> racks = new HashSet<>();
racks.add("1a");
Assert.assertTrue(Ec2Snitch.validate(datacenters, racks, true));
}
@Test
public void validateRack_RequiresAmazonName_CorrectAmazonName()
{
Set<String> racks = new HashSet<>();
racks.add("us-east-1a");
Assert.assertTrue(Ec2Snitch.validate(Collections.emptySet(), racks, false));
}
@Test
public void validateRack_RequiresAmazonName_LegacyName()
{
Set<String> racks = new HashSet<>();
racks.add("1a");
Assert.assertFalse(Ec2Snitch.validate(Collections.emptySet(), racks, false));
}
@Test
public void validate_RequiresAmazonName_HappyPath()
{
Set<String> datacenters = new HashSet<>();
datacenters.add("us-east-1");
Set<String> racks = new HashSet<>();
racks.add("us-east-1a");
Assert.assertTrue(Ec2Snitch.validate(datacenters, racks, false));
}
@Test
public void validate_RequiresAmazonName_HappyPathWithDCSuffix()
{
Set<String> datacenters = new HashSet<>();
datacenters.add("us-east-1_CUSTOM_SUFFIX");
Set<String> racks = new HashSet<>();
racks.add("us-east-1a");
Assert.assertTrue(Ec2Snitch.validate(datacenters, racks, false));
}
@AfterClass
public static void tearDown()
{