|
@@ -0,0 +1,358 @@
|
|
|
+/**
|
|
|
+ * 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.hdfs.server.namenode;
|
|
|
+
|
|
|
+import java.io.IOException;
|
|
|
+import java.net.InetAddress;
|
|
|
+import java.net.UnknownHostException;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.HashSet;
|
|
|
+import java.util.Iterator;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.TreeMap;
|
|
|
+
|
|
|
+import org.apache.commons.logging.Log;
|
|
|
+import org.apache.commons.logging.LogFactory;
|
|
|
+import org.apache.hadoop.hdfs.protocol.DatanodeID;
|
|
|
+import org.apache.hadoop.util.HostsFileReader;
|
|
|
+
|
|
|
+/**
|
|
|
+ * This class manages the include and exclude files for HDFS.
|
|
|
+ *
|
|
|
+ * These files control which DataNodes the NameNode expects to see in the
|
|
|
+ * cluster. Loosely speaking, the include file, if it exists and is not
|
|
|
+ * empty, is a list of everything we expect to see. The exclude file is
|
|
|
+ * a list of everything we want to ignore if we do see it.
|
|
|
+ *
|
|
|
+ * Entries may or may not specify a port. If they don't, we consider
|
|
|
+ * them to apply to every DataNode on that host. For example, putting
|
|
|
+ * 192.168.0.100 in the excludes file blacklists both 192.168.0.100:5000 and
|
|
|
+ * 192.168.0.100:6000. This case comes up in unit tests.
|
|
|
+ *
|
|
|
+ * When reading the hosts files, we try to find the IP address for each
|
|
|
+ * entry. This is important because it allows us to de-duplicate entries.
|
|
|
+ * If the user specifies a node as foo.bar.com in the include file, but
|
|
|
+ * 192.168.0.100 in the exclude file, we need to realize that these are
|
|
|
+ * the same node. Resolving the IP address also allows us to give more
|
|
|
+ * information back to getDatanodeListForReport, which makes the web UI
|
|
|
+ * look nicer (among other things.) See HDFS-3934 for more details.
|
|
|
+ *
|
|
|
+ * DNS resolution can be slow. For this reason, we ONLY do it when (re)reading
|
|
|
+ * the hosts files. In all other cases, we rely on the cached values either
|
|
|
+ * in the DatanodeID objects, or in HostFileManager#Entry.
|
|
|
+ * We also don't want to be holding locks when doing this.
|
|
|
+ * See HDFS-3990 for more discussion of DNS overheads.
|
|
|
+ *
|
|
|
+ * Not all entries in the hosts files will have an associated IP address.
|
|
|
+ * Some entries may be "registration names." The "registration name" of
|
|
|
+ * a DataNode is either the actual hostname, or an arbitrary string configured
|
|
|
+ * by dfs.datanode.hostname. It's possible to add registration names to the
|
|
|
+ * include or exclude files. If we can't find an IP address associated with
|
|
|
+ * a host file entry, we assume it's a registered hostname and act accordingly.
|
|
|
+ * The "registration name" feature is a little odd and it may be removed in the
|
|
|
+ * future (I hope?)
|
|
|
+ */
|
|
|
+public class HostFileManager {
|
|
|
+ private static final Log LOG = LogFactory.getLog(HostFileManager.class);
|
|
|
+
|
|
|
+ public static class Entry {
|
|
|
+ /**
|
|
|
+ * This what the user put on the line before the colon, or the whole line
|
|
|
+ * if there is no colon.
|
|
|
+ */
|
|
|
+ private final String prefix;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * This is the port which was specified after the colon. It is 0 if no
|
|
|
+ * port was given.
|
|
|
+ */
|
|
|
+ private final int port;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * If we can resolve the IP address, this is it. Otherwise, it is the
|
|
|
+ * empty string.
|
|
|
+ */
|
|
|
+ private final String ipAddress;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Parse a hosts file Entry.
|
|
|
+ */
|
|
|
+ static Entry parse(String fileName, String entry) throws IOException {
|
|
|
+ final String prefix;
|
|
|
+ final int port;
|
|
|
+ String ipAddress = "";
|
|
|
+
|
|
|
+ int idx = entry.indexOf(':');
|
|
|
+ if (-1 == idx) {
|
|
|
+ prefix = entry;
|
|
|
+ port = 0;
|
|
|
+ } else {
|
|
|
+ prefix = entry.substring(0, idx);
|
|
|
+ String portStr = entry.substring(idx + 1);
|
|
|
+ try {
|
|
|
+ port = Integer.valueOf(portStr);
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ throw new IOException("unable to parse port number for " +
|
|
|
+ "'" + entry + "'", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ // Let's see if we can resolve this prefix to an IP address.
|
|
|
+ // This may fail; one example is with a registered hostname
|
|
|
+ // which is not actually a real DNS name.
|
|
|
+ InetAddress addr = InetAddress.getByName(prefix);
|
|
|
+ ipAddress = addr.getHostAddress();
|
|
|
+ } catch (UnknownHostException e) {
|
|
|
+ LOG.info("When reading " + fileName + ", could not look up " +
|
|
|
+ "IP address for " + prefix + ". We will assume this is a " +
|
|
|
+ "registration name.", e);
|
|
|
+ }
|
|
|
+ return new Entry(prefix, port, ipAddress);
|
|
|
+ }
|
|
|
+
|
|
|
+ public String getIdentifier() {
|
|
|
+ return ipAddress.isEmpty() ? prefix : ipAddress;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Entry(String prefix, int port, String ipAddress) {
|
|
|
+ this.prefix = prefix;
|
|
|
+ this.port = port;
|
|
|
+ this.ipAddress = ipAddress;
|
|
|
+ }
|
|
|
+
|
|
|
+ public String getPrefix() {
|
|
|
+ return prefix;
|
|
|
+ }
|
|
|
+
|
|
|
+ public int getPort() {
|
|
|
+ return port;
|
|
|
+ }
|
|
|
+
|
|
|
+ public String getIpAddress() {
|
|
|
+ return ipAddress;
|
|
|
+ }
|
|
|
+
|
|
|
+ public String toString() {
|
|
|
+ StringBuilder bld = new StringBuilder();
|
|
|
+ bld.append("Entry{").append(prefix).append(", port=").
|
|
|
+ append(port).append(", ipAddress=").append(ipAddress).append("}");
|
|
|
+ return bld.toString();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class EntrySet implements Iterable<Entry> {
|
|
|
+ /**
|
|
|
+ * The index. Each Entry appears in here exactly once.
|
|
|
+ *
|
|
|
+ * It may be indexed by one of:
|
|
|
+ * ipAddress:port
|
|
|
+ * ipAddress
|
|
|
+ * registeredHostname:port
|
|
|
+ * registeredHostname
|
|
|
+ *
|
|
|
+ * The different indexing strategies reflect the fact that we may or may
|
|
|
+ * not have a port or IP address for each entry.
|
|
|
+ */
|
|
|
+ TreeMap<String, Entry> index = new TreeMap<String, Entry>();
|
|
|
+
|
|
|
+ public boolean isEmpty() {
|
|
|
+ return index.isEmpty();
|
|
|
+ }
|
|
|
+
|
|
|
+ public Entry find(DatanodeID datanodeID) {
|
|
|
+ Entry entry;
|
|
|
+ int xferPort = datanodeID.getXferPort();
|
|
|
+ assert(xferPort > 0);
|
|
|
+ String datanodeIpAddr = datanodeID.getIpAddr();
|
|
|
+ if (datanodeIpAddr != null) {
|
|
|
+ entry = index.get(datanodeIpAddr + ":" + xferPort);
|
|
|
+ if (entry != null) {
|
|
|
+ return entry;
|
|
|
+ }
|
|
|
+ entry = index.get(datanodeIpAddr);
|
|
|
+ if (entry != null) {
|
|
|
+ return entry;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ String registeredHostName = datanodeID.getHostName();
|
|
|
+ if (registeredHostName != null) {
|
|
|
+ entry = index.get(registeredHostName + ":" + xferPort);
|
|
|
+ if (entry != null) {
|
|
|
+ return entry;
|
|
|
+ }
|
|
|
+ entry = index.get(registeredHostName);
|
|
|
+ if (entry != null) {
|
|
|
+ return entry;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Entry find(Entry toFind) {
|
|
|
+ int port = toFind.getPort();
|
|
|
+ if (port != 0) {
|
|
|
+ return index.get(toFind.getIdentifier() + ":" + port);
|
|
|
+ } else {
|
|
|
+ // An Entry with no port matches any entry with the same identifer.
|
|
|
+ // In other words, we treat 0 as "any port."
|
|
|
+ Map.Entry<String, Entry> ceil =
|
|
|
+ index.ceilingEntry(toFind.getIdentifier());
|
|
|
+ if ((ceil != null) &&
|
|
|
+ (ceil.getValue().getIdentifier().equals(
|
|
|
+ toFind.getIdentifier()))) {
|
|
|
+ return ceil.getValue();
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public String toString() {
|
|
|
+ StringBuilder bld = new StringBuilder();
|
|
|
+
|
|
|
+ bld.append("HostSet(");
|
|
|
+ for (Map.Entry<String, Entry> entry : index.entrySet()) {
|
|
|
+ bld.append("\n\t");
|
|
|
+ bld.append(entry.getKey()).append("->").
|
|
|
+ append(entry.getValue().toString());
|
|
|
+ }
|
|
|
+ bld.append("\n)");
|
|
|
+ return bld.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Iterator<Entry> iterator() {
|
|
|
+ return index.values().iterator();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class MutableEntrySet extends EntrySet {
|
|
|
+ public void add(DatanodeID datanodeID) {
|
|
|
+ Entry entry = new Entry(datanodeID.getHostName(),
|
|
|
+ datanodeID.getXferPort(), datanodeID.getIpAddr());
|
|
|
+ index.put(datanodeID.getIpAddr() + ":" + datanodeID.getXferPort(),
|
|
|
+ entry);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void add(Entry entry) {
|
|
|
+ int port = entry.getPort();
|
|
|
+ if (port != 0) {
|
|
|
+ index.put(entry.getIdentifier() + ":" + port, entry);
|
|
|
+ } else {
|
|
|
+ index.put(entry.getIdentifier(), entry);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ void readFile(String type, String filename) throws IOException {
|
|
|
+ if (filename.isEmpty()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ HashSet<String> entrySet = new HashSet<String>();
|
|
|
+ HostsFileReader.readFileToSet(type, filename, entrySet);
|
|
|
+ for (String str : entrySet) {
|
|
|
+ Entry entry = Entry.parse(filename, str);
|
|
|
+ add(entry);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private EntrySet includes = new EntrySet();
|
|
|
+ private EntrySet excludes = new EntrySet();
|
|
|
+
|
|
|
+ public HostFileManager() {
|
|
|
+ }
|
|
|
+
|
|
|
+ public void refresh(String includeFile, String excludeFile)
|
|
|
+ throws IOException {
|
|
|
+ MutableEntrySet newIncludes = new MutableEntrySet();
|
|
|
+ IOException includeException = null;
|
|
|
+ try {
|
|
|
+ newIncludes.readFile("included", includeFile);
|
|
|
+ } catch (IOException e) {
|
|
|
+ includeException = e;
|
|
|
+ }
|
|
|
+ MutableEntrySet newExcludes = new MutableEntrySet();
|
|
|
+ IOException excludeException = null;
|
|
|
+ try {
|
|
|
+ newExcludes.readFile("excluded", excludeFile);
|
|
|
+ } catch (IOException e) {
|
|
|
+ excludeException = e;
|
|
|
+ }
|
|
|
+ synchronized(this) {
|
|
|
+ if (includeException == null) {
|
|
|
+ includes = newIncludes;
|
|
|
+ }
|
|
|
+ if (excludeException == null) {
|
|
|
+ excludes = newExcludes;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (includeException == null) {
|
|
|
+ LOG.info("read includes:\n" + newIncludes);
|
|
|
+ } else {
|
|
|
+ LOG.error("failed to read include file '" + includeFile + "'. " +
|
|
|
+ "Continuing to use previous include list.",
|
|
|
+ includeException);
|
|
|
+ }
|
|
|
+ if (excludeException == null) {
|
|
|
+ LOG.info("read excludes:\n" + newExcludes);
|
|
|
+ } else {
|
|
|
+ LOG.error("failed to read exclude file '" + excludeFile + "'." +
|
|
|
+ "Continuing to use previous exclude list.",
|
|
|
+ excludeException);
|
|
|
+ }
|
|
|
+ if (includeException != null) {
|
|
|
+ throw new IOException("error reading hosts file " + includeFile,
|
|
|
+ includeException);
|
|
|
+ }
|
|
|
+ if (excludeException != null) {
|
|
|
+ throw new IOException("error reading exclude file " + excludeFile,
|
|
|
+ excludeException);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public synchronized boolean isIncluded(DatanodeID dn) {
|
|
|
+ if (includes.isEmpty()) {
|
|
|
+ // If the includes list is empty, act as if everything is in the
|
|
|
+ // includes list.
|
|
|
+ return true;
|
|
|
+ } else {
|
|
|
+ return includes.find(dn) != null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public synchronized boolean isExcluded(DatanodeID dn) {
|
|
|
+ return excludes.find(dn) != null;
|
|
|
+ }
|
|
|
+
|
|
|
+ public synchronized boolean hasIncludes() {
|
|
|
+ return !includes.isEmpty();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @return the includes as an immutable set.
|
|
|
+ */
|
|
|
+ public synchronized EntrySet getIncludes() {
|
|
|
+ return includes;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @return the excludes as an immutable set.
|
|
|
+ */
|
|
|
+ public synchronized EntrySet getExcludes() {
|
|
|
+ return excludes;
|
|
|
+ }
|
|
|
+}
|