From b7086fa3d299c267eea10adac2a9f42b50feaf78 Mon Sep 17 00:00:00 2001 From: deardeng Date: Tue, 4 Aug 2026 10:18:47 +0800 Subject: [PATCH] [fix](fe) Avoid eager allocations in cloud tablet indexes (#66378) Problem Summary: Cloud tablet route-cache rebuilding calls fillBeToTablets for current and future placements. The previous putIfAbsent calls eagerly constructed up to six candidate maps and sets for every placement even when the keys already existed, and then performed separate lookups. Use the containers returned by computeIfAbsent at every level and add a focused unit test that verifies all six container paths while preserving the global, table, and partition index contents. An isomorphic multi-scale JVM allocation model with 2 million entities, four-route fan-out, current/future passes, and three index scopes estimated cumulative allocation per modeled rebuild at 12.52 GiB before and 8.59 GiB after the change, saving 3.93 GiB (31.4%). For 4 million tablets, the fill-path cumulative-allocation reduction is expected to remain about 31% under comparable topology, while the absolute GiB saving depends on cluster and replica fan-out. The retained graph remained about 3.76 GiB and the peak proxy about 966 MiB, so this change does not claim a comparable steady-state heap or process-wide reduction. Timing samples were noisy, so no throughput improvement is claimed. --- .../cloud/catalog/CloudTabletRebalancer.java | 21 +++--- .../catalog/CloudTabletRebalancerTest.java | 69 +++++++++++++++++++ 2 files changed, 78 insertions(+), 12 deletions(-) diff --git a/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudTabletRebalancer.java b/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudTabletRebalancer.java index 82d29f3d701f68..4c0832b35e94c8 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudTabletRebalancer.java +++ b/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudTabletRebalancer.java @@ -1034,22 +1034,19 @@ public void fillBeToTablets(long be, long tableId, long partId, long indexId, lo ConcurrentHashMap>>> partToTablets) { // global - globalBeToTablets.putIfAbsent(be, ConcurrentHashMap.newKeySet()); - globalBeToTablets.get(be).add(tabletId); + globalBeToTablets.computeIfAbsent(be, ignored -> ConcurrentHashMap.newKeySet()).add(tabletId); // table - beToTabletsInTable.putIfAbsent(tableId, new ConcurrentHashMap>()); - ConcurrentHashMap> beToTabletsOfTable = beToTabletsInTable.get(tableId); - beToTabletsOfTable.putIfAbsent(be, ConcurrentHashMap.newKeySet()); - beToTabletsOfTable.get(be).add(tabletId); + ConcurrentHashMap> beToTabletsOfTable = + beToTabletsInTable.computeIfAbsent(tableId, ignored -> new ConcurrentHashMap<>()); + beToTabletsOfTable.computeIfAbsent(be, ignored -> ConcurrentHashMap.newKeySet()).add(tabletId); // partition - partToTablets.putIfAbsent(partId, new ConcurrentHashMap>>()); - ConcurrentHashMap>> indexToTablets = partToTablets.get(partId); - indexToTablets.putIfAbsent(indexId, new ConcurrentHashMap>()); - ConcurrentHashMap> beToTabletsOfIndex = indexToTablets.get(indexId); - beToTabletsOfIndex.putIfAbsent(be, ConcurrentHashMap.newKeySet()); - beToTabletsOfIndex.get(be).add(tabletId); + ConcurrentHashMap>> indexToTablets = + partToTablets.computeIfAbsent(partId, ignored -> new ConcurrentHashMap<>()); + ConcurrentHashMap> beToTabletsOfIndex = + indexToTablets.computeIfAbsent(indexId, ignored -> new ConcurrentHashMap<>()); + beToTabletsOfIndex.computeIfAbsent(be, ignored -> ConcurrentHashMap.newKeySet()).add(tabletId); } private void enqueueWarmupTask(WarmupTabletTask task) { diff --git a/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudTabletRebalancerTest.java b/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudTabletRebalancerTest.java index c9dcd1ef9c92a5..a9abe1685ecc50 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudTabletRebalancerTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudTabletRebalancerTest.java @@ -41,6 +41,7 @@ import java.util.Random; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; public class CloudTabletRebalancerTest { @@ -75,6 +76,30 @@ protected boolean isInternalDbId(Long dbId) { } } + private static class CountingConcurrentHashMap extends ConcurrentHashMap { + private int computeIfAbsentCalls; + private int getCalls; + private int putIfAbsentCalls; + + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + computeIfAbsentCalls++; + return super.computeIfAbsent(key, mappingFunction); + } + + @Override + public V get(Object key) { + getCalls++; + return super.get(key); + } + + @Override + public V putIfAbsent(K key, V value) { + putIfAbsentCalls++; + return super.putIfAbsent(key, value); + } + } + private static void setField(Object obj, String name, Object value) throws Exception { Field f = CloudTabletRebalancer.class.getDeclaredField(name); f.setAccessible(true); @@ -94,6 +119,50 @@ private static T invokePrivate(Object obj, String method, Class[] types, return (T) m.invoke(obj, args); } + @Test + public void testFillBeToTabletsUsesComputedContainers() { + TestRebalancer rebalancer = new TestRebalancer(); + long beId = 1L; + long tableId = 2L; + long partitionId = 3L; + long indexId = 4L; + + CountingConcurrentHashMap> globalBeToTablets = new CountingConcurrentHashMap<>(); + CountingConcurrentHashMap>> beToTabletsInTable = + new CountingConcurrentHashMap<>(); + CountingConcurrentHashMap> beToTabletsOfTable = new CountingConcurrentHashMap<>(); + beToTabletsInTable.put(tableId, beToTabletsOfTable); + + CountingConcurrentHashMap>>> + partToTablets = new CountingConcurrentHashMap<>(); + CountingConcurrentHashMap>> indexToTablets = + new CountingConcurrentHashMap<>(); + CountingConcurrentHashMap> beToTabletsOfIndex = new CountingConcurrentHashMap<>(); + partToTablets.put(partitionId, indexToTablets); + indexToTablets.put(indexId, beToTabletsOfIndex); + + rebalancer.fillBeToTablets(beId, tableId, partitionId, indexId, 5L, + globalBeToTablets, beToTabletsInTable, partToTablets); + rebalancer.fillBeToTablets(beId, tableId, partitionId, indexId, 6L, + globalBeToTablets, beToTabletsInTable, partToTablets); + + assertComputedContainerUsed(globalBeToTablets); + assertComputedContainerUsed(beToTabletsInTable); + assertComputedContainerUsed(beToTabletsOfTable); + assertComputedContainerUsed(partToTablets); + assertComputedContainerUsed(indexToTablets); + assertComputedContainerUsed(beToTabletsOfIndex); + Assertions.assertEquals(Set.of(5L, 6L), globalBeToTablets.get(beId)); + Assertions.assertEquals(Set.of(5L, 6L), beToTabletsOfTable.get(beId)); + Assertions.assertEquals(Set.of(5L, 6L), beToTabletsOfIndex.get(beId)); + } + + private static void assertComputedContainerUsed(CountingConcurrentHashMap map) { + Assertions.assertEquals(2, map.computeIfAbsentCalls); + Assertions.assertEquals(0, map.putIfAbsentCalls); + Assertions.assertEquals(0, map.getCalls); + } + @Test public void testPickTabletPreferCold_picksColdWhenAvailable() throws Exception { TestRebalancer r = new TestRebalancer();