This is an automated email from the ASF dual-hosted git repository.
sammichen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/master by this push:
new 8e52a3a6f2 HDDS-8773. [S3G] Improve list performance in FSO bucket
(#4868)
8e52a3a6f2 is described below
commit 8e52a3a6f2c9819163e602765fd4dbe9501a0f0f
Author: Hongbing Wang <[email protected]>
AuthorDate: Thu Nov 9 12:13:16 2023 +0800
HDDS-8773. [S3G] Improve list performance in FSO bucket (#4868)
---
.../apache/hadoop/ozone/client/OzoneBucket.java | 271 +++++++++++++++------
.../hadoop/ozone/om/TestListKeysWithFSO.java | 96 +++++++-
2 files changed, 290 insertions(+), 77 deletions(-)
diff --git
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneBucket.java
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneBucket.java
index c4c68c00fd..d6e99dd139 100644
---
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneBucket.java
+++
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneBucket.java
@@ -1072,6 +1072,10 @@ public class OzoneBucket extends WithMetadata {
private boolean addedKeyPrefix;
private String delimiterKeyPrefix;
+ boolean shallow() {
+ return shallow;
+ }
+
String getKeyPrefix() {
return keyPrefix;
}
@@ -1088,6 +1092,14 @@ public class OzoneBucket extends WithMetadata {
this.addedKeyPrefix = addedKeyPrefix;
}
+ String getDelimiterKeyPrefix() {
+ return delimiterKeyPrefix;
+ }
+
+ void setDelimiterKeyPrefix(String delimiterKeyPrefix) {
+ this.delimiterKeyPrefix = delimiterKeyPrefix;
+ }
+
/**
* Creates an Iterator to iterate over all keys after prevKey in the
bucket.
* If prevKey is null it iterates from the first key in the bucket.
@@ -1142,8 +1154,8 @@ public class OzoneBucket extends WithMetadata {
}
/**
- * Using listStatus instead of listKeys avoiding listing all children keys.
- * Giving the structure of keys delimited by "/":
+ * Using listStatusLight instead of listKeys avoiding listing all children
+ * keys. Giving the structure of keys delimited by "/":
*
* buck-1
* |
@@ -1192,15 +1204,16 @@ public class OzoneBucket extends WithMetadata {
* In implementation, the keyPrefix "a/b" can be identified in listKeys,
* but cannot be identified in listStatus. Therefore, keyPrefix "a/b"
* needs to be split into keyPrefix "a" and call listKeys method to get
- * the next one key as the startKey in listStatus.
+ * the next one key as the startKey in listStatusLight.
*/
- protected List<OzoneKey> getNextShallowListOfKeys(String prevKey)
+ List<OzoneKey> getNextShallowListOfKeys(String prevKey)
throws IOException {
List<OzoneKey> resultList = new ArrayList<>();
String startKey = prevKey;
- // handle for first round
+ // 1. Get first element as startKey
if (!addedKeyPrefix) {
+ initDelimiterKeyPrefix();
// prepare startKey
List<OzoneKey> nextOneKeys =
proxy.listKeys(volumeName, name, getKeyPrefix(), prevKey, 1);
@@ -1218,16 +1231,9 @@ public class OzoneBucket extends WithMetadata {
startKey.equals(getKeyPrefix())) {
resultList.add(nextOneKeys.get(0));
}
-
- // prepare delimiterKeyPrefix
- delimiterKeyPrefix = getKeyPrefix();
- if (!getKeyPrefix().endsWith(OZONE_URI_DELIMITER)) {
- delimiterKeyPrefix = OzoneFSUtils.getParentDir(getKeyPrefix());
- }
}
- // Elements in statuses must be sorted after startKey,
- // which means they come after the keyPrefix.
+ // 2. Get immediate children by listStatusLight method
List<OzoneFileStatusLight> statuses =
proxy.listStatusLight(volumeName, name, delimiterKeyPrefix, false,
startKey, listCacheSize, false);
@@ -1239,16 +1245,20 @@ public class OzoneBucket extends WithMetadata {
setAddedKeyPrefix(true);
}
- List<OzoneKey> ozoneKeys = buildOzoneKeysFromFileStatus(statuses)
- .stream()
- .filter(key -> StringUtils.startsWith(key.getName(), getKeyPrefix()))
- .collect(Collectors.toList());
+ List<OzoneKey> ozoneKeys = buildKeysWithKeyPrefix(statuses);
resultList.addAll(ozoneKeys);
return resultList;
}
- private List<OzoneKey> buildOzoneKeysFromFileStatus(
+ protected void initDelimiterKeyPrefix() {
+ setDelimiterKeyPrefix(getKeyPrefix());
+ if (!getKeyPrefix().endsWith(OZONE_URI_DELIMITER)) {
+ setDelimiterKeyPrefix(OzoneFSUtils.getParentDir(getKeyPrefix()));
+ }
+ }
+
+ protected List<OzoneKey> buildKeysWithKeyPrefix(
List<OzoneFileStatusLight> statuses) {
return statuses.stream()
.map(status -> {
@@ -1264,6 +1274,7 @@ public class OzoneBucket extends WithMetadata {
keyInfo.getModificationTime(),
keyInfo.getReplicationConfig(), keyInfo.isFile());
})
+ .filter(key -> StringUtils.startsWith(key.getName(), getKeyPrefix()))
.collect(Collectors.toList());
}
@@ -1354,63 +1365,14 @@ public class OzoneBucket extends WithMetadata {
stack = new Stack();
}
+ if (shallow()) {
+ return getNextShallowListOfKeys(prevKey);
+ }
+
// normalize paths
if (!addedKeyPrefix()) {
- prevKey = OmUtils.normalizeKey(prevKey, true);
- String keyPrefixName = "";
- if (StringUtils.isNotBlank(getKeyPrefix())) {
- keyPrefixName = OmUtils.normalizeKey(getKeyPrefix(), true);
- }
- setKeyPrefix(keyPrefixName);
-
- if (StringUtils.isNotBlank(prevKey)) {
- if (StringUtils.startsWith(prevKey, getKeyPrefix())) {
- // 1. Prepare all the seekKeys after the prefixKey.
- // Example case: prefixKey="a1", startKey="a1/b2/d2/f3/f31.tx"
- // Now, stack should be build with all the levels after prefixKey
- // Stack format => <keyPrefix and startKey>, startKey should be an
- // immediate child of keyPrefix.
- // _______________________________________
- // Stack=> top | < a1/b2/d2/f3, a1/b2/d2/f3/f31.tx > |
- // |-------------------------------------|
- // | < a1/b2/d2, a1/b2/d2/f3 > |
- // |-------------------------------------|
- // | < a1/b2, a1/b2/d2 > |
- // |-------------------------------------|
- // bottom | < a1, a1/b2 > |
- // --------------------------------------|
- List<Pair<String, String>> seekPaths = new ArrayList<>();
-
- if (StringUtils.isNotBlank(getKeyPrefix())) {
- // If the prev key is a dir then seek its sub-paths
- // Say, prevKey="a1/b2/d2"
- addPrevDirectoryToSeekPath(prevKey, seekPaths);
- }
-
- // Key Prefix is Blank. The seek all the keys with startKey.
- removeStartKey = prevKey;
- getSeekPathsBetweenKeyPrefixAndStartKey(getKeyPrefix(), prevKey,
- seekPaths);
-
- // 2. Push elements in reverse order so that the FS tree traversal
- // will occur in left-to-right fashion[Depth-First Search]
- for (int index = seekPaths.size() - 1; index >= 0; index--) {
- Pair<String, String> seekDirPath = seekPaths.get(index);
- stack.push(seekDirPath);
- }
- } else if (StringUtils.isNotBlank(getKeyPrefix())) {
- if (!OzoneFSUtils.isAncestorPath(getKeyPrefix(), prevKey)) {
- // Case-1 - sibling: keyPrefix="a1/b2", startKey="a0/b123Invalid"
- // Skip traversing, if the startKey is not a sibling.
- // "a1/b", "a1/b1/e/"
- return new ArrayList<>();
- } else if (StringUtils.compare(prevKey, getKeyPrefix()) < 0) {
- // Case-2 - compare: keyPrefix="a1/b2", startKey="a1/b123Invalid"
- // Since startKey is lexographically behind keyPrefix,
- // the seek precedence goes to keyPrefix.
- stack.push(new ImmutablePair<>(getKeyPrefix(), ""));
- }
- }
+ if (!prepareStack(prevKey)) {
+ return new ArrayList<>();
}
}
@@ -1437,6 +1399,169 @@ public class OzoneBucket extends WithMetadata {
return keysResultList;
}
+ @Override
+ List<OzoneKey> getNextShallowListOfKeys(String prevKey)
+ throws IOException {
+ List<OzoneKey> resultList = new ArrayList<>();
+ String startKey = prevKey;
+ boolean findFirstStartKey = false;
+
+ if (!addedKeyPrefix()) {
+ initDelimiterKeyPrefix();
+
+ if (!prepareStack(prevKey)) {
+ return new ArrayList<>();
+ }
+
+ // 1. Get first element as startKey.
+ List<OzoneKey> firstKeyResult = new ArrayList<>();
+ if (stack.isEmpty()) {
+ // Case: startKey is empty
+ getChildrenKeys(getKeyPrefix(), prevKey, firstKeyResult);
+ } else {
+ // Case: startKey is non-empty
+ while (!stack.isEmpty()) {
+ Pair<String, String> keyPrefixPath = stack.pop();
+ getChildrenKeys(keyPrefixPath.getLeft(), keyPrefixPath.getRight(),
+ firstKeyResult);
+ if (!firstKeyResult.isEmpty()) {
+ break;
+ }
+ }
+ }
+ if (!firstKeyResult.isEmpty()) {
+ startKey = firstKeyResult.get(0).getName();
+ findFirstStartKey = true;
+ }
+
+ // A specific case where findFirstStartKey is false does not mean that
+ // the final result is empty because also need to determine whether
+ // keyPrefix is an existing key. Consider the following structure:
+ // te/
+ // test1/
+ // test1/file1
+ // test1/file2
+ // test2/
+ // when keyPrefix='te' and prevKey='test1/file2', findFirstStartKey
+ // will be false because 'test1/file2' is the last key in dir
+ // 'test1/'. In the correct result for this case, 'test2/' is expected
+ // in the results and "test1/" should be excluded.
+ //
+ if (!findFirstStartKey) {
+ if (StringUtils.isBlank(prevKey) || !keyPrefixExist()
+ || !StringUtils.startsWith(prevKey, getKeyPrefix())) {
+ return new ArrayList<>();
+ }
+ }
+ // A special case where keyPrefix element should present in the
+ // resultList. See the annotation of #addKeyPrefixInfoToResultList
+ if (getKeyPrefix().equals(startKey) && findFirstStartKey
+ && !firstKeyResult.get(0).isFile()) {
+ resultList.add(firstKeyResult.get(0));
+ }
+ // Note that the startKey needs to be an immediate child of the
+ // keyPrefix or black before calling listStatus.
+ startKey = adjustStartKey(startKey);
+ }
+
+ // 2. Get immediate children by listStatus method.
+ List<OzoneFileStatusLight> statuses =
+ proxy.listStatusLight(volumeName, name, getDelimiterKeyPrefix(),
+ false, startKey, listCacheSize, false);
+
+ if (!statuses.isEmpty()) {
+ // If findFirstStartKey is false, indicates that the keyPrefix is an
+ // existing key and the prevKey is the last element within its
+ // directory. In this case, the result should not include the
+ // startKey itself.
+ if (!findFirstStartKey && addedKeyPrefix()) {
+ statuses.remove(0);
+ }
+ List<OzoneKey> ozoneKeys = buildKeysWithKeyPrefix(statuses);
+ resultList.addAll(ozoneKeys);
+ }
+ return resultList;
+ }
+
+ private boolean prepareStack(String prevKey) throws IOException {
+ prevKey = OmUtils.normalizeKey(prevKey, true);
+ String keyPrefixName = "";
+ if (StringUtils.isNotBlank(getKeyPrefix())) {
+ keyPrefixName = OmUtils.normalizeKey(getKeyPrefix(), true);
+ }
+ setKeyPrefix(keyPrefixName);
+
+ if (StringUtils.isNotBlank(prevKey)) {
+ if (StringUtils.startsWith(prevKey, getKeyPrefix())) {
+ // 1. Prepare all the seekKeys after the prefixKey.
+ // Example case: prefixKey="a1", startKey="a1/b2/d2/f3/f31.tx"
+ // Now, stack should be build with all the levels after prefixKey
+ // Stack format => <keyPrefix and startKey>, startKey should be an
+ // immediate child of keyPrefix.
+ // _______________________________________
+ // Stack=> top | < a1/b2/d2/f3, a1/b2/d2/f3/f31.tx > |
+ // |-------------------------------------|
+ // | < a1/b2/d2, a1/b2/d2/f3 > |
+ // |-------------------------------------|
+ // | < a1/b2, a1/b2/d2 > |
+ // |-------------------------------------|
+ // bottom | < a1, a1/b2 > |
+ // --------------------------------------|
+ List<Pair<String, String>> seekPaths = new ArrayList<>();
+
+ if (StringUtils.isNotBlank(getKeyPrefix())) {
+ // If the prev key is a dir then seek its sub-paths
+ // Say, prevKey="a1/b2/d2"
+ addPrevDirectoryToSeekPath(prevKey, seekPaths);
+ }
+
+ // Key Prefix is Blank. The seek all the keys with startKey.
+ removeStartKey = prevKey;
+ getSeekPathsBetweenKeyPrefixAndStartKey(getKeyPrefix(), prevKey,
+ seekPaths);
+
+ // 2. Push elements in reverse order so that the FS tree traversal
+ // will occur in left-to-right fashion[Depth-First Search]
+ for (int index = seekPaths.size() - 1; index >= 0; index--) {
+ Pair<String, String> seekDirPath = seekPaths.get(index);
+ stack.push(seekDirPath);
+ }
+ } else if (StringUtils.isNotBlank(getKeyPrefix())) {
+ if (!OzoneFSUtils.isAncestorPath(getKeyPrefix(), prevKey)) {
+ // Case-1 - sibling: keyPrefix="a1/b2", startKey="a0/b123Invalid"
+ // Skip traversing, if the startKey is not a sibling.
+ // "a1/b", "a1/b1/e/"
+ return false;
+ } else if (StringUtils.compare(prevKey, getKeyPrefix()) < 0) {
+ // Case-2 - compare: keyPrefix="a1/b2", startKey="a1/b123Invalid"
+ // Since startKey is lexographically behind keyPrefix,
+ // the seek precedence goes to keyPrefix.
+ stack.push(new ImmutablePair<>(getKeyPrefix(), ""));
+ }
+ }
+ }
+ return true;
+ }
+
+ private String adjustStartKey(String startKey) {
+ if (getKeyPrefix().endsWith(OZONE_URI_DELIMITER) &&
+ getKeyPrefix().equals(startKey)) {
+ return "";
+ }
+ return OzoneFSUtils.getImmediateChild(startKey, getDelimiterKeyPrefix());
+ }
+
+ private boolean keyPrefixExist() throws IOException {
+ OzoneFileStatus keyPrefixStatus = null;
+ try {
+ keyPrefixStatus =
+ proxy.getOzoneFileStatus(volumeName, name, getKeyPrefix());
+ } catch (OMException ome) {
+ // ignore exception
+ }
+ return keyPrefixStatus != null;
+ }
+
private void addPrevDirectoryToSeekPath(String prevKey,
List<Pair<String, String>> seekPaths)
throws IOException {
diff --git
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestListKeysWithFSO.java
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestListKeysWithFSO.java
index ef6412cd9a..5174fe5af7 100644
---
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestListKeysWithFSO.java
+++
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestListKeysWithFSO.java
@@ -412,6 +412,71 @@ public class TestListKeysWithFSO {
checkKeyList("a", "a1", expectedKeys, fsoOzoneBucket2);
}
+ @Test
+ public void testShallowListKeys() throws Exception {
+ List<String> expectedKeys;
+
+ // case-1: startKey not reaches last key
+ String keyPrefix = "a1/b1/";
+ String startKey = "a1/b1/c12/c2.tx";
+ // a1/b1/c12/
+ // a1/b1/c1222.tx
+ // a1/b1/c1333.tx
+ // a1/b1/c1444.tx
+ // a1/b1/c1555.tx
+ expectedKeys =
+ getExpectedKeyShallowList(keyPrefix, startKey, legacyOzoneBucket);
+ checkKeyShallowList(keyPrefix, startKey, expectedKeys, fsoOzoneBucket);
+
+ // case-2: startKey reaches last key
+ keyPrefix = "a1/b1/";
+ startKey = "a1/b1/c12/c3.tx";
+ // a1/b1/c1222.tx
+ // a1/b1/c1333.tx
+ // a1/b1/c1444.tx
+ // a1/b1/c1555.tx
+ expectedKeys =
+ getExpectedKeyShallowList(keyPrefix, startKey, legacyOzoneBucket);
+ checkKeyShallowList(keyPrefix, startKey, expectedKeys, fsoOzoneBucket);
+
+ // case-3: keyPrefix non-exist and startKey not reaches last key
+ keyPrefix = "a1/b";
+ startKey = "a1/b1/c1444.tx";
+ // a1/b1/
+ // a1/b2/
+ // a1/b3/
+ expectedKeys =
+ getExpectedKeyShallowList(keyPrefix, startKey, legacyOzoneBucket);
+ checkKeyShallowList(keyPrefix, startKey, expectedKeys, fsoOzoneBucket);
+
+ // case-4: keyPrefix non-exist and startKey reaches last key
+ keyPrefix = "a1/b";
+ startKey = "a1/b1/c1555.tx";
+ // a1/b2/
+ // a1/b3/
+ expectedKeys =
+ getExpectedKeyShallowList(keyPrefix, startKey, legacyOzoneBucket);
+ checkKeyShallowList(keyPrefix, startKey, expectedKeys, fsoOzoneBucket);
+
+ // case-5: keyPrefix corresponds to multiple existing keys.
+ keyPrefix = "a1/b1/c12";
+ startKey = "";
+ // a1/b1/c12/
+ // a1/b1/c1222.tx
+ expectedKeys =
+ getExpectedKeyShallowList(keyPrefix, startKey, legacyOzoneBucket);
+ checkKeyShallowList(keyPrefix, startKey, expectedKeys, fsoOzoneBucket);
+
+ // case-6: keyPrefix corresponds to multiple existing keys and
+ // startKey reaches last key
+ keyPrefix = "a1/b1/c12";
+ startKey = "a1/b1/c12/c3.tx";
+ // a1/b1/c1222.tx
+ expectedKeys =
+ getExpectedKeyShallowList(keyPrefix, startKey, legacyOzoneBucket);
+ checkKeyShallowList(keyPrefix, startKey, expectedKeys, fsoOzoneBucket);
+ }
+
/**
* Verify listKeys at different levels.
*
@@ -485,9 +550,10 @@ public class TestListKeysWithFSO {
private static List<String> getExpectedKeyList(String keyPrefix,
- String startKey, OzoneBucket legacyBucket) throws Exception {
+ String startKey, OzoneBucket legacyBucket, boolean shallow)
+ throws Exception {
Iterator<? extends OzoneKey> ozoneKeyIterator =
- legacyBucket.listKeys(keyPrefix, startKey);
+ legacyBucket.listKeys(keyPrefix, startKey, shallow);
List<String> keys = new LinkedList<>();
while (ozoneKeyIterator.hasNext()) {
@@ -497,11 +563,23 @@ public class TestListKeysWithFSO {
return keys;
}
+ private static List<String> getExpectedKeyList(String keyPrefix,
+ String startKey, OzoneBucket legacyBucket)
+ throws Exception {
+ return getExpectedKeyList(keyPrefix, startKey, legacyBucket, false);
+ }
+
+ private static List<String> getExpectedKeyShallowList(String keyPrefix,
+ String startKey, OzoneBucket legacyBucket) throws Exception {
+ return getExpectedKeyList(keyPrefix, startKey, legacyBucket, true);
+ }
+
private void checkKeyList(String keyPrefix, String startKey,
- List<String> keys, OzoneBucket fsoBucket) throws Exception {
+ List<String> keys, OzoneBucket fsoBucket, boolean shallow)
+ throws Exception {
Iterator<? extends OzoneKey> ozoneKeyIterator =
- fsoBucket.listKeys(keyPrefix, startKey);
+ fsoBucket.listKeys(keyPrefix, startKey, shallow);
ReplicationConfig expectedReplication =
Optional.ofNullable(fsoBucket.getReplicationConfig())
.orElse(cluster.getOzoneManager().getDefaultReplicationConfig());
@@ -523,6 +601,16 @@ public class TestListKeysWithFSO {
Assert.assertEquals(keys, outputKeysList);
}
+ private void checkKeyList(String keyPrefix, String startKey,
+ List<String> keys, OzoneBucket fsoBucket) throws Exception {
+ checkKeyList(keyPrefix, startKey, keys, fsoBucket, false);
+ }
+
+ private void checkKeyShallowList(String keyPrefix, String startKey,
+ List<String> keys, OzoneBucket fsoBucket) throws Exception {
+ checkKeyList(keyPrefix, startKey, keys, fsoBucket, true);
+ }
+
private static void createKeys(OzoneBucket ozoneBucket, List<String> keys)
throws Exception {
int length = 10;
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]