/**
* 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.*;
import java.net.*;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.List;
import java.lang.Math;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hdfs.protocol.FSConstants;
import org.apache.hadoop.hdfs.server.common.Storage;
import org.apache.hadoop.hdfs.server.common.StorageErrorReporter;
import org.apache.hadoop.hdfs.server.namenode.NNStorage.NameNodeDirType;
import org.apache.hadoop.hdfs.server.protocol.RemoteEditLog;
import org.apache.hadoop.hdfs.util.InjectionEvent;
import org.apache.hadoop.io.MD5Hash;
import org.apache.hadoop.util.DataTransferThrottler;
import org.apache.hadoop.util.FlushableLogger;
import org.apache.hadoop.util.InjectionHandler;
/**
* This class provides fetching a specified file from the NameNode.
*/
public class TransferFsImage implements FSConstants{
// Image transfer timeout
public static final String DFS_IMAGE_TRANSFER_TIMEOUT_KEY = "dfs.image.transfer.timeout";
public static final int DFS_IMAGE_TRANSFER_TIMEOUT_DEFAULT = 60 * 1000;
public final static String CONTENT_LENGTH = "Content-Length";
public final static String MD5_HEADER = "X-MD5-Digest";
private static final Log LOG = LogFactory.getLog(TransferFsImage.class);
// immediate flush logger
private static final Log FLOG = FlushableLogger.getLogger(LOG);
private static final int BUFFER_SIZE = new Configuration().getInt(
"image.transfer.buffer.size", 4 * 1024 * 1024);
/**
* Download image to local storage with throttling.
*/
static MD5Hash downloadImageToStorage(String fsName, long imageTxId,
FSImage fsImage, boolean needDigest) throws IOException {
// by default do not disable throttling
return downloadImageToStorage(fsName, imageTxId, fsImage, needDigest,
false);
}
/**
* Download image to local storage.
* Throttling can be disabled.
*/
static MD5Hash downloadImageToStorage(String fsName, long imageTxId,
FSImage dstImage, boolean needDigest, boolean disableThrottle)
throws IOException {
String fileid = GetImageServlet.getParamStringForImage(
imageTxId, dstImage.storage, disableThrottle);
List<OutputStream> outputStreams = dstImage.getCheckpointImageOutputStreams(imageTxId);
if (outputStreams.size() == 0) {
throw new IOException("No targets in destination storage!");
}
MD5Hash hash = getFileClient(fsName, fileid, outputStreams, dstImage.storage, needDigest);
LOG.info("Downloaded image files for txid: " + imageTxId);
return hash;
}
static void downloadEditsToStorage(String fsName, RemoteEditLog log,
NNStorage dstStorage) throws IOException {
// by default do not disable throttling
downloadEditsToStorage(fsName, log, dstStorage, false);
}
static void downloadEditsToStorage(String fsName, RemoteEditLog log,
NNStorage dstStorage, boolean disableThrottle) throws IOException {
assert log.getStartTxId() > 0 && log.getEndTxId() > 0 :
"bad log: " + log;
String fileid = GetImageServlet.getParamStringForLog(
log, dstStorage, disableThrottle);
String fileName = NNStorage.getFinalizedEditsFileName(
log.getStartTxId(), log.getEndTxId());
File[] dstFiles = dstStorage.getFiles(NameNodeDirType.EDITS, fileName);
assert !(dstFiles.length == 0) : "No checkpoint targets.";
for (File f : dstFiles) {
if (f.exists() && f.canRead()) {
LOG.info("Skipping download of remote edit log " +
log + " since it already is stored locally at " + f);
return;
} else {
LOG.debug("Dest file: " + f);
}
}
List<OutputStream> outputStreams = ImageSet.convertFilesToStreams(dstFiles,
dstStorage, fsName + fileid);
getFileClient(fsName, fileid, outputStreams, dstStorage, false);
LOG.info("Downloaded file " + dstFiles[0].getName() + " size " +
dstFiles[0].length() + " bytes.");
}
/**
* Requests that the NameNode download an image from this node.
*
* @param fsName the http address for the remote NN
* @param imageListenAddress the host/port where the local node is running an
* HTTPServer hosting GetImageServlet
* @param storage the storage directory to transfer the image from
* @param txid the transaction ID of the image to be uploaded
*/
static void uploadImageFromStorage(String fsName,
String machine, int port,
NNStorage storage, long txid) throws IOException {
String fileid = GetImageServlet.getParamStringToPutImage(
txid, machine, port, storage);
LOG.info("Image upload: Posted URL " + fsName + fileid);
// this doesn't directly upload an image, but rather asks the NN
// to connect back to the 2NN to download the specified image.
TransferFsImage.getFileClient(fsName, fileid, null, storage, false);
LOG.info("Uploaded image with txid " + txid + " to namenode at " +
fsName);
}
/**
* A server-side method to respond to a getfile http request
* Copies the contents of the local file into the output stream.
*/
public static void getFileServer(OutputStream outstream, File localfile,
DataTransferThrottler throttler)
throws IOException {
byte buf[] = new byte[BUFFER_SIZE];
InputStream infile = null;
try {
infile = new BufferedInputStream(new FileInputStream(localfile), BUFFER_SIZE);
if (InjectionHandler.falseCondition(InjectionEvent.TRANSFERFSIMAGE_GETFILESERVER0)
&& localfile.getAbsolutePath().contains("secondary")) {
// throw exception only when the secondary sends its image
throw new IOException("If this exception is not caught by the " +
"name-node fs image will be truncated.");
}
if (InjectionHandler.falseCondition(InjectionEvent.TRANSFERFSIMAGE_GETFILESERVER1)
&& localfile.getAbsolutePath().contains("fsimage")) {
// Test sending image shorter than localfile
long len = localfile.length();
buf = new byte[(int)Math.min(len/2, BUFFER_SIZE)];
// This will read at most half of the image
// and the rest of the image will be sent over the wire
infile.read(buf);
}
int num = 1;
while (num > 0) {
num = infile.read(buf);
if (num <= 0) {
break;
}
if (InjectionHandler.falseCondition(InjectionEvent.TRANSFERFSIMAGE_GETFILESERVER2)) {
// Simulate a corrupted byte on the wire
LOG.warn("SIMULATING A CORRUPT BYTE IN IMAGE TRANSFER!");
buf[0]++;
}
InjectionHandler.processEvent(InjectionEvent.TRANSFERFSIMAGE_GETFILESERVER3);
outstream.write(buf, 0, num);
if (throttler != null) {
throttler.throttle(num);
}
}
} finally {
if (infile != null) {
infile.close();
}
}
}
/**
* A server-side method to respond to a getfile http request
* Copies the contents of the local file into the output stream,
* starting at the given position, sending lengthToSend bytes.
*/
public static void getFileServerForPartialFiles(OutputStream outstream,
String filename, InputStream infile, DataTransferThrottler throttler,
long startPosition, long lengthToSend) throws IOException {
byte buf[] = new byte[BUFFER_SIZE];
try {
int num = 1;
while (num > 0) {
num = infile.read(buf, 0,
Math.min(BUFFER_SIZE,
(int) Math.min(lengthToSend, Integer.MAX_VALUE)));
lengthToSend -= num;
if (num <= 0) {
break;
}
try {
outstream.write(buf, 0, num);
} catch (Exception e) {
// silently ignore. connection might have been closed
break;
}
if (throttler != null) {
throttler.throttle(num);
}
}
if (lengthToSend > 0) {
LOG.warn("Could not serve requested number of bytes. Left with "
+ lengthToSend + " bytes for file: " + filename);
}
} finally {
if (infile != null) {
infile.close();
}
}
}
/**
* Get connection and read timeout.
*/
private static int getHttpTimeout(Storage st) {
if (!(st instanceof NNStorage))
return DFS_IMAGE_TRANSFER_TIMEOUT_DEFAULT;
NNStorage storage = (NNStorage) st;
if (storage == null || storage.getConf() == null) {
return DFS_IMAGE_TRANSFER_TIMEOUT_DEFAULT;
}
return storage.getConf().getInt(DFS_IMAGE_TRANSFER_TIMEOUT_KEY,
DFS_IMAGE_TRANSFER_TIMEOUT_DEFAULT);
}
/**
* Client-side Method to fetch file from a server
* Copies the response from the URL to a list of local files.
* @param dstStorage if an error occurs writing to one of the files,
* this storage object will be notified.
* @Return a digest of the received file if getChecksum is true
*/
static MD5Hash getFileClient(String nnHostPort,
String queryString, List<OutputStream> outputStreams,
Storage dstStorage, boolean getChecksum) throws IOException {
String proto = "http://";
StringBuilder str = new StringBuilder(proto+nnHostPort+"/getimage?");
str.append(queryString);
LOG.info("Opening connection to " + str);
//
// open connection to remote server
//
URL url = new URL(str.toString());
return doGetUrl(url, outputStreams, dstStorage, getChecksum);
}
/**
* Client-side Method to fetch file from a server
* Copies the response from the URL to a list of local files.
* @param dstStorage if an error occurs writing to one of the files,
* this storage object will be notified.
* @Return a digest of the received file if getChecksum is true
*/
public static MD5Hash doGetUrl(URL url, List<OutputStream> outputStreams,
Storage dstStorage, boolean getChecksum) throws IOException {
byte[] buf = new byte[BUFFER_SIZE];
String str = url.toString();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
int timeout = getHttpTimeout(dstStorage);
connection.setConnectTimeout(timeout);
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new IOException(
"Image transfer servlet at " + url +
" failed with status code " + connection.getResponseCode() +
"\nResponse message:\n" + connection.getResponseMessage());
}
long advertisedSize;
String contentLength = connection.getHeaderField(CONTENT_LENGTH);
if (contentLength != null) {
advertisedSize = Long.parseLong(contentLength);
} else {
throw new IOException(CONTENT_LENGTH + " header is not provided " +
"by the namenode when trying to fetch " + str);
}
MD5Hash advertisedDigest = parseMD5Header(connection);
long received = 0;
InputStream stream = connection.getInputStream();
MessageDigest digester = null;
if (getChecksum) {
digester = MD5Hash.getDigester();
stream = new DigestInputStream(stream, digester);
}
boolean finishedReceiving = false;
MD5Hash computedDigest = null;
try {
int num = 1;
int lastPrintedProgress = 0;
while (num > 0) {
num = stream.read(buf);
if (num > 0) {
received += num;
for (OutputStream fos : outputStreams) {
fos.write(buf, 0, num);
}
lastPrintedProgress = printProgress(str, received, advertisedSize,
lastPrintedProgress);
}
}
if(digester != null)
computedDigest = new MD5Hash(digester.digest());
finishedReceiving = true;
} finally {
stream.close();
if (outputStreams != null) {
for (OutputStream os : outputStreams) {
if (os instanceof FileOutputStream)
((FileOutputStream)os).getChannel().force(true);
os.close();
}
}
if (finishedReceiving && received != advertisedSize) {
// only throw this exception if we think we read all of it on our end
// -- otherwise a client-side IOException would be masked by this
// exception that makes it look like a server-side problem!
throw new IOException("File " + str + " received length " + received +
" is not of the advertised size " +
advertisedSize);
}
}
if (computedDigest != null) {
if (advertisedDigest != null &&
!computedDigest.equals(advertisedDigest)) {
throw new IOException("File " + str + " computed digest " +
computedDigest + " does not match advertised digest " +
advertisedDigest);
}
return computedDigest;
} else {
return null;
}
}
/**
* Print progress when downloading files.
*/
private static int printProgress(String str, long received,
long advertisedSize, int lastPrinted) {
if (advertisedSize == 0)
return 0;
int currentPercent = (int) ((received * 100) / advertisedSize);
if (currentPercent != lastPrinted)
FLOG.info("Downloading: " + str + ", completed: " + currentPercent);
return currentPercent;
}
public static MD5Hash parseMD5Header(HttpURLConnection connection) {
String header = connection.getHeaderField(MD5_HEADER);
MD5Hash hash = (header != null) ? new MD5Hash(header) : null;
return hash;
}
}