Skip to content

[feature](fe) Add external metadata cache memory governance - #67726

Open
CalvinKirs wants to merge 1 commit into
apache:masterfrom
CalvinKirs:master-memory-cache
Open

[feature](fe) Add external metadata cache memory governance#67726
CalvinKirs wants to merge 1 commit into
apache:masterfrom
CalvinKirs:master-memory-cache

Conversation

@CalvinKirs

@CalvinKirs CalvinKirs commented Sep 9, 2026

Copy link
Copy Markdown
Member

What problem does this PR solve?

Related PR: #66717 (merged into branch-4.1)

Problem Summary:

Port external metadata cache memory governance to master, adapting it to the connector SPI and shared cache framework rather than cherry-picking the legacy 4.1 cache implementation unchanged.

Master adaptation: rebased onto 1335022902a. Preserved upstream DLF support when resolving the Iceberg/Paimon connector conflicts and adapted three assertions in the newly added FE cache test to master's JUnit 5 migration. No test expectations or additional production behavior were changed by conflict resolution.

Entry-count limits alone cannot control FE heap when external metadata values vary substantially in size. This change adds estimated retained-memory budgets for managed caches while keeping existing count-based behavior when no applicable memory limit is configured.

Scope and design

  • Add a shared estimator/admission interface and FE-global, catalog-total, and logical-entry budgets. Physical caches representing one logical entry share its quota, including connector-owned sibling caches.
  • Integrate the core schema cache and managed Hive/HMS, Iceberg, and Paimon caches. Include Hudi's sibling HMS caches in aggregate accounting and statistics without adding an unrelated Hudi-specific quota namespace.
  • Estimate on load/publication, not on cache hits. Use type-specific estimators and bounded object-graph validation where applicable; incomplete validation rejects cache admission instead of reporting a falsely small weight. Shared infrastructure is excluded at ownership boundaries.
  • Use strong cache values with explicit reservation ownership and coordinated reclamation. Keep replacement/removal accounting generation-safe and release ownership when catalogs/connectors close. This does not promise a global LRU or cross-catalog fairness.
  • Isolate weighted Iceberg statement metadata from cached generations so lazy query-side metadata does not silently grow an admitted value.
  • Expose MAX_WEIGHT, ESTIMATED_WEIGHT, WEIGHT_REJECT_COUNT, and LAST_WEIGHT_REJECT_REASON through information_schema.catalog_meta_cache_statistics, with rate-limited rejection warnings.

No optimizer rules, literal representations, or partition-item implementations are changed.

Configuration and behavior

# Optional FE-wide total, configured in fe.conf; requires restart.
# 0 disables only the global quota. Byte units or a JVM max-heap percentage are supported.
external_meta_cache_max_weight=0
-- Catalog/entry limits also work with the global quota disabled.
ALTER CATALOG lake SET PROPERTIES ('meta.cache.max-weight' = '1GB');
-- Optional per-entry limit; individual keys do not all need configuration.
ALTER CATALOG lake SET PROPERTIES ('meta.cache.iceberg.table.max-weight' = '256MB');

Catalog/entry property changes rebuild the affected cache owners without restarting FE. An oversized value, or one that cannot obtain budget after reclamation, is returned to the current request without being retained in the cache. Normal admission rejection does not fail the query. These are limits on estimated retained cache memory, not pre-load heap reservations or a guarantee against OOM while constructing a large value; active query objects and shared infrastructure are outside this budget.

Paimon microbenchmark (master adaptation)

Added a standalone Paimon benchmark (171 lines) plus two fixture-validation tests (67 lines), with no new dependency or production-code change. This is a microbenchmark harness, not JMH. A fresh focused run passed 33 tests (0 failures/errors/skips); module Checkstyle/architecture validation passed.

Three fresh Java 17 JVMs, -Xms1g -Xmx4g -XX:ActiveProcessorCount=4, 5 warmup and 15 measurement windows per operation. The benchmark calls the production partition collector with recording catalog fakes, using 10/100 schema fields, 1,000/10,000 partitions, and uniform/sampled-tail/unsampled-tail distributions. It rotates operation order and reports actual operation counts and min/median/max window timings. No remote RPC, object-store latency, data scan, cache-hit throughput, or full admission-lock cost is included.

Representative uniform cases (median of the three JVM medians):

