This is an automated email from the ASF dual-hosted git repository. yiguolei pushed a commit to branch branch-4.2 in repository https://gitbox.apache.org/repos/asf/doris.git
commit 0e7ab62c0131b43a12379c9590f9638c83d833b9 Author: deardeng <[email protected]> AuthorDate: Tue Oct 13 10:15:35 2026 +0800 [improvement](fe) Share CloudReplica route identifiers (#68783) (#68950) pick from https://github.com/apache/doris/pull/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. 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 (cherry picked from commit 818d3f63da90fc12828249e8b8af239df30aa7f3) ### What problem does this PR solve? Issue Number: close #xxx Related PR: #xxx Problem Summary: ### Release note None ### Check List (For Author) - Test <!-- At least one of them must be included. --> - [ ] Regression test - [ ] Unit Test - [ ] Manual test (add detailed scripts or steps below) - [ ] No need to test or manual test. Explain why: - [ ] This is a refactor/code format and no logic has been changed. - [ ] Previous test can cover this change. - [ ] No code files have been changed. - [ ] Other reason <!-- Add your reason? --> - Behavior changed: - [ ] No. - [ ] Yes. <!-- Explain the behavior change --> - Does this need documentation? - [ ] No. - [ ] Yes. <!-- Add document PR link here. eg: https://github.com/apache/doris-website/pull/1214 --> ### Check List (For Reviewer who merge this PR) - [ ] Confirm the release note - [ ] Confirm test cases - [ ] Confirm document - [ ] Add branch pick label <!-- Add branch pick label that this PR should merge into --> --- .../apache/doris/cloud/catalog/CloudReplica.java | 23 ++++++- .../doris/cloud/catalog/CloudReplicaTest.java | 80 ++++++++++++++++++++++ 2 files changed, 101 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 4ad4f24dc7c..4c51c65457e 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") @@ -600,8 +606,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); } @@ -616,7 +630,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) { @@ -744,5 +758,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 c1d7f396a70..6a2614c3994 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 @@ -25,8 +25,12 @@ import org.apache.doris.catalog.Replica; import org.apache.doris.cloud.qe.ComputeGroupException; import org.apache.doris.cloud.system.CloudSystemInfoService; import org.apache.doris.common.Config; +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; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -34,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; @@ -343,4 +348,79 @@ public class CloudReplicaTest { List<Backend> primaryBes = replica.getAllPrimaryBes(); Assertions.assertTrue(primaryBes.isEmpty()); } + + private void stubBackend(long id, Backend be) { + Mockito.when(mockInfoService.getBackend(id)).thenReturn(be); + Mockito.when(mockInfoService.getBackendByIdWithBoxedId(id)).thenReturn(be); + } + + private CloudReplica gsonRoundTrip(CloudReplica replica, boolean legacyBesFormat) { + JsonObject json = GsonUtils.GSON.toJsonTree(replica, Replica.class).getAsJsonObject(); + if (legacyBesFormat) { + JsonObject be = json.remove("be").getAsJsonObject(); + JsonObject bes = new JsonObject(); + for (String computeGroup : be.keySet()) { + JsonArray beIds = new JsonArray(); + beIds.add(be.get(computeGroup).getAsLong()); + bes.add(computeGroup, beIds); + } + json.add("bes", bes); + } + 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)); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
