diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DynamoDBMetadataStore.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DynamoDBMetadataStore.java index 590b9c49021..769d3d4c4c3 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DynamoDBMetadataStore.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DynamoDBMetadataStore.java @@ -210,6 +210,18 @@ public class DynamoDBMetadataStore implements MetadataStore, public static final String E_INCOMPATIBLE_VERSION = "Database table is from an incompatible S3Guard version."; + @VisibleForTesting + static final String BILLING_MODE + = "billing-mode"; + + @VisibleForTesting + static final String BILLING_MODE_PER_REQUEST + = "per-request"; + + @VisibleForTesting + static final String BILLING_MODE_PROVISIONED + = "provisioned"; + @VisibleForTesting static final String DESCRIPTION = "S3Guard metadata store in DynamoDB"; @@ -229,6 +241,9 @@ public class DynamoDBMetadataStore implements MetadataStore, @VisibleForTesting static final String THROTTLING = "Throttling"; + public static final String E_ON_DEMAND_NO_SET_CAPACITY + = "Neither ReadCapacityUnits nor WriteCapacityUnits can be specified when BillingMode is PAY_PER_REQUEST"; + private static ValueMap deleteTrackingValueMap = new ValueMap().withBoolean(":false", false); @@ -1515,6 +1530,10 @@ public class DynamoDBMetadataStore implements MetadataStore, = desc.getProvisionedThroughput(); map.put(READ_CAPACITY, throughput.getReadCapacityUnits().toString()); map.put(WRITE_CAPACITY, throughput.getWriteCapacityUnits().toString()); + map.put(BILLING_MODE, + throughput.getWriteCapacityUnits() == 0 + ? BILLING_MODE_PER_REQUEST + : BILLING_MODE_PROVISIONED); map.put(TABLE, desc.toString()); map.put(MetadataStoreCapabilities.PERSISTS_AUTHORITATIVE_BIT, Boolean.toString(true)); @@ -1558,6 +1577,11 @@ public class DynamoDBMetadataStore implements MetadataStore, S3GUARD_DDB_TABLE_CAPACITY_WRITE_KEY, currentWrite); + if (currentRead == 0 || currentWrite == 0) { + // table is pay on demand + throw new IOException(E_ON_DEMAND_NO_SET_CAPACITY); + } + if (newRead != currentRead || newWrite != currentWrite) { LOG.info("Current table capacity is read: {}, write: {}", currentRead, currentWrite); diff --git a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/s3guard.md b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/s3guard.md index 8e56df7c544..284956a5465 100644 --- a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/s3guard.md +++ b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/s3guard.md @@ -272,7 +272,7 @@ Next, you can choose whether or not the table will be automatically created ``` -### 7. If creating a table: Set your DynamoDB IO Capacity +### 7. If creating a table: Set your DynamoDB I/O Capacity Next, you need to set the DynamoDB read and write throughput requirements you expect to need for your cluster. Setting higher values will cost you more @@ -286,7 +286,7 @@ Unit Calculations](http://docs.aws.amazon.com/amazondynamodb/latest/developergui The charges are incurred per hour for the life of the table, *even when the table and the underlying S3 buckets are not being used*. -There are also charges incurred for data storage and for data IO outside of the +There are also charges incurred for data storage and for data I/O outside of the region of the DynamoDB instance. S3Guard only stores metadata in DynamoDB: path names and summary details of objects —the actual data is stored in S3, so billed at S3 rates. @@ -315,10 +315,10 @@ rates. ``` -Attempting to perform more IO than the capacity requested throttles the -IO, and may result in operations failing. Larger IO capacities cost more. +Attempting to perform more I/O than the capacity requested throttles the +I/O, and may result in operations failing. Larger I/O capacities cost more. We recommending using small read and write capacities when initially experimenting -with S3Guard. +with S3Guard, and considering DynamoDB On-Demand. ## Authenticating with S3Guard @@ -578,6 +578,7 @@ Filesystem s3a://ireland-1 is using S3Guard with store DynamoDBMetadataStore{reg Authoritative S3Guard: fs.s3a.metadatastore.authoritative=false Metadata Store Diagnostics: ARN=arn:aws:dynamodb:eu-west-1:00000000:table/ireland-1 + billing-mode=provisioned description=S3Guard metadata store in DynamoDB name=ireland-1 read-capacity=20 @@ -738,7 +739,7 @@ Delete all entries more than 90 minutes old from the table "ireland-team" in the region "eu-west-1". -### Tune the IO capacity of the DynamoDB Table, `s3guard set-capacity` +### Tune the I/O capacity of the DynamoDB Table, `s3guard set-capacity` Alter the read and/or write capacity of a s3guard table. @@ -764,6 +765,7 @@ and 20 write. (This is a low number, incidentally) 2017-08-30 16:21:26,344 [main] INFO s3guard.DynamoDBMetadataStore (DynamoDBMetadataStore.java:updateParameters(1086)) - Changing capacity of table to read: 20, write: 20 Metadata Store Diagnostics: ARN=arn:aws:dynamodb:eu-west-1:00000000000:table/ireland-1 + billing-mode=provisioned description=S3Guard metadata store in DynamoDB name=ireland-1 read-capacity=25 @@ -785,6 +787,7 @@ write values match that already in use. 2017-08-30 16:24:35,337 [main] INFO s3guard.DynamoDBMetadataStore (DynamoDBMetadataStore.java:updateParameters(1090)) - Table capacity unchanged at read: 20, write: 20 Metadata Store Diagnostics: ARN=arn:aws:dynamodb:eu-west-1:00000000000:table/ireland-1 + billing-mode=provisioned description=S3Guard metadata store in DynamoDB name=ireland-1 read-capacity=20 @@ -880,10 +883,10 @@ are only made after successful file creation, deletion and rename, the store is *unlikely* to get out of sync, it is still something which merits more testing before it could be considered reliable. -## Managing DynamoDB IO Capacity +## Managing DynamoDB I/O Capacity -DynamoDB is not only billed on use (data and IO requests), it is billed -on allocated IO Capacity. +By default, DynamoDB is not only billed on use (data and I/O requests) +-it is billed on allocated I/O Capacity. When an application makes more requests than the allocated capacity permits, the request is rejected; it is up to @@ -954,22 +957,102 @@ If operations, especially directory operations, are slow, check the AWS console. It is also possible to set up AWS alerts for capacity limits being exceeded. +### On-Demand Dynamo Capacity + +[Amazon DynamoDB On-Demand](https://aws.amazon.com/blogs/aws/amazon-dynamodb-on-demand-no-capacity-planning-and-pay-per-request-pricing/) +removes the need to pre-allocate I/O capacity for S3Guard tables. +Instead the caller is _only_ charged per I/O Operation. + +* There are no SLA capacity guarantees. This is generally not an issue +for S3Guard applications. +* There's no explicit limit on I/O capacity, so operations which make +heavy use of S3Guard tables (for example: SQL query planning) do not +get throttled. +* There's no way put a limit on the I/O; you may unintentionally run up +large bills through sustained heavy load. +* The `s3guard set-capacity` command fails: it does not make sense any more. + +When idle, S3Guard tables are only billed for the data stored, not for +any unused capacity. For this reason, there is no benefit from sharing +a single S3Guard table across multiple buckets. + +*Enabling DynamoDB On-Demand for a S3Guard table* + +You cannot currently enable DynamoDB on-demand from the `s3guard` command +when creating or updating a bucket. + +Instead it must be done through the AWS console or [the CLI](https://docs.aws.amazon.com/cli/latest/reference/dynamodb/update-table.html). +From the Web console or the command line, switch the billing to pay-per-request. + +Once enabled, the read and write capacities of the table listed in the +`hadoop s3guard bucket-info` command become "0", and the "billing-mode" +attribute changes to "per-request": + +``` +> hadoop s3guard bucket-info s3a://example-bucket/ + +Filesystem s3a://example-bucket +Location: eu-west-1 +Filesystem s3a://example-bucket is using S3Guard with store + DynamoDBMetadataStore{region=eu-west-1, tableName=example-bucket, + tableArn=arn:aws:dynamodb:eu-west-1:11111122223333:table/example-bucket} +Authoritative S3Guard: fs.s3a.metadatastore.authoritative=false +Metadata Store Diagnostics: + ARN=arn:aws:dynamodb:eu-west-1:11111122223333:table/example-bucket + billing-mode=per-request + description=S3Guard metadata store in DynamoDB + name=example-bucket + persist.authoritative.bit=true + read-capacity=0 + region=eu-west-1 + retryPolicy=ExponentialBackoffRetry(maxRetries=9, sleepTime=250 MILLISECONDS) + size=66797 + status=ACTIVE + table={AttributeDefinitions: + [{AttributeName: child,AttributeType: S}, + {AttributeName: parent,AttributeType: S}], + TableName: example-bucket, + KeySchema: [{ + AttributeName: parent,KeyType: HASH}, + {AttributeName: child,KeyType: RANGE}], + TableStatus: ACTIVE, + CreationDateTime: Thu Oct 11 18:51:14 BST 2018, + ProvisionedThroughput: { + LastIncreaseDateTime: Tue Oct 30 16:48:45 GMT 2018, + LastDecreaseDateTime: Tue Oct 30 18:00:03 GMT 2018, + NumberOfDecreasesToday: 0, + ReadCapacityUnits: 0, + WriteCapacityUnits: 0}, + TableSizeBytes: 66797, + ItemCount: 415, + TableArn: arn:aws:dynamodb:eu-west-1:11111122223333:table/example-bucket, + TableId: a7b0728a-f008-4260-b2a0-aaaaabbbbb,} + write-capacity=0 +The "magic" committer is supported +``` + +### Autoscaling S3Guard tables. + [DynamoDB Auto Scaling](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/AutoScaling.html) can automatically increase and decrease the allocated capacity. -This is good for keeping capacity high when needed, but avoiding large -bills when it is not. + +Before DynamoDB On-Demand was introduced, autoscaling was the sole form +of dynamic scaling. Experiments with S3Guard and DynamoDB Auto Scaling have shown that any Auto Scaling operation will only take place after callers have been throttled for a period of time. The clients will still need to be configured to retry when overloaded until any extra capacity is allocated. Furthermore, as this retrying will -block the threads from performing other operations -including more IO, the +block the threads from performing other operations -including more I/O, the the autoscale may not scale fast enough. -We recommend experimenting with this, based on usage information collected -from previous days, and and choosing a combination of -retry counts and an interval which allow for the clients to cope with -some throttling, but not to time out other applications. +This is why the DynamoDB On-Demand appears to be a better option for +workloads with Hadoop, Spark, Hive and other applications. + +If autoscaling is to be used, we recommend experimenting with the option, +based on usage information collected from previous days, and choosing a +combination of retry counts and an interval which allow for the clients to cope with +some throttling, but not to time-out other applications. ## Troubleshooting @@ -1022,7 +1105,7 @@ Consider increasing your provisioning level with the UpdateTable API. (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: ProvisionedThroughputExceededException; ``` -The IO load of clients of the (shared) DynamoDB table was exceeded. +The I/O load of clients of the (shared) DynamoDB table was exceeded. 1. Increase the capacity of the DynamoDB table. 1. Increase the retry count and/or sleep time of S3Guard on throttle events. @@ -1069,6 +1152,18 @@ java.io.IOException: Invalid region specified "iceland-2": The region specified in `fs.s3a.s3guard.ddb.region` is invalid. +# "Neither ReadCapacityUnits nor WriteCapacityUnits can be specified when BillingMode is PAY_PER_REQUEST" + +``` +ValidationException; One or more parameter values were invalid: + Neither ReadCapacityUnits nor WriteCapacityUnits can be specified when + BillingMode is PAY_PER_REQUEST + (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: ValidationException) +``` + +On-Demand DynamoDB tables do not have any fixed capacity -it is an error +to try to change it with the `set-capacity` command. + ## Other Topics For details on how to test S3Guard, see [Testing S3Guard](./testing.html#s3guard) diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/DDBCapacities.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/DDBCapacities.java new file mode 100644 index 00000000000..c6e47c75185 --- /dev/null +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/DDBCapacities.java @@ -0,0 +1,108 @@ +/* + * 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.hadoop.fs.s3a.s3guard; + +import java.util.Map; +import java.util.Objects; + +import org.junit.Assert; + +import static org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore.READ_CAPACITY; + +class DDBCapacities { + private final long read, write; + + DDBCapacities(long read, long write) { + this.read = read; + this.write = write; + } + + public long getRead() { + return read; + } + + public long getWrite() { + return write; + } + + String getReadStr() { + return Long.toString(read); + } + + String getWriteStr() { + return Long.toString(write); + } + + void checkEquals(String text, DDBCapacities that) throws Exception { + if (!this.equals(that)) { + throw new Exception(text + " expected = " + this +"; actual = "+ that); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DDBCapacities that = (DDBCapacities) o; + return read == that.read && write == that.write; + } + + @Override + public int hashCode() { + return Objects.hash(read, write); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Capacities{"); + sb.append("read=").append(read); + sb.append(", write=").append(write); + sb.append('}'); + return sb.toString(); + } + + /** + * Is the the capacity that of a pay-on-demand table? + * @return true if the capacities are both 0. + */ + public boolean isOnDemandTable() { + return read == 0 && write == 0; + } + + /** + * Given a diagnostics map from a DDB store, extract the capacities. + * @param diagnostics diagnostics map to examine. + * @return the capacities + * @throws AssertionError if the fields are missing. + */ + public static DDBCapacities extractCapacities( + final Map diagnostics) { + String read = diagnostics.get(READ_CAPACITY); + Assert.assertNotNull("No " + READ_CAPACITY + " attribute in diagnostics", + read); + return new DDBCapacities( + Long.parseLong(read), + Long.parseLong(diagnostics.get(DynamoDBMetadataStore.WRITE_CAPACITY))); + } + +} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestDynamoDBMetadataStoreScale.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestDynamoDBMetadataStoreScale.java index 48dbce98a77..408c72eb8f5 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestDynamoDBMetadataStoreScale.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestDynamoDBMetadataStoreScale.java @@ -116,6 +116,10 @@ public class ITestDynamoDBMetadataStoreScale assumeTrue("Metadata store for " + fs.getUri() + " is " + store + " -not DynamoDBMetadataStore", store instanceof DynamoDBMetadataStore); + DDBCapacities capacities = DDBCapacities.extractCapacities( + store.getDiagnostics()); + assumeTrue("DBB table is on-demand", + !capacities.isOnDemandTable()); DynamoDBMetadataStore fsStore = (DynamoDBMetadataStore) store; Configuration conf = new Configuration(fs.getConf()); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardToolDynamoDB.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardToolDynamoDB.java index aa88b0b118b..aaaae2e4621 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardToolDynamoDB.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardToolDynamoDB.java @@ -22,7 +22,6 @@ import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Random; import java.util.UUID; import java.util.concurrent.Callable; @@ -39,17 +38,17 @@ import org.junit.Test; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.s3a.Constants; import org.apache.hadoop.fs.s3a.S3AFileSystem; -import org.apache.hadoop.fs.s3a.S3AUtils; import org.apache.hadoop.fs.s3a.s3guard.S3GuardTool.Destroy; import org.apache.hadoop.fs.s3a.s3guard.S3GuardTool.Init; -import org.apache.hadoop.test.LambdaTestUtils; import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_REGION_KEY; import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_TABLE_NAME_KEY; import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_TABLE_TAG; +import static org.apache.hadoop.fs.s3a.S3AUtils.setBucketOption; import static org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore.*; import static org.apache.hadoop.fs.s3a.s3guard.S3GuardTool.*; import static org.apache.hadoop.fs.s3a.s3guard.S3GuardToolTestHelper.exec; +import static org.apache.hadoop.test.LambdaTestUtils.intercept; /** * Test S3Guard related CLI commands against DynamoDB. @@ -85,7 +84,7 @@ public class ITestS3GuardToolDynamoDB extends AbstractS3GuardToolTestBase { final String testRegion = "invalidRegion"; // Initialize MetadataStore final Init initCmd = new Init(getFileSystem().getConf()); - LambdaTestUtils.intercept(IOException.class, + intercept(IOException.class, new Callable() { @Override public String call() throws Exception { @@ -160,73 +159,8 @@ public class ITestS3GuardToolDynamoDB extends AbstractS3GuardToolTestBase { return stringBuilder.toString(); } - - private static class Capacities { - private final long read, write; - - Capacities(long read, long write) { - this.read = read; - this.write = write; - } - - public long getRead() { - return read; - } - - public long getWrite() { - return write; - } - - String getReadStr() { - return Long.toString(read); - } - - String getWriteStr() { - return Long.toString(write); - } - - void checkEquals(String text, Capacities that) throws Exception { - if (!this.equals(that)) { - throw new Exception(text + " expected = " + this +"; actual = "+ that); - } - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Capacities that = (Capacities) o; - return read == that.read && write == that.write; - } - - @Override - public int hashCode() { - return Objects.hash(read, write); - } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder("Capacities{"); - sb.append("read=").append(read); - sb.append(", write=").append(write); - sb.append('}'); - return sb.toString(); - } - } - - private Capacities getCapacities() throws IOException { - Map diagnostics = getMetadataStore().getDiagnostics(); - return getCapacities(diagnostics); - } - - private Capacities getCapacities(Map diagnostics) { - return new Capacities( - Long.parseLong(diagnostics.get(DynamoDBMetadataStore.READ_CAPACITY)), - Long.parseLong(diagnostics.get(DynamoDBMetadataStore.WRITE_CAPACITY))); + private DDBCapacities getCapacities() throws IOException { + return DDBCapacities.extractCapacities(getMetadataStore().getDiagnostics()); } @Test @@ -240,7 +174,11 @@ public class ITestS3GuardToolDynamoDB extends AbstractS3GuardToolTestBase { Init initCmd = new Init(fs.getConf()); expectSuccess("Init command did not exit successfully - see output", initCmd, - "init", "-meta", "dynamodb://" + testTableName, testS3Url); + Init.NAME, + "-" + READ_FLAG, "2", + "-" + WRITE_FLAG, "2", + "-" + META_FLAG, "dynamodb://" + testTableName, + testS3Url); // Verify it exists MetadataStore ms = getMetadataStore(); assertTrue("metadata store should be DynamoDBMetadataStore", @@ -253,7 +191,7 @@ public class ITestS3GuardToolDynamoDB extends AbstractS3GuardToolTestBase { Configuration conf = fs.getConf(); String bucket = fs.getBucket(); // force in a new bucket - S3AUtils.setBucketOption(conf, bucket, Constants.S3_METADATA_STORE_IMPL, + setBucketOption(conf, bucket, Constants.S3_METADATA_STORE_IMPL, Constants.S3GUARD_METASTORE_DYNAMO); initCmd = new Init(conf); String initOutput = exec(initCmd, @@ -273,18 +211,32 @@ public class ITestS3GuardToolDynamoDB extends AbstractS3GuardToolTestBase { // get the current values to set again // play with the set-capacity option - Capacities original = getCapacities(); - String fsURI = getFileSystem().getUri().toString(); - String capacityOut = exec(newSetCapacity(), - S3GuardTool.SetCapacity.NAME, - fsURI); - LOG.info("Set Capacity output=\n{}", capacityOut); - capacityOut = exec(newSetCapacity(), - S3GuardTool.SetCapacity.NAME, - "-" + READ_FLAG, original.getReadStr(), - "-" + WRITE_FLAG, original.getWriteStr(), - fsURI); - LOG.info("Set Capacity output=\n{}", capacityOut); + DDBCapacities original = getCapacities(); + String fsURI = getFileSystem().getUri().toString(); + if (!original.isOnDemandTable()) { + // classic provisioned table + assertTrue("Wrong billing mode in " + info, + info.contains(BILLING_MODE_PROVISIONED)); + String capacityOut = exec(newSetCapacity(), + SetCapacity.NAME, + fsURI); + LOG.info("Set Capacity output=\n{}", capacityOut); + capacityOut = exec(newSetCapacity(), + SetCapacity.NAME, + "-" + READ_FLAG, original.getReadStr(), + "-" + WRITE_FLAG, original.getWriteStr(), + fsURI); + LOG.info("Set Capacity output=\n{}", capacityOut); + } else { + // on demand table + assertTrue("Wrong billing mode in " + info, + info.contains(BILLING_MODE_PER_REQUEST)); + // on demand tables fail here, so expect that + intercept(IOException.class, E_ON_DEMAND_NO_SET_CAPACITY, + () -> exec(newSetCapacity(), + SetCapacity.NAME, + fsURI)); + } // that call does not change the values original.checkEquals("unchanged", getCapacities());