HADOOP-10755. Support negative caching of user-group mapping. Contributed by Lei Xu.

git-svn-id: https://svn.apache.org/repos/asf/hadoop/common/branches/branch-2@1612409 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Andrew Wang 2014-07-21 21:52:52 +00:00
parent 0e2fee873d
commit 04b1192ed6
9 changed files with 234 additions and 9 deletions

View File

@ -39,6 +39,9 @@ Release 2.6.0 - UNRELEASED
HADOOP-10817. ProxyUsers configuration should support configurable HADOOP-10817. ProxyUsers configuration should support configurable
prefixes. (tucu) prefixes. (tucu)
HADOOP-10755. Support negative caching of user-group mapping.
(Lei Xu via wang)
OPTIMIZATIONS OPTIMIZATIONS
BUG FIXES BUG FIXES

View File

@ -250,6 +250,12 @@ public class CommonConfigurationKeysPublic {
public static final long HADOOP_SECURITY_GROUPS_CACHE_SECS_DEFAULT = public static final long HADOOP_SECURITY_GROUPS_CACHE_SECS_DEFAULT =
300; 300;
/** See <a href="{@docRoot}/../core-default.html">core-default.xml</a> */ /** 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 = public static final String HADOOP_SECURITY_GROUPS_CACHE_WARN_AFTER_MS =
"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 = public static final long HADOOP_SECURITY_GROUPS_CACHE_WARN_AFTER_MS_DEFAULT =

View File

@ -33,7 +33,7 @@ import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.CommonConfigurationKeys; import org.apache.hadoop.fs.CommonConfigurationKeys;
import org.apache.hadoop.util.ReflectionUtils; import org.apache.hadoop.util.ReflectionUtils;
import org.apache.hadoop.util.StringUtils; 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.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
@ -58,9 +58,15 @@ public class Groups {
private final Map<String, List<String>> staticUserToGroupsMap = private final Map<String, List<String>> staticUserToGroupsMap =
new HashMap<String, List<String>>(); new HashMap<String, List<String>>();
private final long cacheTimeout; private final long cacheTimeout;
private final long negativeCacheTimeout;
private final long warningDeltaMs; private final long warningDeltaMs;
private final Timer timer;
public Groups(Configuration conf) { public Groups(Configuration conf) {
this(conf, new Timer());
}
public Groups(Configuration conf, Timer timer) {
impl = impl =
ReflectionUtils.newInstance( ReflectionUtils.newInstance(
conf.getClass(CommonConfigurationKeys.HADOOP_SECURITY_GROUP_MAPPING, conf.getClass(CommonConfigurationKeys.HADOOP_SECURITY_GROUP_MAPPING,
@ -71,11 +77,16 @@ public class Groups {
cacheTimeout = cacheTimeout =
conf.getLong(CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_CACHE_SECS, conf.getLong(CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_CACHE_SECS,
CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_CACHE_SECS_DEFAULT) * 1000; 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 = warningDeltaMs =
conf.getLong(CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_CACHE_WARN_AFTER_MS, conf.getLong(CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_CACHE_WARN_AFTER_MS,
CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_CACHE_WARN_AFTER_MS_DEFAULT); CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_CACHE_WARN_AFTER_MS_DEFAULT);
parseStaticMapping(conf); parseStaticMapping(conf);
this.timer = timer;
if(LOG.isDebugEnabled()) if(LOG.isDebugEnabled())
LOG.debug("Group mapping impl=" + impl.getClass().getName() + LOG.debug("Group mapping impl=" + impl.getClass().getName() +
"; cacheTimeout=" + cacheTimeout + "; warningDeltaMs=" + "; cacheTimeout=" + cacheTimeout + "; warningDeltaMs=" +
@ -112,6 +123,28 @@ public class 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. * Get the group memberships of a given user.
* @param user User's name * @param user User's name
@ -126,18 +159,22 @@ public class Groups {
} }
// Return cached value if available // Return cached value if available
CachedGroups groups = userToGroupsMap.get(user); CachedGroups groups = userToGroupsMap.get(user);
long startMs = Time.monotonicNow(); long startMs = timer.monotonicNow();
// if cache has a value and it hasn't expired if (!hasExpired(groups, startMs)) {
if (groups != null && (groups.getTimestamp() + cacheTimeout > startMs)) {
if(LOG.isDebugEnabled()) { if(LOG.isDebugEnabled()) {
LOG.debug("Returning cached groups for '" + user + "'"); 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(); return groups.getGroups();
} }
// Create and cache user's groups // Create and cache user's groups
List<String> groupList = impl.getGroups(user); List<String> groupList = impl.getGroups(user);
long endMs = Time.monotonicNow(); long endMs = timer.monotonicNow();
long deltaMs = endMs - startMs ; long deltaMs = endMs - startMs ;
UserGroupInformation.metrics.addGetGroups(deltaMs); UserGroupInformation.metrics.addGetGroups(deltaMs);
if (deltaMs > warningDeltaMs) { if (deltaMs > warningDeltaMs) {
@ -146,6 +183,9 @@ public class Groups {
} }
groups = new CachedGroups(groupList, endMs); groups = new CachedGroups(groupList, endMs);
if (groups.getGroups().isEmpty()) { if (groups.getGroups().isEmpty()) {
if (isNegativeCacheEnabled()) {
userToGroupsMap.put(user, groups);
}
throw new IOException("No groups found for user " + user); throw new IOException("No groups found for user " + user);
} }
userToGroupsMap.put(user, groups); userToGroupsMap.put(user, groups);

View File

@ -201,7 +201,8 @@ public class LdapGroupsMapping
} catch (CommunicationException e) { } catch (CommunicationException e) {
LOG.warn("Connection is closed, will try to reconnect"); LOG.warn("Connection is closed, will try to reconnect");
} catch (NamingException e) { } 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; return emptyResults;
} }
@ -215,7 +216,8 @@ public class LdapGroupsMapping
} catch (CommunicationException e) { } catch (CommunicationException e) {
LOG.warn("Connection being closed, reconnecting failed, retryCount = " + retryCount); LOG.warn("Connection being closed, reconnecting failed, retryCount = " + retryCount);
} catch (NamingException e) { } 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; return emptyResults;
} }
} }

View File

@ -84,7 +84,8 @@ public class ShellBasedUnixGroupsMapping
result = Shell.execCommand(Shell.getGroupsForUserCommand(user)); result = Shell.execCommand(Shell.getGroupsForUserCommand(user));
} catch (ExitCodeException e) { } catch (ExitCodeException e) {
// if we didn't get the group - just return empty list; // 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>(); return new LinkedList<String>();
} }

View File

@ -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(); }
}

View File

@ -197,6 +197,20 @@ for ldap providers in the same way as above does.
</description> </description>
</property> </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> <property>
<name>hadoop.security.groups.cache.warn.after.ms</name> <name>hadoop.security.groups.cache.warn.after.ms</name>
<value>5000</value> <value>5000</value>

View File

@ -26,8 +26,11 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import org.apache.hadoop.test.GenericTestUtils;
import org.apache.hadoop.util.FakeTimer;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
@ -94,6 +97,9 @@ public class TestGroupsCaching {
@Test @Test
public void testGroupsCaching() throws Exception { public void testGroupsCaching() throws Exception {
// Disable negative cache.
conf.setLong(
CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_NEGATIVE_CACHE_SECS, 0);
Groups groups = new Groups(conf); Groups groups = new Groups(conf);
groups.cacheGroupsAdd(Arrays.asList(myGroups)); groups.cacheGroupsAdd(Arrays.asList(myGroups));
groups.refresh(); groups.refresh();
@ -163,4 +169,54 @@ public class TestGroupsCaching {
FakeunPrivilegedGroupMapping.invoked); 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));
}
} }

View File

@ -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;
}
}