HADOOP-10755. Support negative caching of user-group mapping. Contributed by Lei Xu.
git-svn-id: https://svn.apache.org/repos/asf/hadoop/common/trunk@1612408 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
c2174a5536
commit
d3bf8186ae
@ -441,6 +441,9 @@ Release 2.6.0 - UNRELEASED
|
||||
HADOOP-10817. ProxyUsers configuration should support configurable
|
||||
prefixes. (tucu)
|
||||
|
||||
HADOOP-10755. Support negative caching of user-group mapping.
|
||||
(Lei Xu via wang)
|
||||
|
||||
OPTIMIZATIONS
|
||||
|
||||
BUG FIXES
|
||||
|
@ -250,6 +250,12 @@ public class CommonConfigurationKeysPublic {
|
||||
public static final long HADOOP_SECURITY_GROUPS_CACHE_SECS_DEFAULT =
|
||||
300;
|
||||
/** See <a href="{@docRoot}/../core-default.html">core-default.xml</a> */
|
||||
public static final String HADOOP_SECURITY_GROUPS_NEGATIVE_CACHE_SECS =
|
||||
"hadoop.security.groups.negative-cache.secs";
|
||||
/** See <a href="{@docRoot}/../core-default.html">core-default.xml</a> */
|
||||
public static final long HADOOP_SECURITY_GROUPS_NEGATIVE_CACHE_SECS_DEFAULT =
|
||||
30;
|
||||
/** See <a href="{@docRoot}/../core-default.html">core-default.xml</a> */
|
||||
public static final String HADOOP_SECURITY_GROUPS_CACHE_WARN_AFTER_MS =
|
||||
"hadoop.security.groups.cache.warn.after.ms";
|
||||
public static final long HADOOP_SECURITY_GROUPS_CACHE_WARN_AFTER_MS_DEFAULT =
|
||||
|
@ -33,7 +33,7 @@
|
||||
import org.apache.hadoop.fs.CommonConfigurationKeys;
|
||||
import org.apache.hadoop.util.ReflectionUtils;
|
||||
import org.apache.hadoop.util.StringUtils;
|
||||
import org.apache.hadoop.util.Time;
|
||||
import org.apache.hadoop.util.Timer;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@ -58,24 +58,35 @@ public class Groups {
|
||||
private final Map<String, List<String>> staticUserToGroupsMap =
|
||||
new HashMap<String, List<String>>();
|
||||
private final long cacheTimeout;
|
||||
private final long negativeCacheTimeout;
|
||||
private final long warningDeltaMs;
|
||||
private final Timer timer;
|
||||
|
||||
public Groups(Configuration conf) {
|
||||
this(conf, new Timer());
|
||||
}
|
||||
|
||||
public Groups(Configuration conf, Timer timer) {
|
||||
impl =
|
||||
ReflectionUtils.newInstance(
|
||||
conf.getClass(CommonConfigurationKeys.HADOOP_SECURITY_GROUP_MAPPING,
|
||||
ShellBasedUnixGroupsMapping.class,
|
||||
GroupMappingServiceProvider.class),
|
||||
conf);
|
||||
|
||||
|
||||
cacheTimeout =
|
||||
conf.getLong(CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_CACHE_SECS,
|
||||
CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_CACHE_SECS_DEFAULT) * 1000;
|
||||
negativeCacheTimeout =
|
||||
conf.getLong(CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_NEGATIVE_CACHE_SECS,
|
||||
CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_NEGATIVE_CACHE_SECS_DEFAULT) * 1000;
|
||||
warningDeltaMs =
|
||||
conf.getLong(CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_CACHE_WARN_AFTER_MS,
|
||||
CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_CACHE_WARN_AFTER_MS_DEFAULT);
|
||||
parseStaticMapping(conf);
|
||||
|
||||
this.timer = timer;
|
||||
|
||||
if(LOG.isDebugEnabled())
|
||||
LOG.debug("Group mapping impl=" + impl.getClass().getName() +
|
||||
"; cacheTimeout=" + cacheTimeout + "; warningDeltaMs=" +
|
||||
@ -111,7 +122,29 @@ private void parseStaticMapping(Configuration conf) {
|
||||
staticUserToGroupsMap.put(user, groups);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the CachedGroups is expired.
|
||||
* @param groups cached groups for one user.
|
||||
* @return true if groups is expired from useToGroupsMap.
|
||||
*/
|
||||
private boolean hasExpired(CachedGroups groups, long startMs) {
|
||||
if (groups == null) {
|
||||
return true;
|
||||
}
|
||||
long timeout = cacheTimeout;
|
||||
if (isNegativeCacheEnabled() && groups.getGroups().isEmpty()) {
|
||||
// This CachedGroups is in the negative cache, thus it should expire
|
||||
// sooner.
|
||||
timeout = negativeCacheTimeout;
|
||||
}
|
||||
return groups.getTimestamp() + timeout <= startMs;
|
||||
}
|
||||
|
||||
private boolean isNegativeCacheEnabled() {
|
||||
return negativeCacheTimeout > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the group memberships of a given user.
|
||||
* @param user User's name
|
||||
@ -126,18 +159,22 @@ public List<String> getGroups(String user) throws IOException {
|
||||
}
|
||||
// Return cached value if available
|
||||
CachedGroups groups = userToGroupsMap.get(user);
|
||||
long startMs = Time.monotonicNow();
|
||||
// if cache has a value and it hasn't expired
|
||||
if (groups != null && (groups.getTimestamp() + cacheTimeout > startMs)) {
|
||||
long startMs = timer.monotonicNow();
|
||||
if (!hasExpired(groups, startMs)) {
|
||||
if(LOG.isDebugEnabled()) {
|
||||
LOG.debug("Returning cached groups for '" + user + "'");
|
||||
}
|
||||
if (groups.getGroups().isEmpty()) {
|
||||
// Even with enabling negative cache, getGroups() has the same behavior
|
||||
// that throws IOException if the groups for the user is empty.
|
||||
throw new IOException("No groups found for user " + user);
|
||||
}
|
||||
return groups.getGroups();
|
||||
}
|
||||
|
||||
// Create and cache user's groups
|
||||
List<String> groupList = impl.getGroups(user);
|
||||
long endMs = Time.monotonicNow();
|
||||
long endMs = timer.monotonicNow();
|
||||
long deltaMs = endMs - startMs ;
|
||||
UserGroupInformation.metrics.addGetGroups(deltaMs);
|
||||
if (deltaMs > warningDeltaMs) {
|
||||
@ -146,6 +183,9 @@ public List<String> getGroups(String user) throws IOException {
|
||||
}
|
||||
groups = new CachedGroups(groupList, endMs);
|
||||
if (groups.getGroups().isEmpty()) {
|
||||
if (isNegativeCacheEnabled()) {
|
||||
userToGroupsMap.put(user, groups);
|
||||
}
|
||||
throw new IOException("No groups found for user " + user);
|
||||
}
|
||||
userToGroupsMap.put(user, groups);
|
||||
|
@ -201,7 +201,8 @@ public synchronized List<String> getGroups(String user) throws IOException {
|
||||
} catch (CommunicationException e) {
|
||||
LOG.warn("Connection is closed, will try to reconnect");
|
||||
} catch (NamingException e) {
|
||||
LOG.warn("Exception trying to get groups for user " + user, e);
|
||||
LOG.warn("Exception trying to get groups for user " + user + ": "
|
||||
+ e.getMessage());
|
||||
return emptyResults;
|
||||
}
|
||||
|
||||
@ -215,7 +216,8 @@ public synchronized List<String> getGroups(String user) throws IOException {
|
||||
} catch (CommunicationException e) {
|
||||
LOG.warn("Connection being closed, reconnecting failed, retryCount = " + retryCount);
|
||||
} catch (NamingException e) {
|
||||
LOG.warn("Exception trying to get groups for user " + user, e);
|
||||
LOG.warn("Exception trying to get groups for user " + user + ":"
|
||||
+ e.getMessage());
|
||||
return emptyResults;
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +84,8 @@ private static List<String> getUnixGroups(final String user) throws IOException
|
||||
result = Shell.execCommand(Shell.getGroupsForUserCommand(user));
|
||||
} catch (ExitCodeException e) {
|
||||
// if we didn't get the group - just return empty list;
|
||||
LOG.warn("got exception trying to get groups for user " + user, e);
|
||||
LOG.warn("got exception trying to get groups for user " + user + ": "
|
||||
+ e.getMessage());
|
||||
return new LinkedList<String>();
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hadoop.util;
|
||||
|
||||
import org.apache.hadoop.classification.InterfaceAudience;
|
||||
import org.apache.hadoop.classification.InterfaceStability;
|
||||
|
||||
/**
|
||||
* Utility methods for getting the time and computing intervals.
|
||||
*
|
||||
* It has the same behavior as {{@link Time}}, with the exception that its
|
||||
* functions can be overridden for dependency injection purposes.
|
||||
*/
|
||||
@InterfaceAudience.Private
|
||||
@InterfaceStability.Unstable
|
||||
public class Timer {
|
||||
/**
|
||||
* Current system time. Do not use this to calculate a duration or interval
|
||||
* to sleep, because it will be broken by settimeofday. Instead, use
|
||||
* monotonicNow.
|
||||
* @return current time in msec.
|
||||
*/
|
||||
public long now() {
|
||||
return Time.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Current time from some arbitrary time base in the past, counting in
|
||||
* milliseconds, and not affected by settimeofday or similar system clock
|
||||
* changes. This is appropriate to use when computing how much longer to
|
||||
* wait for an interval to expire.
|
||||
* @return a monotonic clock that counts in milliseconds.
|
||||
*/
|
||||
public long monotonicNow() { return Time.monotonicNow(); }
|
||||
}
|
@ -197,6 +197,20 @@ for ldap providers in the same way as above does.
|
||||
</description>
|
||||
</property>
|
||||
|
||||
<property>
|
||||
<name>hadoop.security.groups.negative-cache.secs</name>
|
||||
<value>30</value>
|
||||
<description>
|
||||
Expiration time for entries in the the negative user-to-group mapping
|
||||
caching, in seconds. This is useful when invalid users are retrying
|
||||
frequently. It is suggested to set a small value for this expiration, since
|
||||
a transient error in group lookup could temporarily lock out a legitimate
|
||||
user.
|
||||
|
||||
Set this to zero or negative value to disable negative user-to-group caching.
|
||||
</description>
|
||||
</property>
|
||||
|
||||
<property>
|
||||
<name>hadoop.security.groups.cache.warn.after.ms</name>
|
||||
<value>5000</value>
|
||||
|
@ -26,8 +26,11 @@
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.hadoop.test.GenericTestUtils;
|
||||
import org.apache.hadoop.util.FakeTimer;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.fail;
|
||||
@ -94,6 +97,9 @@ public static void addToBlackList(String user) throws IOException {
|
||||
|
||||
@Test
|
||||
public void testGroupsCaching() throws Exception {
|
||||
// Disable negative cache.
|
||||
conf.setLong(
|
||||
CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_NEGATIVE_CACHE_SECS, 0);
|
||||
Groups groups = new Groups(conf);
|
||||
groups.cacheGroupsAdd(Arrays.asList(myGroups));
|
||||
groups.refresh();
|
||||
@ -163,4 +169,54 @@ public void testGroupLookupForStaticUsers() throws Exception {
|
||||
FakeunPrivilegedGroupMapping.invoked);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNegativeGroupCaching() throws Exception {
|
||||
final String user = "negcache";
|
||||
final String failMessage = "Did not throw IOException: ";
|
||||
conf.setLong(
|
||||
CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_NEGATIVE_CACHE_SECS, 2);
|
||||
FakeTimer timer = new FakeTimer();
|
||||
Groups groups = new Groups(conf, timer);
|
||||
groups.cacheGroupsAdd(Arrays.asList(myGroups));
|
||||
groups.refresh();
|
||||
FakeGroupMapping.addToBlackList(user);
|
||||
|
||||
// In the first attempt, the user will be put in the negative cache.
|
||||
try {
|
||||
groups.getGroups(user);
|
||||
fail(failMessage + "Failed to obtain groups from FakeGroupMapping.");
|
||||
} catch (IOException e) {
|
||||
// Expects to raise exception for the first time. But the user will be
|
||||
// put into the negative cache
|
||||
GenericTestUtils.assertExceptionContains("No groups found for user", e);
|
||||
}
|
||||
|
||||
// The second time, the user is in the negative cache.
|
||||
try {
|
||||
groups.getGroups(user);
|
||||
fail(failMessage + "The user is in the negative cache.");
|
||||
} catch (IOException e) {
|
||||
GenericTestUtils.assertExceptionContains("No groups found for user", e);
|
||||
}
|
||||
|
||||
// Brings back the backend user-group mapping service.
|
||||
FakeGroupMapping.clearBlackList();
|
||||
|
||||
// It should still get groups from the negative cache.
|
||||
try {
|
||||
groups.getGroups(user);
|
||||
fail(failMessage + "The user is still in the negative cache, even " +
|
||||
"FakeGroupMapping has resumed.");
|
||||
} catch (IOException e) {
|
||||
GenericTestUtils.assertExceptionContains("No groups found for user", e);
|
||||
}
|
||||
|
||||
// Let the elements in the negative cache expire.
|
||||
timer.advance(4 * 1000);
|
||||
|
||||
// The groups for the user is expired in the negative cache, a new copy of
|
||||
// groups for the user is fetched.
|
||||
assertEquals(Arrays.asList(myGroups), groups.getGroups(user));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hadoop.util;
|
||||
|
||||
import org.apache.hadoop.classification.InterfaceAudience;
|
||||
import org.apache.hadoop.classification.InterfaceStability;
|
||||
|
||||
/**
|
||||
* FakeTimer can be used for test purposes to control the return values
|
||||
* from {{@link Timer}}.
|
||||
*/
|
||||
@InterfaceAudience.Private
|
||||
@InterfaceStability.Unstable
|
||||
public class FakeTimer extends Timer {
|
||||
private long nowMillis;
|
||||
|
||||
/** Constructs a FakeTimer with a non-zero value */
|
||||
public FakeTimer() {
|
||||
nowMillis = 1000; // Initialize with a non-trivial value.
|
||||
}
|
||||
|
||||
@Override
|
||||
public long now() {
|
||||
return nowMillis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long monotonicNow() {
|
||||
return nowMillis;
|
||||
}
|
||||
|
||||
/** Increases the time by milliseconds */
|
||||
public void advance(long advMillis) {
|
||||
nowMillis += advMillis;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user