Schema fields Partitions Collect only Collect + weighted view View publication only
10 1,000 213.49 us 219.41 us 3.76 us
10 10,000 2,492.62 us 2,524.50 us 7.91 us
100 1,000 212.04 us 219.03 us 3.89 us
100 10,000 2,544.63 us 2,648.27 us 7.93 us

The small +1.3% to +4.1% differences overlap run variation and are not precise overhead guarantees. All-case publication-only medians are 3.76–8.51 us; prepared-weight callback timing is about 14 ns including loop/volatile-sink overhead, not complete cache admission. Each fork measures 120–3,840 collection operations and 30,720–245,760 publication operations per case. This is not a claim of end-to-end query speedup.

Release note

Add configurable estimated-memory limits, dynamic catalog/entry quota changes, and memory usage/rejection statistics for managed external metadata caches.

Check List (For Author)

  • Test

    • Unit Test: post-rebase focused run passed 314 tests across 19 classes, with 0 failures/errors/skips. Covers configuration, budgets, concurrent cache lifecycle, reflective estimation, Hudi HMS ownership, Iceberg table isolation, FE weighted mutation rejection, and Iceberg/Paimon DLF catalog configuration and connectivity tests. The first post-rebase run caught the JUnit 4/5 assertion mismatch; the final run includes its correction.
    • Regression test (pre-rebase head f684d9fe479): existing external_table_p0/test_catalog_ddl passed on a running cluster (1 suite, 0 failures/fatal errors/skips). Not rerun against a deployed post-rebase FE.
    • Manual test (pre-rebase head f684d9fe479): local integration suite executed with run-regression-test.sh, followed by a separate comparison run against automatically generated and inspected output (1 suite, 0 failures/fatal errors/skips). Actual Iceberg REST/MinIO data: 256 partitions / 8,192 rows. Verified catalog-only quotas with global quota disabled, 64MB -> 1B -> 64MB, entry-local rejection/recovery, rejection observability, and identical query results with/without cache admission (COUNT(*)=8192, SUM(id)=33550336).
    • Post-rebase FE and connector plugins built and packaged with build.sh --fe into an isolated output directory; Checkstyle and git diff --check passed. A temporary FE-only guard avoided an unrelated missing native-thirdparty rebuild; that environment adjustment was restored and is not included in this PR. The running pre-rebase test cluster was not replaced.
    • Validation boundary: the integration cluster reused a 4.1.3 BE. Actual Iceberg scans passed, but that BE cannot scan the new system-table columns (no match column ... MAX_WEIGHT). Memory statistics were checked through the running FE's Thrift endpoint. Matching-master BE compilation/system-table end-to-end validation, full external-engine integration, and follower replay remain unverified by this local run. No new end-to-end performance claims are made.
  • Behavior changed:

    • Yes. Configured memory quotas govern admission and expose rejection statistics; unconfigured caches preserve count-based behavior.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label if needed

@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@CalvinKirs

Copy link
Copy Markdown
Member Author

run buildall

@CalvinKirs

Copy link
Copy Markdown
Member Author

/review

### What problem does this PR solve?

Issue Number: None

Related PR: apache#66717

Problem Summary: External catalog metadata caches were bounded primarily by entry count, so large Hive, Iceberg, and Paimon metadata objects could consume unpredictable FE heap and cause OOM. Add bounded retained-size estimation, hierarchical global/catalog/entry budgets, coordinated local reclamation, rejection observability, and catalog cache statistics. Keep estimation off the cache-hit path, preserve unbounded behavior when no weight limit is configured, and prevent managed cache owners from escaping connector lifecycle or registry concurrency. Include the Hive gateway's Hudi sibling HMS caches in shared aggregate accounting and statistics while preserving their existing entry-local configuration namespace.

### Release note

Add configurable memory limits and usage statistics for external metadata caches.

### Check List (For Author)

- Test: Unit Test
    - ./run-fe-ut.sh --run org.apache.doris.connector.cache.MetaCacheWeightGovernanceTest,org.apache.doris.connector.hive.HiveConnectorSiblingTest,org.apache.doris.connector.paimon.PaimonCatalogFactoryTest
    - EXTRA_FE_MODULES=hudi=fe-connector/fe-connector-hudi ./run-fe-ut.sh --run HudiConnectorHmsCacheTest,HudiResourceLifecycleTest,MetaCacheWeightGovernanceTest,HiveConnectorSiblingTest
    - Paimon partition weighting: 571 connector unit tests passed; one opt-in live-connectivity test skipped. FE/plugin packaging and Checkstyle passed. Three fresh JVM microbenchmark runs and independent local JOL calibration completed.
