blob: 3e89f9031389c5b277d8c1b727bbd80c936fe9d3 [file] [log] [blame]
/**
* 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.ozone.container.common.volume;
import javax.annotation.Nullable;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.GetSpaceUsed;
import org.apache.hadoop.fs.StorageType;
import org.apache.hadoop.hdfs.server.datanode.StorageLocation;
import org.apache.hadoop.hdfs.server.datanode.checker.Checkable;
import org.apache.hadoop.hdfs.server.datanode.checker.VolumeCheckResult;
import org.apache.hadoop.ozone.common.InconsistentStorageStateException;
import org.apache.hadoop.ozone.container.common.DataNodeLayoutVersion;
import org.apache.hadoop.ozone.container.common.helpers.DatanodeVersionFile;
import org.apache.hadoop.ozone.container.common.impl.ChunkLayOutVersion;
import org.apache.hadoop.ozone.container.common.utils.HddsVolumeUtil;
import org.apache.hadoop.util.DiskChecker;
import org.apache.hadoop.util.Time;
import org.apache.yetus.audience.InterfaceAudience;
import org.apache.yetus.audience.InterfaceStability;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
/**
* HddsVolume represents volume in a datanode. {@link VolumeSet} maintains a
* list of HddsVolumes, one for each volume in the Datanode.
* {@link VolumeInfo} in encompassed by this class.
* <p>
* The disk layout per volume is as follows:
* <p>../hdds/VERSION
* <p>{@literal ../hdds/<<scmUuid>>/current/<<containerDir>>/<<containerID
* >>/metadata}
* <p>{@literal ../hdds/<<scmUuid>>/current/<<containerDir>>/<<containerID
* >>/<<dataDir>>}
* <p>
* Each hdds volume has its own VERSION file. The hdds volume will have one
* scmUuid directory for each SCM it is a part of (currently only one SCM is
* supported).
*
* During DN startup, if the VERSION file exists, we verify that the
* clusterID in the version file matches the clusterID from SCM.
*/
@InterfaceAudience.Private
@InterfaceStability.Unstable
@SuppressWarnings("finalclass")
public class HddsVolume
implements Checkable<Boolean, VolumeCheckResult> {
private static final Logger LOG = LoggerFactory.getLogger(HddsVolume.class);
public static final String HDDS_VOLUME_DIR = "hdds";
private final File hddsRootDir;
private final VolumeInfo volumeInfo;
private VolumeState state;
private final VolumeIOStats volumeIOStats;
// VERSION file properties
private String storageID; // id of the file system
private String clusterID; // id of the cluster
private String datanodeUuid; // id of the DataNode
private long cTime; // creation time of the file system state
private int layoutVersion; // layout version of the storage data
private final AtomicLong committedBytes; // till Open containers become full
/**
* Run a check on the current volume to determine if it is healthy.
* @param unused context for the check, ignored.
* @return result of checking the volume.
* @throws Exception if an exception was encountered while running
* the volume check.
*/
@Override
public VolumeCheckResult check(@Nullable Boolean unused) throws Exception {
DiskChecker.checkDir(hddsRootDir);
return VolumeCheckResult.HEALTHY;
}
/**
* Builder for HddsVolume.
*/
public static class Builder {
private final String volumeRootStr;
private Configuration conf;
private StorageType storageType;
private long configuredCapacity;
private String datanodeUuid;
private String clusterID;
private boolean failedVolume = false;
public Builder(String rootDirStr) {
this.volumeRootStr = rootDirStr;
}
public Builder conf(Configuration config) {
this.conf = config;
return this;
}
public Builder storageType(StorageType st) {
this.storageType = st;
return this;
}
public Builder configuredCapacity(long capacity) {
this.configuredCapacity = capacity;
return this;
}
public Builder datanodeUuid(String datanodeUUID) {
this.datanodeUuid = datanodeUUID;
return this;
}
public Builder clusterID(String cid) {
this.clusterID = cid;
return this;
}
// This is added just to create failed volume objects, which will be used
// to create failed HddsVolume objects in the case of any exceptions caused
// during creating HddsVolume object.
public Builder failedVolume(boolean failed) {
this.failedVolume = failed;
return this;
}
public HddsVolume build() throws IOException {
return new HddsVolume(this);
}
}
private HddsVolume(Builder b) throws IOException {
if (!b.failedVolume) {
StorageLocation location = StorageLocation.parse(b.volumeRootStr);
hddsRootDir = new File(location.getUri().getPath(), HDDS_VOLUME_DIR);
this.state = VolumeState.NOT_INITIALIZED;
this.clusterID = b.clusterID;
this.datanodeUuid = b.datanodeUuid;
this.volumeIOStats = new VolumeIOStats();
VolumeInfo.Builder volumeBuilder =
new VolumeInfo.Builder(b.volumeRootStr, b.conf)
.storageType(b.storageType)
.configuredCapacity(b.configuredCapacity);
this.volumeInfo = volumeBuilder.build();
this.committedBytes = new AtomicLong(0);
LOG.info("Creating Volume: " + this.hddsRootDir + " of storage type : " +
b.storageType + " and capacity : " + volumeInfo.getCapacity());
initialize();
} else {
// Builder is called with failedVolume set, so create a failed volume
// HddsVolumeObject.
hddsRootDir = new File(b.volumeRootStr);
volumeIOStats = null;
volumeInfo = null;
storageID = UUID.randomUUID().toString();
state = VolumeState.FAILED;
committedBytes = null;
}
}
public VolumeInfo getVolumeInfo() {
return volumeInfo;
}
/**
* Initializes the volume.
* Creates the Version file if not present,
* otherwise returns with IOException.
* @throws IOException
*/
private void initialize() throws IOException {
VolumeState intialVolumeState = analyzeVolumeState();
switch (intialVolumeState) {
case NON_EXISTENT:
// Root directory does not exist. Create it.
if (!hddsRootDir.mkdirs()) {
throw new IOException("Cannot create directory " + hddsRootDir);
}
setState(VolumeState.NOT_FORMATTED);
createVersionFile();
break;
case NOT_FORMATTED:
// Version File does not exist. Create it.
createVersionFile();
break;
case NOT_INITIALIZED:
// Version File exists. Verify its correctness and update property fields.
readVersionFile();
setState(VolumeState.NORMAL);
break;
case INCONSISTENT:
// Volume Root is in an inconsistent state. Skip loading this volume.
throw new IOException("Volume is in an " + VolumeState.INCONSISTENT +
" state. Skipped loading volume: " + hddsRootDir.getPath());
default:
throw new IOException("Unrecognized initial state : " +
intialVolumeState + "of volume : " + hddsRootDir);
}
}
private VolumeState analyzeVolumeState() {
if (!hddsRootDir.exists()) {
// Volume Root does not exist.
return VolumeState.NON_EXISTENT;
}
if (!hddsRootDir.isDirectory()) {
// Volume Root exists but is not a directory.
return VolumeState.INCONSISTENT;
}
File[] files = hddsRootDir.listFiles();
if (files == null || files.length == 0) {
// Volume Root exists and is empty.
return VolumeState.NOT_FORMATTED;
}
if (!getVersionFile().exists()) {
// Volume Root is non empty but VERSION file does not exist.
return VolumeState.INCONSISTENT;
}
// Volume Root and VERSION file exist.
return VolumeState.NOT_INITIALIZED;
}
public void format(String cid) throws IOException {
Preconditions.checkNotNull(cid, "clusterID cannot be null while " +
"formatting Volume");
this.clusterID = cid;
initialize();
}
/**
* Create Version File and write property fields into it.
* @throws IOException
*/
private void createVersionFile() throws IOException {
this.storageID = HddsVolumeUtil.generateUuid();
this.cTime = Time.now();
this.layoutVersion = ChunkLayOutVersion.getLatestVersion().getVersion();
if (this.clusterID == null || datanodeUuid == null) {
// HddsDatanodeService does not have the cluster information yet. Wait
// for registration with SCM.
LOG.debug("ClusterID not available. Cannot format the volume {}",
this.hddsRootDir.getPath());
setState(VolumeState.NOT_FORMATTED);
} else {
// Write the version file to disk.
writeVersionFile();
setState(VolumeState.NORMAL);
}
}
private void writeVersionFile() throws IOException {
Preconditions.checkNotNull(this.storageID,
"StorageID cannot be null in Version File");
Preconditions.checkNotNull(this.clusterID,
"ClusterID cannot be null in Version File");
Preconditions.checkNotNull(this.datanodeUuid,
"DatanodeUUID cannot be null in Version File");
Preconditions.checkArgument(this.cTime > 0,
"Creation Time should be positive");
Preconditions.checkArgument(this.layoutVersion ==
DataNodeLayoutVersion.getLatestVersion().getVersion(),
"Version File should have the latest LayOutVersion");
File versionFile = getVersionFile();
LOG.debug("Writing Version file to disk, {}", versionFile);
DatanodeVersionFile dnVersionFile = new DatanodeVersionFile(this.storageID,
this.clusterID, this.datanodeUuid, this.cTime, this.layoutVersion);
dnVersionFile.createVersionFile(versionFile);
}
/**
* Read Version File and update property fields.
* Get common storage fields.
* Should be overloaded if additional fields need to be read.
*
* @throws IOException on error
*/
private void readVersionFile() throws IOException {
File versionFile = getVersionFile();
Properties props = DatanodeVersionFile.readFrom(versionFile);
if (props.isEmpty()) {
throw new InconsistentStorageStateException(
"Version file " + versionFile + " is missing");
}
LOG.debug("Reading Version file from disk, {}", versionFile);
this.storageID = HddsVolumeUtil.getStorageID(props, versionFile);
this.clusterID = HddsVolumeUtil.getClusterID(props, versionFile,
this.clusterID);
this.datanodeUuid = HddsVolumeUtil.getDatanodeUUID(props, versionFile,
this.datanodeUuid);
this.cTime = HddsVolumeUtil.getCreationTime(props, versionFile);
this.layoutVersion = HddsVolumeUtil.getLayOutVersion(props, versionFile);
}
private File getVersionFile() {
return HddsVolumeUtil.getVersionFile(hddsRootDir);
}
public File getHddsRootDir() {
return hddsRootDir;
}
public StorageType getStorageType() {
if(volumeInfo != null) {
return volumeInfo.getStorageType();
}
return StorageType.DEFAULT;
}
public String getStorageID() {
return storageID;
}
public String getClusterID() {
return clusterID;
}
public String getDatanodeUuid() {
return datanodeUuid;
}
public long getCTime() {
return cTime;
}
public int getLayoutVersion() {
return layoutVersion;
}
public VolumeState getStorageState() {
return state;
}
public long getCapacity() throws IOException {
if(volumeInfo != null) {
return volumeInfo.getCapacity();
}
return 0;
}
public long getAvailable() throws IOException {
if(volumeInfo != null) {
return volumeInfo.getAvailable();
}
return 0;
}
public void setState(VolumeState state) {
this.state = state;
}
public boolean isFailed() {
return (state == VolumeState.FAILED);
}
public VolumeIOStats getVolumeIOStats() {
return volumeIOStats;
}
public void failVolume() {
setState(VolumeState.FAILED);
if (volumeInfo != null) {
volumeInfo.shutdownUsageThread();
}
}
public void shutdown() {
this.state = VolumeState.NON_EXISTENT;
if (volumeInfo != null) {
volumeInfo.shutdownUsageThread();
}
}
/**
* VolumeState represents the different states a HddsVolume can be in.
* NORMAL =&gt; Volume can be used for storage
* FAILED =&gt; Volume has failed due and can no longer be used for
* storing containers.
* NON_EXISTENT =&gt; Volume Root dir does not exist
* INCONSISTENT =&gt; Volume Root dir is not empty but VERSION file is
* missing or Volume Root dir is not a directory
* NOT_FORMATTED =&gt; Volume Root exists but not formatted(no VERSION file)
* NOT_INITIALIZED =&gt; VERSION file exists but has not been verified for
* correctness.
*/
public enum VolumeState {
NORMAL,
FAILED,
NON_EXISTENT,
INCONSISTENT,
NOT_FORMATTED,
NOT_INITIALIZED
}
/**
* add "delta" bytes to committed space in the volume.
* @param delta bytes to add to committed space counter
* @return bytes of committed space
*/
public long incCommittedBytes(long delta) {
return committedBytes.addAndGet(delta);
}
/**
* return the committed space in the volume.
* @return bytes of committed space
*/
public long getCommittedBytes() {
return committedBytes.get();
}
/**
* Only for testing. Do not use otherwise.
*/
@VisibleForTesting
public void setScmUsageForTesting(GetSpaceUsed scmUsageForTest) {
if (volumeInfo != null) {
volumeInfo.setScmUsageForTesting(scmUsageForTest);
}
}
}