HIVE-29818: ServletSecurity needs a UGI cache - #6704
Conversation
The REST Catalog creates a fresh proxy UserGroupInformation per request via UserGroupInformation.createProxyUser. Hadoop's FileSystem.CACHE retains a reference to every such UGI (and its RPC/IPC resources), so under proxy authentication these short-lived UGIs accumulate and eventually exhaust memory in long-running deployments. Cache the proxy UGI in ServletSecurity with a bounded, idle-evicting Caffeine cache keyed by (realUser, loginUser). Evicted entries release their resources via FileSystem.closeAllForUGI. Two new config vars tune the cache: - metastore.catalog.servlet.ugi.cache.size (default 1000) - metastore.catalog.servlet.ugi.cache.expiry (default 3600s, 0 disables) Add TestServletSecurity covering per-user caching, distinct proxies per user, eviction-triggered FileSystem cleanup, and disabled expiry.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (4)
standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/ServletSecurity.java:123
- The
UgiKeyfield namerealUseris misleading given the Javadoc: this value represents the effective/proxy user being impersonated, while the real user is the login user. Renaming the record components to something likeeffectiveUserandloginUser(orloginUserName) would reduce confusion and make logs likekey.realUser()accurate.
/**
* Cache key for a proxy {@link UserGroupInformation}. A proxy UGI is bound to both the effective user it
* impersonates and the server login user acting as its real user, so both participate in identity.
*/
record UgiKey(String realUser, String loginUser) {}
standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/ServletSecurity.java:137
- Casting the configured cache size from
longtointcan truncate large configured values (or wrap negative), leading to an incorrectmaximumSizeand unexpected behavior. Consider keeping this as alongend-to-end (Caffeine’smaximumSizeaccepts along) and validating the value (e.g., reject negatives).
this.proxyUserCache = createCacheWithConfig(
MetastoreConf.getTimeVar(conf, MetastoreConf.ConfVars.CATALOG_SERVLET_UGI_CACHE_EXPIRY, TimeUnit.MILLISECONDS),
(int) MetastoreConf.getLongVar(conf, MetastoreConf.ConfVars.CATALOG_SERVLET_UGI_CACHE_SIZE));
standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/ServletSecurity.java:170
- Using
.executor(Runnable::run)makes the removal listener run inline on the calling thread (likely a request thread). Since the listener callsFileSystem.closeAllForUGI, eviction can add noticeable latency or block request handling during bursts/evictions. Consider using the default executor or a dedicated bounded executor for removals so cleanup work doesn’t run on latency-sensitive threads.
Caffeine<UgiKey, UserGroupInformation> builder = Caffeine.<UgiKey, UserGroupInformation>newBuilder()
.maximumSize(maxSize)
.executor(Runnable::run)
.removalListener(cleanupListener);
standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/ServletSecurity.java:196
- Logging the effective username at
INFOcan leak user identity information into logs and may be considered sensitive in some deployments. Since this log happens for cache misses (and potentially many distinct users), consider downgrading it toDEBUGor making it configurable/redacted.
UserGroupInformation getProxyUser(String userName, UserGroupInformation loginUser) {
return proxyUserCache.get(new UgiKey(userName, loginUser.getUserName()), key -> {
LOG.info("Creating proxy user for: {}", key.realUser());
return UserGroupInformation.createProxyUser(key.realUser(), loginUser);
});
}
ayushtkn
left a comment
There was a problem hiding this comment.
Thanx @henrib this overall looks good to me, there are some suppressed comments from co-pilot but I feel they are minor and maybe worth addressing, can u give a check once
standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/ServletSecurity.java:123
The UgiKey field name realUser is misleading given the Javadoc: this value represents the effective/proxy user being impersonated, while the real user is the login user. Renaming the record components to something like effectiveUser and loginUser (or loginUserName) would reduce confusion and make logs like key.realUser() accurate.
/**
* Cache key for a proxy {@link UserGroupInformation}. A proxy UGI is bound to both the effective user it
* impersonates and the server login user acting as its real user, so both participate in identity.
*/
record UgiKey(String realUser, String loginUser) {}
standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/ServletSecurity.java:137
Casting the configured cache size from long to int can truncate large configured values (or wrap negative), leading to an incorrect maximumSize and unexpected behavior. Consider keeping this as a long end-to-end (Caffeine’s maximumSize accepts a long) and validating the value (e.g., reject negatives).
this.proxyUserCache = createCacheWithConfig(
MetastoreConf.getTimeVar(conf, MetastoreConf.ConfVars.CATALOG_SERVLET_UGI_CACHE_EXPIRY, TimeUnit.MILLISECONDS),
(int) MetastoreConf.getLongVar(conf, MetastoreConf.ConfVars.CATALOG_SERVLET_UGI_CACHE_SIZE));
standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/ServletSecurity.java:170
Using .executor(Runnable::run) makes the removal listener run inline on the calling thread (likely a request thread). Since the listener calls FileSystem.closeAllForUGI, eviction can add noticeable latency or block request handling during bursts/evictions. Consider using the default executor or a dedicated bounded executor for removals so cleanup work doesn’t run on latency-sensitive threads.
Caffeine<UgiKey, UserGroupInformation> builder = Caffeine.<UgiKey, UserGroupInformation>newBuilder()
.maximumSize(maxSize)
.executor(Runnable::run)
.removalListener(cleanupListener);
standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/ServletSecurity.java:196
Logging the effective username at INFO can leak user identity information into logs and may be considered sensitive in some deployments. Since this log happens for cache misses (and potentially many distinct users), consider downgrading it to DEBUG or making it configurable/redacted.
UserGroupInformation getProxyUser(String userName, UserGroupInformation loginUser) {
return proxyUserCache.get(new UgiKey(userName, loginUser.getUserName()), key -> {
LOG.info("Creating proxy user for: {}", key.realUser());
return UserGroupInformation.createProxyUser(key.realUser(), loginUser);
});
}
- Rename UgiKey.realUser to effectiveUser (impersonated user, not the Hadoop real user) - Keep cache size as long end-to-end, dropping the truncating int cast - Run removal-listener cleanup on ForkJoinPool.commonPool() instead of the request thread; add a test-only constructor to inject a synchronous executor - Downgrade proxy-user creation log from INFO to DEBUG
|
saihemanth-cloudera
left a comment
There was a problem hiding this comment.
Overall patch looks good to me. A couple of points to think about.
| private Cache<UgiKey, UserGroupInformation> createCacheWithConfig(long expirationMs, long maxSize, | ||
| Executor cacheCleanupExecutor) { | ||
| // Note: eviction closes the UGI's FileSystems. If an entry is evicted while a request is still inside doAs, | ||
| // that in-flight operation could see a "FileSystem closed" error. We don't reference-count to prevent this; |
There was a problem hiding this comment.
So we can potentially see failures for in-flight REST catalog operations.
How about we keep tracking of active users and deferring cleanup until the last request exits, or using any another lifecycle model that never closes a UGI currently in use?
| .removalListener(cleanupListener); | ||
|
|
||
| if (expirationMs > 0) { | ||
| builder.expireAfterAccess(Duration.ofMillis(expirationMs)) |
There was a problem hiding this comment.
This is edge case but worth thinking about: A burst of distinct proxy users followed by an idle period can keep the UGI/FileSystem resources alive well past metastore.catalog.servlet.ugi.cache.expiry
Since the feature is specifically about idle cleanup, should we think about adding a scheduler or a servlet-owned maintenance task that periodically calls cleanUp()
| (key, ugi, cause) -> { | ||
| if (ugi != null) { | ||
| try { | ||
| FileSystem.closeAllForUGI(ugi); |
There was a problem hiding this comment.
This can perform blocking filesystem cleanup. Because we are invoking this on ForkJoinPool.commonPool() running that on the JVM common pool risks interfering with unrelated async work.
Should a small dedicated executor owned by ServletSecurity or the metastore service would be a safer appraoch, with lifecycle shutdown?



What changes were proposed in this pull request?
The REST Catalog creates a fresh proxy
UserGroupInformationper request viaUserGroupInformation.createProxyUser. Hadoop'sFileSystem.CACHEretains a reference to every such UGI (and its RPC/IPC resources), so under proxy authentication these short-lived UGIs accumulate and eventually exhaust memory in long-running deployments.This PR caches the proxy UGI in
ServletSecurityusing a bounded, idle-evicting Caffeine cache keyed by(realUser, loginUser). Evicted entries release their resources viaFileSystem.closeAllForUGI. Two new config vars tune the cache:metastore.catalog.servlet.ugi.cache.size(default 1000)metastore.catalog.servlet.ugi.cache.expiry(default 3600s, 0 disables expiry)A note in the code documents why eviction-while-in-use is not reference-counted: eviction is idle-based (
expireAfterAccess) and both the expiry window and max size are expected to be kept well above the longest operation / peak concurrent distinct users.Why are the changes needed?
To prevent the
OutOfMemoryErrorcaused by unbounded accumulation of proxy UGIs and their associated FileSystem/IPC resources in long-running REST Catalog deployments.Does this PR introduce any user-facing change?
Two new (optional) metastore configuration properties, both with sensible defaults.
How was this patch tested?
Added
TestServletSecuritycovering per-user caching, distinct proxies per user, eviction-triggeredFileSystem.closeAllForUGIcleanup, and disabled expiry. All 4 tests pass.