- Behavior changed: Yes (external metadata caches can be governed by retained-size limits and expose usage/rejection statistics)
- Does this need documentation: Yes (maintained in the Doris website documentation change)
@CalvinKirs

Copy link
Copy Markdown
Member Author

run buildall

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: six distinct issues remain on exact head 9010fa1.

Blocking findings:

  1. P1: exact-key invalidation can leave a resident value unaccounted.
  2. P1: Hive and Iceberg sampled variable payloads are falsely marked complete.
  3. P1: an unknown future max-weight key can block every unrelated ALTER after downgrade.
  4. P2: a disabled Iceberg table cache serializes and estimates full metadata on every live load under an enclosing limit.
  5. P2: changing the effective catalog maximum can race lazy reinitialization against the old core owner.
  6. P1: new FE statistics slots fail on an old BE before the RPC fallback can run.

Checkpoint conclusions:

  • Goal and proof: the managed memory-governance design is cohesive and has substantial coverage, but the quota, generation, configuration-compatibility, and rolling-upgrade invariants above are not met; the decisive regression tests are missing.
  • Scope: the authoritative 79-file patch is focused on governance and connector integration.
  • Concurrency and locks: peer reclaim, ordinary replacement, and close lock ordering look sound, with no additional deadlock found; findings 1 and 5 are the remaining races.
  • Lifecycle and static state: parent-first governance identity, initialization, and ordinary owner cleanup are sound; dynamic rebuilding still has finding 5.
  • Configuration and persistence: global restart-only and catalog dynamic routing were traced; findings 3 and 5 break downgrade and dynamic-update guarantees. There are no storage-format or transactional-write changes.
  • Compatibility and nullability: new-BE/old-FE RPC fallback and 24/28-cell materialization are safe, and appended nullable defaults otherwise look sound, but the new-FE/old-BE path in finding 6 remains broken.
  • Parallel and special paths: core, Remote Doris, Hive/HMS/Hudi, Iceberg, and Paimon were traced. Finding 2 affects Hive and both Iceberg estimators. Explicit Paimon SDK caching is an intentional unmanaged opt-in and is not raised.
  • Tests and results: coverage is broad, but deterministic same-node, skewed-tail, future-key ALTER, disabled-plus-bounded, real ALTER/reinit, and new-FE/old-BE slot tests are missing. This was a review-only run, so I did not build or execute tests; the PR's reported old-BE validation reproduces finding 6.
  • Observability: the new fields and warnings are appropriate, but findings 1 and 2 can underreport usage and finding 6 makes the fields unusable during upgrade.
  • Performance: estimation is generally confined to load/publication paths, but finding 4 adds avoidable full serialization to a deliberately disabled path.

Overall: request changes. No additional user focus was supplied. The review converged after three bounded rounds, with every final agent returning NO_NEW_VALUABLE_FINDINGS.

