Skip to content

Commit 69b1df1

Browse files
authored
HDDS-15335. Recon: parallelize NSSummaryTask sub-tasks and cache OmBucketInfo lookups (#10321)
1 parent 459f353 commit 69b1df1

5 files changed

Lines changed: 563 additions & 52 deletions

File tree

hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/tasks/NSSummaryTask.java

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,16 @@ public class NSSummaryTask implements ReconOmTask {
8383
private final NSSummaryTaskWithLegacy nsSummaryTaskWithLegacy;
8484
private final NSSummaryTaskWithOBS nsSummaryTaskWithOBS;
8585

86+
// Shared executor for the three FSO/Legacy/OBS sub-tasks during process().
87+
// The sub-tasks operate on disjoint slices of the event stream (filtered by
88+
// table and bucket layout) and write to disjoint NSSummary entries, so they
89+
// are safe to run in parallel.
90+
private static final ExecutorService SUB_TASK_EXECUTOR =
91+
Executors.newFixedThreadPool(3, new ThreadFactoryBuilder()
92+
.setNameFormat("NSSummarySubTask-%d")
93+
.setDaemon(true)
94+
.build());
95+
8696
/**
8797
* Rebuild state enum to track NSSummary tree rebuild status.
8898
*/
@@ -172,44 +182,58 @@ public String getDescription() {
172182
@Override
173183
public TaskResult process(
174184
OMUpdateEventBatch events, Map<String, Integer> subTaskSeekPosMap) {
175-
boolean anyFailure = false; // Track if any bucket fails
176185
Map<String, Integer> updatedSeekPositions = new HashMap<>();
177186

178-
// Process FSO bucket
179-
Integer bucketSeek = subTaskSeekPosMap.getOrDefault(BucketType.FSO.name(), 0);
180-
Pair<Integer, Boolean> bucketResult = nsSummaryTaskWithFSO.processWithFSO(events, bucketSeek);
181-
updatedSeekPositions.put(BucketType.FSO.name(), bucketResult.getLeft());
182-
if (!bucketResult.getRight()) {
183-
LOG.error("processWithFSO failed.");
184-
anyFailure = true;
185-
}
186-
187-
// Process Legacy bucket
188-
bucketSeek = subTaskSeekPosMap.getOrDefault(BucketType.LEGACY.name(), 0);
189-
bucketResult = nsSummaryTaskWithLegacy.processWithLegacy(events, bucketSeek);
190-
updatedSeekPositions.put(BucketType.LEGACY.name(), bucketResult.getLeft());
191-
if (!bucketResult.getRight()) {
192-
LOG.error("processWithLegacy failed.");
193-
anyFailure = true;
194-
}
195-
196-
// Process OBS bucket
197-
bucketSeek = subTaskSeekPosMap.getOrDefault(BucketType.OBS.name(), 0);
198-
bucketResult = nsSummaryTaskWithOBS.processWithOBS(events, bucketSeek);
199-
updatedSeekPositions.put(BucketType.OBS.name(), bucketResult.getLeft());
200-
if (!bucketResult.getRight()) {
201-
LOG.error("processWithOBS failed.");
202-
anyFailure = true;
203-
}
187+
int fsoSeek = subTaskSeekPosMap.getOrDefault(BucketType.FSO.name(), 0);
188+
int legacySeek = subTaskSeekPosMap.getOrDefault(BucketType.LEGACY.name(), 0);
189+
int obsSeek = subTaskSeekPosMap.getOrDefault(BucketType.OBS.name(), 0);
190+
191+
Future<Pair<Integer, Boolean>> fsoFuture = SUB_TASK_EXECUTOR.submit(
192+
() -> nsSummaryTaskWithFSO.processWithFSO(events, fsoSeek));
193+
Future<Pair<Integer, Boolean>> legacyFuture = SUB_TASK_EXECUTOR.submit(
194+
() -> nsSummaryTaskWithLegacy.processWithLegacy(events, legacySeek));
195+
Future<Pair<Integer, Boolean>> obsFuture = SUB_TASK_EXECUTOR.submit(
196+
() -> nsSummaryTaskWithOBS.processWithOBS(events, obsSeek));
197+
198+
boolean anyFailure = false;
199+
anyFailure |= !awaitSubTask("processWithFSO", BucketType.FSO,
200+
fsoFuture, fsoSeek, updatedSeekPositions);
201+
anyFailure |= !awaitSubTask("processWithLegacy", BucketType.LEGACY,
202+
legacyFuture, legacySeek, updatedSeekPositions);
203+
anyFailure |= !awaitSubTask("processWithOBS", BucketType.OBS,
204+
obsFuture, obsSeek, updatedSeekPositions);
204205

205-
// Return task failure if any bucket failed, while keeping each bucket's latest seek position
206206
return new TaskResult.Builder()
207207
.setTaskName(getTaskName())
208208
.setSubTaskSeekPositions(updatedSeekPositions)
209209
.setTaskSuccess(!anyFailure)
210210
.build();
211211
}
212212

213+
private boolean awaitSubTask(String name, BucketType type,
214+
Future<Pair<Integer, Boolean>> future,
215+
int fallbackSeek,
216+
Map<String, Integer> updatedSeekPositions) {
217+
try {
218+
Pair<Integer, Boolean> result = future.get();
219+
updatedSeekPositions.put(type.name(), result.getLeft());
220+
if (!result.getRight()) {
221+
LOG.error("{} failed.", name);
222+
return false;
223+
}
224+
return true;
225+
} catch (InterruptedException e) {
226+
Thread.currentThread().interrupt();
227+
LOG.error("{} interrupted.", name, e);
228+
updatedSeekPositions.put(type.name(), fallbackSeek);
229+
return false;
230+
} catch (ExecutionException e) {
231+
LOG.error("{} threw an exception.", name, e.getCause());
232+
updatedSeekPositions.put(type.name(), fallbackSeek);
233+
return false;
234+
}
235+
}
236+
213237
@Override
214238
public TaskResult reprocess(OMMetadataManager omMetadataManager) {
215239
// Unified control for all NSS tree rebuild operations

hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/tasks/NSSummaryTaskDbEventHandler.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
import java.io.IOException;
2121
import java.util.Collection;
2222
import java.util.Collections;
23+
import java.util.HashMap;
2324
import java.util.Map;
2425
import org.apache.hadoop.hdds.utils.db.RDBBatchOperation;
26+
import org.apache.hadoop.ozone.om.helpers.OmBucketInfo;
2527
import org.apache.hadoop.ozone.om.helpers.OmDirectoryInfo;
2628
import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
2729
import org.apache.hadoop.ozone.recon.ReconUtils;
@@ -43,6 +45,18 @@ public class NSSummaryTaskDbEventHandler {
4345
private ReconNamespaceSummaryManager reconNamespaceSummaryManager;
4446
private ReconOMMetadataManager reconOMMetadataManager;
4547

48+
// Cache OmBucketInfo lookups across process() calls so the Legacy and OBS
49+
// sub-tasks don't pay a RocksDB point read per event. A bucket's objectID and
50+
// layout are stable while the bucket exists, but a bucket can be deleted and
51+
// recreated under the same volume/bucket name with a new objectID (same DB
52+
// key, different identity). A recreate is always preceded by a delete, so the
53+
// sub-tasks call invalidateBucketCache() when they observe a bucketTable
54+
// delete event; a recreated bucket is then re-read instead of served stale.
55+
//
56+
// Single-thread access only (each sub-task runs on its own thread and owns
57+
// its own cache instance). HashMap is fine.
58+
private final Map<String, OmBucketInfo> bucketInfoCache = new HashMap<>();
59+
4660
public NSSummaryTaskDbEventHandler(ReconNamespaceSummaryManager
4761
reconNamespaceSummaryManager,
4862
ReconOMMetadataManager
@@ -51,6 +65,34 @@ public NSSummaryTaskDbEventHandler(ReconNamespaceSummaryManager
5165
this.reconOMMetadataManager = reconOMMetadataManager;
5266
}
5367

68+
/** Look up an {@link OmBucketInfo} via {@code getBucketTable().getSkipCache}
69+
* and cache the result. Bucket layout/object-id are stable while a bucket
70+
* exists, so a field-level cache avoids one RocksDB point read per event in
71+
* the per-event sub-task loops. Entries are dropped via
72+
* {@link #invalidateBucketCache(String)} when a bucketTable delete event is
73+
* seen, so a bucket deleted and recreated under the same name is not served
74+
* stale. */
75+
protected OmBucketInfo lookupBucketCached(String bucketDBKey) throws IOException {
76+
OmBucketInfo cached = bucketInfoCache.get(bucketDBKey);
77+
if (cached != null) {
78+
return cached;
79+
}
80+
OmBucketInfo info = reconOMMetadataManager.getBucketTable().getSkipCache(bucketDBKey);
81+
if (info != null) {
82+
bucketInfoCache.put(bucketDBKey, info);
83+
}
84+
return info;
85+
}
86+
87+
/** Drop the cached {@link OmBucketInfo} for the given bucket DB key. Invoked
88+
* when a bucketTable delete event is observed so the next key event re-reads
89+
* the current bucket info. This matters when a bucket is deleted and
90+
* recreated under the same volume/bucket name, which assigns a new objectID;
91+
* the recreate always follows the delete, so invalidating on delete suffices. */
92+
protected void invalidateBucketCache(String bucketDBKey) {
93+
bucketInfoCache.remove(bucketDBKey);
94+
}
95+
5496
public ReconNamespaceSummaryManager getReconNamespaceSummaryManager() {
5597
return reconNamespaceSummaryManager;
5698
}

hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/tasks/NSSummaryTaskWithLegacy.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package org.apache.hadoop.ozone.recon.tasks;
1919

2020
import static org.apache.hadoop.ozone.OzoneConsts.OM_KEY_PREFIX;
21+
import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.BUCKET_TABLE;
2122
import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.KEY_TABLE;
2223

2324
import java.io.IOException;
@@ -91,9 +92,20 @@ public Pair<Integer, Boolean> processWithLegacy(OMUpdateEventBatch events,
9192
OMDBUpdateEvent.OMDBUpdateAction action = omdbUpdateEvent.getAction();
9293
eventCounter++;
9394

94-
// we only process updates on OM's KeyTable
9595
String table = omdbUpdateEvent.getTable();
96+
// A bucket can be deleted and recreated under the same name with a new
97+
// objectID. A recreate is always preceded by a delete, so dropping the
98+
// cached OmBucketInfo on the delete event is enough for a later key event
99+
// to re-read the recreated bucket. Bucket property updates don't change
100+
// objectID or layout, so they need not invalidate the cache.
101+
if (table.equals(BUCKET_TABLE)) {
102+
if (action == OMDBUpdateEvent.OMDBUpdateAction.DELETE) {
103+
invalidateBucketCache(omdbUpdateEvent.getKey());
104+
}
105+
continue;
106+
}
96107

108+
// we only process updates on OM's KeyTable
97109
if (!table.equals(KEY_TABLE)) {
98110
continue;
99111
}
@@ -363,8 +375,7 @@ private long setParentBucketId(OmKeyInfo keyInfo)
363375
throws IOException {
364376
String bucketKey = getReconOMMetadataManager()
365377
.getBucketKey(keyInfo.getVolumeName(), keyInfo.getBucketName());
366-
OmBucketInfo parentBucketInfo =
367-
getReconOMMetadataManager().getBucketTable().getSkipCache(bucketKey);
378+
OmBucketInfo parentBucketInfo = lookupBucketCached(bucketKey);
368379

369380
if (parentBucketInfo != null) {
370381
return parentBucketInfo.getObjectID();
@@ -388,8 +399,7 @@ private boolean isBucketLayoutValid(ReconOMMetadataManager metadataManager,
388399
String volumeName = keyInfo.getVolumeName();
389400
String bucketName = keyInfo.getBucketName();
390401
String bucketDBKey = metadataManager.getBucketKey(volumeName, bucketName);
391-
OmBucketInfo omBucketInfo =
392-
metadataManager.getBucketTable().getSkipCache(bucketDBKey);
402+
OmBucketInfo omBucketInfo = lookupBucketCached(bucketDBKey);
393403

394404
if (omBucketInfo.getBucketLayout() != LEGACY_BUCKET_LAYOUT) {
395405
LOG.debug(

hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/tasks/NSSummaryTaskWithOBS.java

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
package org.apache.hadoop.ozone.recon.tasks;
1919

20+
import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.BUCKET_TABLE;
2021
import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.KEY_TABLE;
2122

2223
import java.io.IOException;
@@ -201,10 +202,21 @@ public Pair<Integer, Boolean> processWithOBS(OMUpdateEventBatch events,
201202
OMDBUpdateEvent.OMDBUpdateAction action = omdbUpdateEvent.getAction();
202203
eventCounter++;
203204

204-
// We only process updates on OM's KeyTable
205205
String table = omdbUpdateEvent.getTable();
206-
boolean updateOnKeyTable = table.equals(KEY_TABLE);
207-
if (!updateOnKeyTable) {
206+
// A bucket can be deleted and recreated under the same name with a new
207+
// objectID. A recreate is always preceded by a delete, so dropping the
208+
// cached OmBucketInfo on the delete event is enough for a later key event
209+
// to re-read the recreated bucket. Bucket property updates don't change
210+
// objectID or layout, so they need not invalidate the cache.
211+
if (table.equals(BUCKET_TABLE)) {
212+
if (action == OMDBUpdateEvent.OMDBUpdateAction.DELETE) {
213+
invalidateBucketCache(omdbUpdateEvent.getKey());
214+
}
215+
continue;
216+
}
217+
218+
// We only process updates on OM's KeyTable
219+
if (!table.equals(KEY_TABLE)) {
208220
continue;
209221
}
210222

@@ -234,15 +246,13 @@ public Pair<Integer, Boolean> processWithOBS(OMUpdateEventBatch events,
234246
String bucketName = updatedKeyInfo.getBucketName();
235247
String bucketDBKey =
236248
getReconOMMetadataManager().getBucketKey(volumeName, bucketName);
237-
// Get bucket info from bucket table
238-
OmBucketInfo omBucketInfo = getReconOMMetadataManager().getBucketTable()
239-
.getSkipCache(bucketDBKey);
249+
OmBucketInfo omBucketInfo = lookupBucketCached(bucketDBKey);
240250

241251
if (omBucketInfo.getBucketLayout() != BUCKET_LAYOUT) {
242252
continue;
243253
}
244254

245-
long parentObjectID = getKeyParentID(updatedKeyInfo);
255+
long parentObjectID = omBucketInfo.getObjectID();
246256

247257
switch (action) {
248258
case PUT:
@@ -253,9 +263,10 @@ public Pair<Integer, Boolean> processWithOBS(OMUpdateEventBatch events,
253263
break;
254264
case UPDATE:
255265
if (oldKeyInfo != null) {
256-
// delete first, then put
257-
long oldKeyParentObjectID = getKeyParentID(oldKeyInfo);
258-
handleDeleteKeyEvent(oldKeyInfo, nsSummaryMap, oldKeyParentObjectID);
266+
// For OBS, parent is always the bucket, so same parentObjectID
267+
// applies to old and new (a key cannot move between buckets via
268+
// an UPDATE event — that would be a delete+put).
269+
handleDeleteKeyEvent(oldKeyInfo, nsSummaryMap, parentObjectID);
259270
} else {
260271
LOG.warn("Update event does not have the old keyInfo for {}.",
261272
updatedKey);

0 commit comments

Comments
 (0)