001 /** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018 package org.apache.hadoop.hdfs.server.namenode; 019 020 import java.io.*; 021 import java.net.*; 022 import java.security.DigestInputStream; 023 import java.security.MessageDigest; 024 import java.util.ArrayList; 025 import java.util.List; 026 import java.lang.Math; 027 028 import javax.servlet.ServletOutputStream; 029 import javax.servlet.ServletResponse; 030 import javax.servlet.http.HttpServletResponse; 031 032 import org.apache.commons.logging.Log; 033 import org.apache.commons.logging.LogFactory; 034 import org.apache.hadoop.classification.InterfaceAudience; 035 import org.apache.hadoop.conf.Configuration; 036 import org.apache.hadoop.fs.FileUtil; 037 import org.apache.hadoop.http.HttpConfig; 038 import org.apache.hadoop.security.SecurityUtil; 039 import org.apache.hadoop.util.Time; 040 import org.apache.hadoop.hdfs.DFSConfigKeys; 041 import org.apache.hadoop.hdfs.HdfsConfiguration; 042 import org.apache.hadoop.hdfs.protocol.HdfsConstants; 043 import org.apache.hadoop.hdfs.server.common.StorageErrorReporter; 044 import org.apache.hadoop.hdfs.server.common.Storage; 045 import org.apache.hadoop.hdfs.server.common.Storage.StorageDirectory; 046 import org.apache.hadoop.hdfs.server.namenode.NNStorage.NameNodeDirType; 047 import org.apache.hadoop.hdfs.server.protocol.RemoteEditLog; 048 import org.apache.hadoop.hdfs.util.DataTransferThrottler; 049 import org.apache.hadoop.io.MD5Hash; 050 051 import com.google.common.annotations.VisibleForTesting; 052 import com.google.common.collect.Lists; 053 054 055 /** 056 * This class provides fetching a specified file from the NameNode. 057 */ 058 @InterfaceAudience.Private 059 public class TransferFsImage { 060 061 public final static String CONTENT_LENGTH = "Content-Length"; 062 public final static String MD5_HEADER = "X-MD5-Digest"; 063 @VisibleForTesting 064 static int timeout = 0; 065 066 private static final Log LOG = LogFactory.getLog(TransferFsImage.class); 067 068 public static void downloadMostRecentImageToDirectory(String fsName, 069 File dir) throws IOException { 070 String fileId = GetImageServlet.getParamStringForMostRecentImage(); 071 getFileClient(fsName, fileId, Lists.newArrayList(dir), 072 null, false); 073 } 074 075 public static MD5Hash downloadImageToStorage( 076 String fsName, long imageTxId, Storage dstStorage, boolean needDigest) 077 throws IOException { 078 String fileid = GetImageServlet.getParamStringForImage( 079 imageTxId, dstStorage); 080 String fileName = NNStorage.getCheckpointImageFileName(imageTxId); 081 082 List<File> dstFiles = dstStorage.getFiles( 083 NameNodeDirType.IMAGE, fileName); 084 if (dstFiles.isEmpty()) { 085 throw new IOException("No targets in destination storage!"); 086 } 087 088 MD5Hash hash = getFileClient(fsName, fileid, dstFiles, dstStorage, needDigest); 089 LOG.info("Downloaded file " + dstFiles.get(0).getName() + " size " + 090 dstFiles.get(0).length() + " bytes."); 091 return hash; 092 } 093 094 static void downloadEditsToStorage(String fsName, RemoteEditLog log, 095 NNStorage dstStorage) throws IOException { 096 assert log.getStartTxId() > 0 && log.getEndTxId() > 0 : 097 "bad log: " + log; 098 String fileid = GetImageServlet.getParamStringForLog( 099 log, dstStorage); 100 String finalFileName = NNStorage.getFinalizedEditsFileName( 101 log.getStartTxId(), log.getEndTxId()); 102 103 List<File> finalFiles = dstStorage.getFiles(NameNodeDirType.EDITS, 104 finalFileName); 105 assert !finalFiles.isEmpty() : "No checkpoint targets."; 106 107 for (File f : finalFiles) { 108 if (f.exists() && FileUtil.canRead(f)) { 109 LOG.info("Skipping download of remote edit log " + 110 log + " since it already is stored locally at " + f); 111 return; 112 } else if (LOG.isDebugEnabled()) { 113 LOG.debug("Dest file: " + f); 114 } 115 } 116 117 final long milliTime = System.currentTimeMillis(); 118 String tmpFileName = NNStorage.getTemporaryEditsFileName( 119 log.getStartTxId(), log.getEndTxId(), milliTime); 120 List<File> tmpFiles = dstStorage.getFiles(NameNodeDirType.EDITS, 121 tmpFileName); 122 getFileClient(fsName, fileid, tmpFiles, dstStorage, false); 123 LOG.info("Downloaded file " + tmpFiles.get(0).getName() + " size " + 124 finalFiles.get(0).length() + " bytes."); 125 126 CheckpointFaultInjector.getInstance().beforeEditsRename(); 127 128 for (StorageDirectory sd : dstStorage.dirIterable(NameNodeDirType.EDITS)) { 129 File tmpFile = NNStorage.getTemporaryEditsFile(sd, 130 log.getStartTxId(), log.getEndTxId(), milliTime); 131 File finalizedFile = NNStorage.getFinalizedEditsFile(sd, 132 log.getStartTxId(), log.getEndTxId()); 133 if (LOG.isDebugEnabled()) { 134 LOG.debug("Renaming " + tmpFile + " to " + finalizedFile); 135 } 136 boolean success = tmpFile.renameTo(finalizedFile); 137 if (!success) { 138 LOG.warn("Unable to rename edits file from " + tmpFile 139 + " to " + finalizedFile); 140 } 141 } 142 } 143 144 /** 145 * Requests that the NameNode download an image from this node. 146 * 147 * @param fsName the http address for the remote NN 148 * @param imageListenAddress the host/port where the local node is running an 149 * HTTPServer hosting GetImageServlet 150 * @param storage the storage directory to transfer the image from 151 * @param txid the transaction ID of the image to be uploaded 152 */ 153 public static void uploadImageFromStorage(String fsName, 154 InetSocketAddress imageListenAddress, 155 Storage storage, long txid) throws IOException { 156 157 String fileid = GetImageServlet.getParamStringToPutImage( 158 txid, imageListenAddress, storage); 159 // this doesn't directly upload an image, but rather asks the NN 160 // to connect back to the 2NN to download the specified image. 161 try { 162 TransferFsImage.getFileClient(fsName, fileid, null, null, false); 163 } catch (HttpGetFailedException e) { 164 if (e.getResponseCode() == HttpServletResponse.SC_CONFLICT) { 165 // this is OK - this means that a previous attempt to upload 166 // this checkpoint succeeded even though we thought it failed. 167 LOG.info("Image upload with txid " + txid + 168 " conflicted with a previous image upload to the " + 169 "same NameNode. Continuing...", e); 170 return; 171 } else { 172 throw e; 173 } 174 } 175 LOG.info("Uploaded image with txid " + txid + " to namenode at " + 176 fsName); 177 } 178 179 180 /** 181 * A server-side method to respond to a getfile http request 182 * Copies the contents of the local file into the output stream. 183 */ 184 public static void getFileServer(ServletResponse response, File localfile, 185 FileInputStream infile, 186 DataTransferThrottler throttler) 187 throws IOException { 188 byte buf[] = new byte[HdfsConstants.IO_FILE_BUFFER_SIZE]; 189 ServletOutputStream out = null; 190 try { 191 CheckpointFaultInjector.getInstance() 192 .aboutToSendFile(localfile); 193 out = response.getOutputStream(); 194 195 if (CheckpointFaultInjector.getInstance(). 196 shouldSendShortFile(localfile)) { 197 // Test sending image shorter than localfile 198 long len = localfile.length(); 199 buf = new byte[(int)Math.min(len/2, HdfsConstants.IO_FILE_BUFFER_SIZE)]; 200 // This will read at most half of the image 201 // and the rest of the image will be sent over the wire 202 infile.read(buf); 203 } 204 int num = 1; 205 while (num > 0) { 206 num = infile.read(buf); 207 if (num <= 0) { 208 break; 209 } 210 if (CheckpointFaultInjector.getInstance() 211 .shouldCorruptAByte(localfile)) { 212 // Simulate a corrupted byte on the wire 213 LOG.warn("SIMULATING A CORRUPT BYTE IN IMAGE TRANSFER!"); 214 buf[0]++; 215 } 216 217 out.write(buf, 0, num); 218 if (throttler != null) { 219 throttler.throttle(num); 220 } 221 } 222 } finally { 223 if (out != null) { 224 out.close(); 225 } 226 } 227 } 228 229 /** 230 * Client-side Method to fetch file from a server 231 * Copies the response from the URL to a list of local files. 232 * @param dstStorage if an error occurs writing to one of the files, 233 * this storage object will be notified. 234 * @Return a digest of the received file if getChecksum is true 235 */ 236 static MD5Hash getFileClient(String nnHostPort, 237 String queryString, List<File> localPaths, 238 Storage dstStorage, boolean getChecksum) throws IOException { 239 240 String str = HttpConfig.getSchemePrefix() + nnHostPort + "/getimage?" + 241 queryString; 242 LOG.info("Opening connection to " + str); 243 // 244 // open connection to remote server 245 // 246 URL url = new URL(str); 247 return doGetUrl(url, localPaths, dstStorage, getChecksum); 248 } 249 250 public static MD5Hash doGetUrl(URL url, List<File> localPaths, 251 Storage dstStorage, boolean getChecksum) throws IOException { 252 long startTime = Time.monotonicNow(); 253 254 HttpURLConnection connection = (HttpURLConnection) 255 SecurityUtil.openSecureHttpConnection(url); 256 257 if (timeout <= 0) { 258 Configuration conf = new HdfsConfiguration(); 259 timeout = conf.getInt(DFSConfigKeys.DFS_IMAGE_TRANSFER_TIMEOUT_KEY, 260 DFSConfigKeys.DFS_IMAGE_TRANSFER_TIMEOUT_DEFAULT); 261 } 262 263 if (timeout > 0) { 264 connection.setConnectTimeout(timeout); 265 connection.setReadTimeout(timeout); 266 } 267 268 if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { 269 throw new HttpGetFailedException( 270 "Image transfer servlet at " + url + 271 " failed with status code " + connection.getResponseCode() + 272 "\nResponse message:\n" + connection.getResponseMessage(), 273 connection); 274 } 275 276 long advertisedSize; 277 String contentLength = connection.getHeaderField(CONTENT_LENGTH); 278 if (contentLength != null) { 279 advertisedSize = Long.parseLong(contentLength); 280 } else { 281 throw new IOException(CONTENT_LENGTH + " header is not provided " + 282 "by the namenode when trying to fetch " + url); 283 } 284 285 if (localPaths != null) { 286 String fsImageName = connection.getHeaderField( 287 GetImageServlet.HADOOP_IMAGE_EDITS_HEADER); 288 // If the local paths refer to directories, use the server-provided header 289 // as the filename within that directory 290 List<File> newLocalPaths = new ArrayList<File>(); 291 for (File localPath : localPaths) { 292 if (localPath.isDirectory()) { 293 if (fsImageName == null) { 294 throw new IOException("No filename header provided by server"); 295 } 296 newLocalPaths.add(new File(localPath, fsImageName)); 297 } else { 298 newLocalPaths.add(localPath); 299 } 300 } 301 localPaths = newLocalPaths; 302 } 303 304 MD5Hash advertisedDigest = parseMD5Header(connection); 305 306 long received = 0; 307 InputStream stream = connection.getInputStream(); 308 MessageDigest digester = null; 309 if (getChecksum) { 310 digester = MD5Hash.getDigester(); 311 stream = new DigestInputStream(stream, digester); 312 } 313 boolean finishedReceiving = false; 314 315 List<FileOutputStream> outputStreams = Lists.newArrayList(); 316 317 try { 318 if (localPaths != null) { 319 for (File f : localPaths) { 320 try { 321 if (f.exists()) { 322 LOG.warn("Overwriting existing file " + f 323 + " with file downloaded from " + url); 324 } 325 outputStreams.add(new FileOutputStream(f)); 326 } catch (IOException ioe) { 327 LOG.warn("Unable to download file " + f, ioe); 328 // This will be null if we're downloading the fsimage to a file 329 // outside of an NNStorage directory. 330 if (dstStorage != null && 331 (dstStorage instanceof StorageErrorReporter)) { 332 ((StorageErrorReporter)dstStorage).reportErrorOnFile(f); 333 } 334 } 335 } 336 337 if (outputStreams.isEmpty()) { 338 throw new IOException( 339 "Unable to download to any storage directory"); 340 } 341 } 342 343 int num = 1; 344 byte[] buf = new byte[HdfsConstants.IO_FILE_BUFFER_SIZE]; 345 while (num > 0) { 346 num = stream.read(buf); 347 if (num > 0) { 348 received += num; 349 for (FileOutputStream fos : outputStreams) { 350 fos.write(buf, 0, num); 351 } 352 } 353 } 354 finishedReceiving = true; 355 } finally { 356 stream.close(); 357 for (FileOutputStream fos : outputStreams) { 358 fos.getChannel().force(true); 359 fos.close(); 360 } 361 if (finishedReceiving && received != advertisedSize) { 362 // only throw this exception if we think we read all of it on our end 363 // -- otherwise a client-side IOException would be masked by this 364 // exception that makes it look like a server-side problem! 365 throw new IOException("File " + url + " received length " + received + 366 " is not of the advertised size " + 367 advertisedSize); 368 } 369 } 370 double xferSec = Math.max( 371 ((float)(Time.monotonicNow() - startTime)) / 1000.0, 0.001); 372 long xferKb = received / 1024; 373 LOG.info(String.format("Transfer took %.2fs at %.2f KB/s", 374 xferSec, xferKb / xferSec)); 375 376 if (digester != null) { 377 MD5Hash computedDigest = new MD5Hash(digester.digest()); 378 379 if (advertisedDigest != null && 380 !computedDigest.equals(advertisedDigest)) { 381 throw new IOException("File " + url + " computed digest " + 382 computedDigest + " does not match advertised digest " + 383 advertisedDigest); 384 } 385 return computedDigest; 386 } else { 387 return null; 388 } 389 } 390 391 private static MD5Hash parseMD5Header(HttpURLConnection connection) { 392 String header = connection.getHeaderField(MD5_HEADER); 393 return (header != null) ? new MD5Hash(header) : null; 394 } 395 396 public static class HttpGetFailedException extends IOException { 397 private static final long serialVersionUID = 1L; 398 private final int responseCode; 399 400 HttpGetFailedException(String msg, HttpURLConnection connection) throws IOException { 401 super(msg); 402 this.responseCode = connection.getResponseCode(); 403 } 404 405 public int getResponseCode() { 406 return responseCode; 407 } 408 } 409 410 }