&& current.keyState == invalidated.keyState) {
data.asMap().remove(key, current);
}
if (registered != null && invalidated.node.registration.compareAndSet(registered, null)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Keep invalidation cleanup tied to the invalidated generation. A put can reuse this KeyNode after the state swap and install a new registration before cleanup reaches this CAS; cleanup then leaves the new value in Caffeine but clears/releases its reservation. Please compare against a registration tied to the invalidated KeyState and add a deterministic same-node test without the intervening getIfPresent.

bytes = add(bytes, JvmSizeUtils.stringSize(key.getTable()));
bytes = add(bytes, ARRAY_LIST_SHALLOW_BYTES);
bytes = add(bytes, JvmSizeUtils.objectArraySize(partitions.size()));
bytes = add(bytes, JvmSizeUtils.sampledListPayload(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Do not mark a sampled variable-length payload complete. A large partition at an unsampled index is retained but absent from both this sample and the reflective fallback, so the entry can be admitted below its real weight. The same bug is present in both Iceberg derived-view estimators; use an exhaustive construction-time payload pass (as Paimon does) or return incomplete, and add skewed-tail tests.

}
String entry = key.substring(prefix.length(), key.length() - KEY_MAX_WEIGHT.length());
if (!entries.contains(entry)) {
throw new IllegalArgumentException("Unknown metadata cache weight property: " + key);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve ALTER after downgrade. This binary can replay a catalog containing a future meta.cache.<engine>.<entry>.max-weight key, but the default update validator merges that key into every unrelated ALTER and this branch rejects it; ALTER CATALOG cannot remove properties. Distinguish unknown keys introduced by this statement from unknown keys already persisted so CREATE/new typos may still fail, and add a future-key plus unrelated-ALTER test.

}
};
TableOwner loaded = new TableOwner(table, cleanup, true);
TableOwner loaded = new TableOwner(table, cleanup, true, entry.isWeightBounded());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Skip weight preparation when this cache is disabled. With ttl-second=0 or enable=false plus a global/catalog limit, isWeightBounded is still true, so every live table load serializes and estimates the full TableMetadata even though the disabled path immediately discards it. Gate this work on entry.isEnabled() as well and cover the combined configuration.

ExternalMetaCacheMgr cacheMgr = Env.getCurrentEnv().getExtMetaCacheMgr();
if (updatedProps.get(SCHEMA_CACHE_TTL_SECOND) != null) {
if (updatedProps.get(SCHEMA_CACHE_TTL_SECOND) != null
|| updatedProps.containsKey(MetaCacheBudgetManager.CATALOG_MAX_WEIGHT_PROPERTY)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Retire old catalog budgets before allowing reinitialization. When this ALTER changes the effective maximum, resetToUninitialized releases the catalog monitor before removeCatalog runs, so a concurrent makeSureInitialized can build the new connector while the default-engine owner still holds the old catalog bucket; createEntryBudget then throws a conflicting-limit error. Fence reinitialization through removal and add a real old/new-budget interleaving test.

.column("LAST_LOAD_SUCCESS_TIME", ScalarType.createStringType())
.column("LAST_LOAD_FAILURE_TIME", ScalarType.createStringType())
.column("LAST_ERROR", ScalarType.createStringType())
.column("MAX_WEIGHT", ScalarType.createType(PrimitiveType.BIGINT))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Keep the appended slots usable during a rolling upgrade. A new FE can put MAX_WEIGHT (and the other new columns) in the destination tuple sent to an old BE, but the old SchemaScanOperatorX rejects that unknown slot during prepare, before this PR's FE-RPC 28-to-24 fallback can run. Gate planning/routing by BE compatibility and add a mixed-version test that selects and filters on MAX_WEIGHT.

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 66.28% (57/86) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 16765 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 9010fa178e52fe7a51364c379d40023d2048b511, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17597	3029	3013	3013
q2	2211	264	212	212
q3	10136	857	499	499
q4	4672	254	203	203
q5	7670	577	389	389
q6	135	117	98	98
q7	518	535	392	392
q8	9248	855	860	855
q9	3505	2401	2412	2401
q10	6509	857	731	731
q11	391	193	181	181
q12	599	258	200	200
q13	18141	1584	1179	1179
q14	155	161	141	141
q15	q16	439	397	377	377
q17	1370	891	783	783
q18	3171	2333	2299	2299
q19	1265	921	763	763
q20	394	292	198	198
q21	5573	1622	1853	1622
q22	327	272	229	229
Total cold run time: 94026 ms
Total hot run time: 16765 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	3378	3340	3307	3307
q2	511	398	376	376
q3	2262	2352	2176	2176
q4	1211	1198	897	897
q5	2218	2175	2208	2175
q6	171	117	88	88
q7	1043	945	907	907
q8	1573	1377	1390	1377
q9	3233	3177	3166	3166
q10	1904	1842	1683	1683
q11	362	276	255	255
q12	452	441	336	336
q13	1484	1555	1142	1142
q14	169	171	155	155
q15	q16	398	396	361	361
q17	3755	3321	3284	3284
q18	4920	4551	4877	4551
q19	1011	869	852	852
q20	1033	1009	879	879
q21	3816	3251	3200	3200
q22	390	344	307	307
Total cold run time: 35294 ms
Total hot run time: 31474 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 83285 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 9010fa178e52fe7a51364c379d40023d2048b511, data reload: false

query5	4237	411	344	344
query6	393	133	128	128
query7	4940	422	230	230
query8	308	128	134	128
query9	8698	2855	2865	2855
query10	403	241	181	181
query11	5372	1054	929	929
query12	119	71	70	70
query13	1186	442	328	328
query14	6113	2248	2131	2131
query14_1	2027	2018	2036	2018
query15	177	123	116	116
query16	899	371	345	345
query17	793	459	366	366
query18	2333	326	242	242
query19	163	141	105	105
query20	72	73	73	73
query21	200	100	87	87
query22	5603	5553	5509	5509
query23	6981	6366	6168	6168
query23_1	6199	6326	6322	6322
query24	7256	1110	749	749
query24_1	775	791	780	780
query25	440	291	253	253
query26	1233	241	132	132
query27	2781	427	254	254
query28	4696	1506	1501	1501
query29	937	442	353	353
query30	248	156	128	128
query31	828	407	335	335
query32	134	75	72	72
query33	467	230	177	177
query34	992	780	475	475
query35	402	419	349	349
query36	585	577	517	517
query37	117	98	76	76
query38	1022	857	843	843
query39	478	496	478	478
query39_1	467	459	459	459
query40	209	91	81	81
query41	60	59	57	57
query42	75	72	74	72
query43	242	241	215	215
query44	987	533	539	533
query45	116	106	113	106
query46	784	833	562	562
query47	765	753	744	744
query48	302	321	234	234
query49	534	276	183	183
query50	694	273	187	187
query51	8084	7965	7917	7917
query52	71	66	57	57
query53	213	234	153	153
query54	219	157	146	146
query55	73	62	54	54
query56	196	173	174	173
query57	710	651	669	651
query58	200	169	160	160
query59	1250	1246	1119	1119
query60	239	206	167	167
query61	111	106	111	106
query62	350	220	173	173
query63	189	138	140	138
query64	2747	741	639	639
query65	1730	1636	1650	1636
query66	1756	250	194	194
query67	10046	10098	10201	10098
query68	2921	1219	701	701
query69	343	213	185	185
query70	668	635	622	622
query71	253	180	162	162
query72	2253	1693	1481	1481
query73	642	626	328	328
query74	1972	1241	1143	1143
query75	1184	1115	980	980
query76	2338	708	506	506
query77	256	262	208	208
query78	4046	3887	3425	3425
query79	2261	858	594	594
query80	1595	321	258	258
query81	485	165	138	138
query82	607	122	100	100
query83	282	208	198	198
query84	292	112	89	89
query85	783	327	288	288
query86	380	177	170	170
query87	1054	1004	908	908
query88	2763	2085	2114	2085
query89	272	199	173	173
query90	1974	129	128	128
query91	130	114	97	97
query92	82	68	72	68
query93	1370	1013	736	736
query94	624	235	223	223
query95	517	307	226	226
query96	766	547	257	257
query97	1082	1050	1019	1019
query98	143	136	143	136
query99	420	344	308	308
Total cold run time: 178217 ms
Total hot run time: 83285 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 14.71 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 9010fa178e52fe7a51364c379d40023d2048b511, data reload: false

query1	0.01	0.01	0.00
query2	0.09	0.04	0.04
query3	0.25	0.11	0.11
query4	1.60	0.09	0.10
query5	0.17	0.16	0.16
query6	1.25	0.67	0.68
query7	0.03	0.01	0.00
query8	0.04	0.03	0.03
query9	0.29	0.21	0.21
query10	0.36	0.35	0.37
query11	0.16	0.12	0.12
query12	0.15	0.13	0.12
query13	0.30	0.30	0.31
query14	0.46	0.46	0.46
query15	0.36	0.35	0.36
query16	0.21	0.24	0.23
query17	0.67	0.69	0.68
query18	0.19	0.17	0.18
query19	1.13	1.10	1.13
query20	0.02	0.01	0.01
query21	15.45	0.17	0.12
query22	5.04	0.04	0.04
query23	16.16	0.25	0.10
query24	3.00	0.29	0.26
query25	0.12	0.03	0.04
query26	0.77	0.17	0.14
query27	0.04	0.03	0.02
query28	3.60	0.54	0.27
query29	12.46	3.12	2.54
query30	0.26	0.12	0.12
query31	2.76	0.38	0.18
query32	3.48	0.31	0.24
query33	1.41	1.47	1.41
query34	15.42	2.25	1.81
query35	1.75	1.81	1.73
query36	0.47	0.29	0.28
query37	0.07	0.04	0.04
query38	0.04	0.02	0.02
query39	0.03	0.03	0.02
query40	0.12	0.08	0.07
query41	0.08	0.03	0.03
query42	0.04	0.02	0.02
query43	0.04	0.03	0.02
Total cold run time: 90.35 s
Total hot run time: 14.71 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100% (0/0) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 76.50% (34947/45681)
Line Coverage 61.59% (394618/640725)
Region Coverage 57.83% (331903/573954)
Branch Coverage 58.65% (151572/258432)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants