-
Notifications
You must be signed in to change notification settings - Fork 3.7k
[opt](memory) Reduce CloudReplica per-instance memory footprint #61289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -56,7 +56,7 @@ public class CloudReplica extends Replica implements GsonPostProcessable { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @SerializedName(value = "bes") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private ConcurrentHashMap<String, List<Long>> primaryClusterToBackends = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @SerializedName(value = "be") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private ConcurrentHashMap<String, Long> primaryClusterToBackend = new ConcurrentHashMap<>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private volatile ConcurrentHashMap<String, Long> primaryClusterToBackend; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @SerializedName(value = "dbId") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private long dbId = -1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @SerializedName(value = "tableId") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -73,11 +73,22 @@ public class CloudReplica extends Replica implements GsonPostProcessable { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final Random rand = new Random(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Intern pool for cluster ID strings to avoid millions of duplicate String instances. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Typically only a handful of distinct cluster IDs exist in the system. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final ConcurrentHashMap<String, String> CLUSTER_ID_POOL = new ConcurrentHashMap<>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static String internClusterId(String clusterId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (clusterId == null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String existing = CLUSTER_ID_POOL.putIfAbsent(clusterId, clusterId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return existing != null ? existing : clusterId; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private Map<String, List<Long>> memClusterToBackends = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // clusterId, secondaryBe, changeTimestamp | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private Map<String, Pair<Long, Long>> secondaryClusterToBackends | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| = new ConcurrentHashMap<String, Pair<Long, Long>>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private volatile Map<String, Pair<Long, Long>> secondaryClusterToBackends; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private volatile Map<String, Pair<Long, Long>> secondaryClusterToBackends; | |
| private volatile ConcurrentHashMap<String, Pair<Long, Long>> secondaryClusterToBackends; |
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
secondaryClusterToBackends is typed as Map, so after Gson deserialization it may be a non-concurrent implementation (e.g., LinkedTreeMap). In that case it will be non-null and getOrCreateSecondaryMap() will not replace it, and subsequent concurrent reads/writes can become unsafe. A concrete fix is to (1) declare the field as ConcurrentHashMap<String, Pair<Long, Long>> (and have getOrCreateSecondaryMap() return ConcurrentHashMap), and/or (2) in gsonPostProcess() normalize any deserialized map into a ConcurrentHashMap (similar to what’s done for primaryClusterToBackend).
| private Map<String, Pair<Long, Long>> getOrCreateSecondaryMap() { | |
| Map<String, Pair<Long, Long>> map = secondaryClusterToBackends; | |
| if (map == null) { | |
| synchronized (this) { | |
| map = secondaryClusterToBackends; | |
| if (map == null) { | |
| map = new ConcurrentHashMap<>(2); | |
| secondaryClusterToBackends = map; | |
| } | |
| } | |
| } | |
| return map; | |
| private ConcurrentHashMap<String, Pair<Long, Long>> getOrCreateSecondaryMap() { | |
| Map<String, Pair<Long, Long>> map = secondaryClusterToBackends; | |
| if (map instanceof ConcurrentHashMap) { | |
| return (ConcurrentHashMap<String, Pair<Long, Long>>) map; | |
| } | |
| synchronized (this) { | |
| map = secondaryClusterToBackends; | |
| if (map instanceof ConcurrentHashMap) { | |
| return (ConcurrentHashMap<String, Pair<Long, Long>>) map; | |
| } | |
| ConcurrentHashMap<String, Pair<Long, Long>> concurrentMap = new ConcurrentHashMap<>(2); | |
| if (map != null) { | |
| concurrentMap.putAll(map); | |
| } | |
| secondaryClusterToBackends = concurrentMap; | |
| return concurrentMap; | |
| } |
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does two hash lookups (containsKey then get). Since ConcurrentHashMap disallows null values, you can do a single get and check for null (or use getOrDefault) to reduce overhead on a hot path.
| if (!replicaEnough && !allowColdRead && priMap != null && priMap.containsKey(clusterId)) { | |
| backendId = priMap.get(clusterId); | |
| if (!replicaEnough && !allowColdRead && priMap != null) { | |
| Long primaryBackendId = priMap.get(clusterId); | |
| if (primaryBackendId != null) { | |
| backendId = primaryBackendId; | |
| } |
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This eagerly allocates primaryClusterToBackend whenever primaryClusterToBackends is non-null, even if all entries have empty/null backend lists (no effective data to migrate). To preserve the memory-saving goal, consider deferring getOrCreatePrimaryMap() until the first time you actually encounter a non-empty beIds (i.e., allocate only when you’re about to put).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The static
CLUSTER_ID_POOLis unbounded and will retain all distinct cluster IDs for the lifetime of the JVM. If cluster IDs can be unbounded (e.g., come from external inputs or can churn over time), this becomes a memory leak and can negate the intended savings. Consider using a bounded cache (e.g., max size + eviction), or weak-value/weak-key interning (if available in the codebase) so unused IDs can be reclaimed; at minimum, document/enforce that cluster IDs are from a small, fixed set.