This is an automated email from the ASF dual-hosted git repository.

deardeng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 818d3f63da9 [improvement](fe) Share CloudReplica route identifiers 
(#68783)
818d3f63da9 is described below

commit 818d3f63da90fc12828249e8b8af239df30aa7f3
Author: deardeng <[email protected]>
AuthorDate: Fri Oct 9 10:33:46 2026 +0800

    [improvement](fe) Share CloudReplica route identifiers (#68783)
    
    Related PR: #67636
    Problem Summary: CloudReplica image deserialization created independent
    String cluster IDs and boxed Long backend IDs for equivalent routes. The
    tested production image contains 4,197,906 replicas and 62,708,955
    routes but only 15 cluster IDs and about 85 backend IDs, so millions of
    equal route identifiers retained duplicate heap objects. Canonical
    sharing reduced forced-GC live heap from 15,311,045,744 bytes to
    8,539,550,968 bytes, saving 6,771,494,776 bytes (6.31 GiB, 44.2%).
    
    Intern cluster IDs and primary backend IDs on route updates and after
    image deserialization so equal routes reuse the same objects. Use Guava
    weak interners instead of strong process-lifetime maps: active routes
    keep canonical objects alive, while identifiers become collectable after
    their last route is removed. Routing behavior and the serialized image
    format are unchanged.
    
    The full 6.93 GB image completed compatibility load and dump with the
    final weak-interner implementation. A clean run loaded in 225.679
    seconds and dumped in 145.935 seconds. JFR attributed 255 of 18,139
    load-stage CPU samples (1.41%) to the interner path, with no sampled
    allocations attributed directly to it.
    
    ### Release note
    
    Reduce FE heap usage for CloudReplica routing metadata and release
    unused historical route identifiers.
    
    - Test: Unit Test / Manual test
        - CloudReplicaTest: 23 tests passed
        - ./build.sh --fe passed with Checkstyle 0 violations
        - Full production image compatibility load and dump passed
        - JFR profile verified the interner path is not a load hotspot
---
 .../apache/doris/cloud/catalog/CloudReplica.java   | 23 ++++++++-
 .../doris/cloud/catalog/CloudReplicaTest.java      | 57 ++++++++++++++++++++++
 2 files changed, 78 insertions(+), 2 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudReplica.java 
b/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudReplica.java
index b7ae1308ed5..b02120903db 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudReplica.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudReplica.java
@@ -31,6 +31,8 @@ import org.apache.doris.system.Backend;
 import org.apache.doris.system.SystemInfoService;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.Interner;
+import com.google.common.collect.Interners;
 import com.google.common.hash.HashCode;
 import com.google.common.hash.Hashing;
 import com.google.gson.annotations.SerializedName;
@@ -52,6 +54,10 @@ import java.util.stream.Collectors;
 public class CloudReplica extends Replica implements GsonPostProcessable {
     private static final Logger LOG = LogManager.getLogger(CloudReplica.class);
 
+    // Routes keep shared objects alive; weak interners release IDs after 
their last route disappears.
+    private static final Interner<String> SHARED_CLUSTER_IDS = 
Interners.newWeakInterner();
+    private static final Interner<Long> SHARED_BACKEND_IDS = 
Interners.newWeakInterner();
+
     // a replica is mapped to one BE in a cluster, use primaryClusterToBackend 
instead of primaryClusterToBackends
     @Deprecated
     @SerializedName(value = "bes")
@@ -599,8 +605,16 @@ public class CloudReplica extends Replica implements 
GsonPostProcessable {
         return idx;
     }
 
+    private static String shareClusterId(String clusterId) {
+        return SHARED_CLUSTER_IDS.intern(clusterId);
+    }
+
+    private static Long shareBackendId(Long backendId) {
+        return SHARED_BACKEND_IDS.intern(backendId);
+    }
+
     public void updateClusterToPrimaryBe(String cluster, long beId) {
-        primaryClusterToBackend.put(cluster, beId);
+        primaryClusterToBackend.put(shareClusterId(cluster), 
shareBackendId(beId));
         secondaryClusterToBackends.remove(cluster);
     }
 
@@ -615,7 +629,7 @@ public class CloudReplica extends Replica implements 
GsonPostProcessable {
             LOG.debug("add to secondary clusterId {}, beId {}, changeTimestamp 
{}, replica info {}",
                     cluster, beId, changeTimestamp, this);
         }
-        secondaryClusterToBackends.put(cluster, Pair.of(beId, 
changeTimestamp));
+        secondaryClusterToBackends.put(shareClusterId(cluster), Pair.of(beId, 
changeTimestamp));
     }
 
     public void clearClusterToBe(String cluster) {
@@ -745,5 +759,10 @@ public class CloudReplica extends Replica implements 
GsonPostProcessable {
         // checkpoint thread resolves Env.getCurrentEnv() to its own Env, so 
the backend set read here is
         // the one belonging to the image being loaded.
         removeInvalidRoutes();
+
+        ConcurrentHashMap<String, Long> sharedRoutes = new 
ConcurrentHashMap<>(primaryClusterToBackend.size());
+        primaryClusterToBackend.forEach((clusterId, backendId) ->
+                sharedRoutes.put(shareClusterId(clusterId), 
shareBackendId(backendId)));
+        primaryClusterToBackend = sharedRoutes;
     }
 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudReplicaTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudReplicaTest.java
index 8b04976a490..cc2c05c3951 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudReplicaTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudReplicaTest.java
@@ -28,6 +28,7 @@ import org.apache.doris.common.FeConstants;
 import org.apache.doris.persist.gson.GsonUtils;
 import org.apache.doris.system.Backend;
 
+import com.google.common.testing.GcFinalization;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonObject;
 import org.junit.jupiter.api.AfterEach;
@@ -37,6 +38,7 @@ import org.junit.jupiter.api.Test;
 import org.mockito.MockedStatic;
 import org.mockito.Mockito;
 
+import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -548,6 +550,61 @@ public class CloudReplicaTest {
         return (CloudReplica) GsonUtils.GSON.fromJson(json, Replica.class);
     }
 
+    private void assertPrimaryRouteObjectsShared(CloudReplica first, 
CloudReplica second) {
+        
Assertions.assertSame(first.getPrimaryComputeGroupIds().iterator().next(),
+                second.getPrimaryComputeGroupIds().iterator().next());
+        
Assertions.assertSame(first.getNonColocatedPrimaryBackendId(CLUSTER_ID_1),
+                second.getNonColocatedPrimaryBackendId(CLUSTER_ID_1));
+    }
+
+    @Test
+    public void testRouteObjectsSharedOnUpdate() {
+        CloudReplica first = createReplica();
+        CloudReplica second = createReplica();
+
+        first.updateClusterToPrimaryBe(new String(CLUSTER_ID_1), 1001L);
+        second.updateClusterToPrimaryBe(new String(CLUSTER_ID_1), 1001L);
+
+        assertPrimaryRouteObjectsShared(first, second);
+    }
+
+    @Test
+    public void testUnusedRouteObjectsCanBeCollected() {
+        List<WeakReference<?>> references = createAndClearRouteReferences();
+
+        references.forEach(GcFinalization::awaitClear);
+    }
+
+    private List<WeakReference<?>> createAndClearRouteReferences() {
+        String clusterId = new String("collectable_cluster_id");
+        long backendId = 9_000_000_000_001L;
+        CloudReplica replica = createReplica();
+        replica.updateClusterToPrimaryBe(clusterId, backendId);
+        WeakReference<String> clusterReference = new WeakReference<>(
+                replica.getPrimaryComputeGroupIds().iterator().next());
+        WeakReference<Long> backendReference = new WeakReference<>(
+                replica.getNonColocatedPrimaryBackendId(clusterId));
+
+        replica.clearClusterToBe(clusterId);
+        return Arrays.asList(clusterReference, backendReference);
+    }
+
+    @Test
+    public void testRouteObjectsSharedOnImageLoad() {
+        stubBackend(1001L, createBackend(1001L, true, false));
+        CloudReplica replica = createReplica();
+        replica.updateClusterToPrimaryBe(CLUSTER_ID_1, 1001L);
+        JsonObject originalJson = GsonUtils.GSON.toJsonTree(replica, 
Replica.class).getAsJsonObject();
+
+        CloudReplica first = gsonRoundTrip(replica, false);
+        CloudReplica second = gsonRoundTrip(replica, false);
+        assertPrimaryRouteObjectsShared(first, second);
+        Assertions.assertEquals(originalJson,
+                GsonUtils.GSON.toJsonTree(first, 
Replica.class).getAsJsonObject());
+
+        assertPrimaryRouteObjectsShared(gsonRoundTrip(replica, true), 
gsonRoundTrip(replica, true));
+    }
+
     @Test
     public void testRemoveInvalidRoutes_onImageLoad() {
         boolean savedClean = Config.enable_cloud_replica_stale_route_clean;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to