What's Wrong?
S3ClientFactory::_cache (be/src/util/s3_util.h) is a plain std::unordered_map<S3ClientConf, ..., S3ClientConfHash> with no eviction, TTL, or size cap:
auto it = _cache.find(s3_conf);
if (it != _cache.end()) { ... }
...
auto [it, _] = _cache.emplace(s3_conf, std::move(obj_client));
(s3_util.cpp, S3ClientFactory::create — identical on master and branch-4.1/4.1.4.)
S3ClientConf's identity includes the bucket, so every distinct bucket a BE process ever accesses creates and permanently retains a new cached client. This is a real problem for any catalog where storage is bucket-per-table rather than bucket-per-warehouse — e.g. AWS S3 Tables, which auto-provisions one bucket per Iceberg table. A long-lived BE process that scans N distinct tables accumulates N permanently-cached Aws::S3::S3Client objects, each with its own libcurl connection pool (CA bundle parsed per pooled handle) and, when using STS/WebIdentity-style credentials, an auxiliary STS credentials-provider sub-client built via a bare default constructor — bypassing Doris's own caFile/connection settings entirely.
Confirmed via jemalloc heap profiling (prof_active) that this is genuinely live allocated memory (jemalloc allocated, not just retained/fragmented resident), invisible to Doris's own MemTracker/workload-group accounting — it shows up purely as UntrackedMemory while workload groups report ~0 the whole time. One measured instance attributed roughly 700 MiB to these allocation sites after a routine ETL window, in a steadily-climbing, never-plateauing pattern that eventually leads to the process being killed under memory pressure.
How to Reproduce
- Configure an external catalog (e.g. Iceberg REST) where table storage maps to a distinct S3 bucket per table (S3 Tables does this automatically).
- From a single long-lived BE process, scan many distinct tables over time, no restarts between them.
- Observe RSS / jemalloc
allocated climbing roughly linearly with the number of distinct buckets ever touched, never plateauing, independent of query concurrency or workload-group memory settings.
- Workload-group memory metrics stay near zero throughout — the growth is invisible from Doris's own accounting.
What You Expected
Some bound on S3ClientFactory's cache — idle-TTL and/or LRU eviction, similar to the bounded/expiring cache (_azure_cache, capacity 256, LRU + expiry pruning) added for Azure clients in #68117. That PR explicitly left the non-Azure _cache untouched.
Version
Reproduced on the official 4.1.4 release (branch-4.1). Same unbounded _cache design confirmed present on master by source inspection.
Anything Else?
Happy to contribute a patch — fix direction: capacity-bounded LRU + idle-TTL eviction on _cache (mirroring #68117's _azure_cache); share one credentials-provider/STS client across cache entries that share the same credential config instead of one per bucket; and separately, the maxConnections fallback to 102400 in _create_s3_client when no value is supplied looks worth revisiting too.
Related but distinct: #66300 (different trigger, same "unbounded S3 connection" smell); #66997/#65416/#67056 (separate BE thread-pool leak, unrelated to this cache).
This issue was drafted by an AI coding assistant (Claude) based on source-code review and a heap-profiling investigation carried out by our engineering team, and was reviewed and submitted by a human maintainer of this report.
What's Wrong?
S3ClientFactory::_cache(be/src/util/s3_util.h) is a plainstd::unordered_map<S3ClientConf, ..., S3ClientConfHash>with no eviction, TTL, or size cap:(
s3_util.cpp,S3ClientFactory::create— identical onmasterandbranch-4.1/4.1.4.)S3ClientConf's identity includes the bucket, so every distinct bucket a BE process ever accesses creates and permanently retains a new cached client. This is a real problem for any catalog where storage is bucket-per-table rather than bucket-per-warehouse — e.g. AWS S3 Tables, which auto-provisions one bucket per Iceberg table. A long-lived BE process that scans N distinct tables accumulates N permanently-cachedAws::S3::S3Clientobjects, each with its own libcurl connection pool (CA bundle parsed per pooled handle) and, when using STS/WebIdentity-style credentials, an auxiliary STS credentials-provider sub-client built via a bare default constructor — bypassing Doris's owncaFile/connection settings entirely.Confirmed via jemalloc heap profiling (
prof_active) that this is genuinely live allocated memory (jemalloc allocated, not just retained/fragmentedresident), invisible to Doris's ownMemTracker/workload-group accounting — it shows up purely asUntrackedMemorywhile workload groups report ~0 the whole time. One measured instance attributed roughly 700 MiB to these allocation sites after a routine ETL window, in a steadily-climbing, never-plateauing pattern that eventually leads to the process being killed under memory pressure.How to Reproduce
allocatedclimbing roughly linearly with the number of distinct buckets ever touched, never plateauing, independent of query concurrency or workload-group memory settings.What You Expected
Some bound on
S3ClientFactory's cache — idle-TTL and/or LRU eviction, similar to the bounded/expiring cache (_azure_cache, capacity 256, LRU + expiry pruning) added for Azure clients in #68117. That PR explicitly left the non-Azure_cacheuntouched.Version
Reproduced on the official 4.1.4 release (branch-4.1). Same unbounded
_cachedesign confirmed present onmasterby source inspection.Anything Else?
Happy to contribute a patch — fix direction: capacity-bounded LRU + idle-TTL eviction on
_cache(mirroring #68117's_azure_cache); share one credentials-provider/STS client across cache entries that share the same credential config instead of one per bucket; and separately, themaxConnectionsfallback to 102400 in_create_s3_clientwhen no value is supplied looks worth revisiting too.Related but distinct: #66300 (different trigger, same "unbounded S3 connection" smell); #66997/#65416/#67056 (separate BE thread-pool leak, unrelated to this cache).
This issue was drafted by an AI coding assistant (Claude) based on source-code review and a heap-profiling investigation carried out by our engineering team, and was reviewed and submitted by a human maintainer of this report.