HDFS符號連結和硬連結
前言
初看這個標題,可能很多人會心生疑問:符號連結和硬連結是什麼意思?這些概念不是在Linux作業系統下才有的嘛,HDFS目前也有?當然大家可能還會有其他疑問,沒關係,在後面的內容講述中答案會一一揭曉。歸納起來一句話:不管是符號連結還是硬連結,它們本質上都是一種快捷的連結方式。熟悉Linux系統的同學應該都知道在Linux檔案系統下有硬連結和軟連結的概念,而HDFS同樣作為一套檔案系統,它也能支援檔案連結的形式。本文所講述的主題正是HDFS下的檔案連結操作。
相關背景:Linux軟連結和硬連結
在講述HDFS下的檔案連結內容之前,有必要了解一下相關內容:Linux軟連結和硬連結。因為HDFS檔案連結的原理設計基本與此相似。Linux軟連結和硬連結分別代表著2種截然不同的連線形式,它們在作用上也存在部分微妙的差異。
軟連結
軟連結又被稱為符號連結,英文名稱soft link或symbolic link(HDFS內採用的軟連結名稱就是symbolic link,簡稱symlink)。就個人感覺而言,軟連結在實際工作中用得頻率會略高於硬連結。一句話簡單地概括軟連結的作用:
軟連結其實就是檔案的一個快捷方式,軟連結刪除了,其指向的真實檔案並不會受到影響。
所以在實際的工作中,我們一般會把依賴資源包的路徑用軟連結的方式引用,這樣的話資源包的路徑可以一直保持不變,而其所指向的真實資源包可以根據使用場景進行任意變化。
硬連結
硬連結,英文名稱為hard link。它的一個主要目的在於檔案的共享。檔案的硬連結創建出來之後,它具有如下的特點:
硬連結相當於檔案的一個別名。它指向的是一個檔案inode的引用地址,而非軟連結中的檔案路徑指向。所以對於硬連結中的檔案做修改會影響到其所指向的真實檔案,當對硬連結做刪除動作後,如果其所指向的檔案inode當前沒有被外部引用的話,則原檔案會被刪除,否則原檔案不會被刪除。
上面的意思通俗地解釋就是說,當真實檔案或此檔案的硬連結有一個存在的情況下,對檔案執行刪除操作,檔案不會被真正刪除。當只剩下一個檔案inode引用的情況下,刪除操作才能生效。
HDFS符號連結(軟連結)與硬連結概述
瞭解完Linux下的軟連結與硬連結的概念後,我們將進入本文的主題:HDFS下的符號連結與硬連結。
Hadoop社群在HDFS-245(Create symbolic links in HDFS)中優先對符號連結功能進行了實現。符號連結另外一部分的工作在HADOOP-6421(Symbolic links)中,此部分是HADOOP-COMMON模組相關的底層程式碼改動。在硬連結方面,社群目前有相關的JIRA:HDFS-3370(HDFS hardlink),但是社群目前並沒有在跟進,只有初始的設計文件。所以本文準備講述的HDFS硬連結將會是一個設計模型,並未真正實現,這點需要注意。
鑑於目前HDFS的硬連結功能並未真正實現,所以本文的主講內容還是符號連結的功能,硬連結功能將給出大致設計模型。
HDFS符號連結(軟連結)
HDFS符號連結,在HDFS中稱之為symbolic link,在下文的講述中此名稱都將會簡稱為symlink。與Linux檔案系統中的軟連結概念一樣,HDFS符號連結也是相當於給目標檔案新建一個自定義路徑,這個自定義路徑實質指向目標檔案。所以在這裡HDFS符號連結中做的最重要的事情就是路徑的解析。而且這個解析還有可能是巢狀的,符號連結中指向的是另外一個符號連結。下面來學習HDFS符號連結的主要原理實現。
HDFS符號連結原理
HDFS符號連結功能的實現主要分為2個部分:一個是對現有檔案符號連結的新增,另外一個則是符號連結的解析過程。
對現有檔案符號連結的新增在HDFS中的實現就是新增一個新的INode物件在NameNode中,只是這個INode物件是Symlink型別的,叫做INodeSymlink。此物件內部會包含一個實際指向的target地址。
而符號連結的解析過程則會略微複雜一些,HDFS並不是直接在NameNode最終處理方法的地方做解析的,而是在前面一層FileContext類上做解析的,然後將解析好後的路徑再傳到HDFS中做處理。換句話說,客戶端需要通過FileContext上下文物件來操作符號連結相關的操作,如果直接用FileSystem的API來操作的話,會丟擲解析異常的錯誤。
HDFS符號連結核心程式碼實現
此部分我們將通過部分程式碼的跟蹤來學習HDFS符號連結的實現過程。
同樣首先是建立符號連結的過程,我們直接進入到最終的服務端處理方法,FSNamesystem的createSymlink方法。
*// 為目標檔案建立一個符號連結
void createSymlink(String target, String link,
PermissionStatus dirPerms, boolean createParent, boolean logRetryCache)
throws IOException {
if (!FileSystem.areSymlinksEnabled()) {
throw new UnsupportedOperationException("Symlinks not supported");
}
HdfsFileStatus auditStat = null;
writeLock();
try {
checkOperation(OperationCategory.WRITE);
checkNameNodeSafeMode("Cannot create symlink " + link);
// 此處進入建立軟連結實際操作
auditStat = FSDirSymlinkOp.createSymlinkInt(this, target, link, dirPerms,
createParent, logRetryCache);
...
我們繼續進入FSDirSymlinkOp的createSymlinkInt方法,
static HdfsFileStatus createSymlinkInt(
FSNamesystem fsn, String target, final String linkArg,
PermissionStatus dirPerms, boolean createParent, boolean logRetryCache)
throws IOException {
FSDirectory fsd = fsn.getFSDirectory();
String link = linkArg;
// 檢驗符號連結的名稱
if (!DFSUtil.isValidName(link)) {
throw new InvalidPathException("Invalid link name: " + link);
}
if (FSDirectory.isReservedName(target) || target.isEmpty()
|| FSDirectory.isExactReservedName(target)) {
throw new InvalidPathException("Invalid target name: " + target);
}
if (NameNode.stateChangeLog.isDebugEnabled()) {
NameNode.stateChangeLog.debug("DIR* NameSystem.createSymlink: target="
+ target + " link=" + link);
}
FSPermissionChecker pc = fsn.getPermissionChecker();
INodesInPath iip;
fsd.writeLock();
try {
// 解析得到符號連結的路徑物件
iip = fsd.resolvePathForWrite(pc, link, false);
link = iip.getPath();
...
// 將此符號連結物件加入到NameNode元資料中
addSymlink(fsd, link, iip, target, dirPerms, createParent, logRetryCache);
...
我們繼續進入addSymlink方法,
private static INodeSymlink addSymlink(FSDirectory fsd, String path,
INodesInPath iip, String target, PermissionStatus dirPerms,
boolean createParent, boolean logRetryCache) throws IOException {
final long mtime = now();
final INodesInPath parent;
// 獲取目標符號連結的父親物件
if (createParent) {
// 如果需要建立父親物件,則進行建立父目錄操作方法
parent = FSDirMkdirOp.createAncestorDirectories(fsd, iip, dirPerms);
if (parent == null) {
return null;
}
} else {
parent = iip.getParentINodesInPath();
}
final String userName = dirPerms.getUserName();
long id = fsd.allocateNewInodeId();
PermissionStatus perm = new PermissionStatus(
userName, null, FsPermission.getDefault());
// 加入符號連結型別的INode物件到父物件中
INodeSymlink newNode = unprotectedAddSymlink(fsd, parent,
iip.getLastLocalName(), id, target, mtime, mtime, perm);
...
最後呼叫到加入NameNode名稱空間方法,
static INodeSymlink unprotectedAddSymlink(FSDirectory fsd, INodesInPath iip,
byte[] localName, long id, String target, long mtime, long atime,
PermissionStatus perm)
throws UnresolvedLinkException, QuotaExceededException {
assert fsd.hasWriteLock();
// 新建符號連結型別的INode物件
final INodeSymlink symlink = new INodeSymlink(id, null, perm, mtime, atime,
target);
symlink.setLocalName(localName);
// 將符號連結物件加入到最終路徑對應的上一級父目錄下
return fsd.addINode(iip, symlink, perm.getPermission()) != null ?
symlink : null;
}
至此,新增軟連結操作正式完畢,與普通HDFS檔案INode物件的新增過程類似。
接下來我們來看另外一個部分的內容:HDFS符號連結的路徑解析過程。這個過程是本文的一個重點。在沒有學習HDFS符號連結原始碼之前,本人一直以為HDFS把符號連結的解析過程放在了NameNode的處理方法中,但最終結果表明,事實並不是這樣的。
假設我們直接以HDFS檔案的符號連結進行讀寫操作,究竟會發生什麼事情呢?這裡我們以setReplication設定副本數操作為例。
首先通過DFSClient端執行setReplication方法,
*// 為指定的檔案進行副本數的設定,假設我們這裡傳入的src路徑是一個符號連結,而非真實檔案地址
public boolean setReplication(String src, short replication)
throws IOException {
checkOpen();
try (TraceScope ignored = newPathTraceScope("setReplication", src)) {
// 向服務端發起設定副本數操作請求
return namenode.setReplication(src, replication);
} catch (RemoteException re) {
...
}
}
我們直接進入FSNamesystem的相應處理方法,
boolean setReplication(final String src, final short replication)
throws IOException {
boolean success = false;
checkOperation(OperationCategory.WRITE);
writeLock();
try {
checkOperation(OperationCategory.WRITE);
checkNameNodeSafeMode("Cannot set replication for " + src);
// 呼叫真正設定副本數方法
success = FSDirAttrOp.setReplication(dir, blockManager, src, replication);
} catch (AccessControlException e) {
logAuditEvent(false, "setReplication", src);
throw e;
} finally {
writeUnlock();
}
...
return success;
}
這裡進入FSDirAttrOp的setReplication方法,
static boolean setReplication(
FSDirectory fsd, BlockManager bm, String src, final short replication)
throws IOException {
bm.verifyReplication(src, replication, null);
final boolean isFile;
FSPermissionChecker pc = fsd.getPermissionChecker();
fsd.writeLock();
try {
// 解析此路徑得到INode路徑物件
final INodesInPath iip = fsd.resolvePathForWrite(pc, src);
if (fsd.isPermissionEnabled()) {
fsd.checkPathAccess(pc, iip, FsAction.WRITE);
}
// 對此路徑進行副本的設定
final BlockInfo[] blocks = unprotectedSetReplication(fsd, iip,
replication);
...
至少從目前來看,我們還沒看到解析符號連結的操作,我們繼續進入設定副本的進一步操作:unprotectedSetReplication方法。
static BlockInfo[] unprotectedSetReplication(
FSDirectory fsd, INodesInPath iip, short replication)
throws QuotaExceededException, UnresolvedLinkException,
SnapshotAccessControlException, UnsupportedActionException {
assert fsd.hasWriteLock();
final BlockManager bm = fsd.getBlockManager();
// 獲取路徑物件的最後一個節點,也就是最終需要設定副本的檔案INode物件
final INode inode = iip.getLastINode();
if (inode == null || !inode.isFile() || inode.asFile().isStriped()) {
// TODO we do not support replication on stripe layout files yet
return null;
}
// 得到此INodeFile物件
INodeFile file = inode.asFile();
// Make sure the directory has sufficient quotas
short oldBR = file.getPreferredBlockReplication();
long size = file.computeFileSize(true, true);
// Ensure the quota does not exceed
if (oldBR < replication) {
fsd.updateCount(iip, 0L, size, oldBR, replication, true);
}
// 物件檔案物件進行副本數的設定
file.setFileReplication(replication, iip.getLatestSnapshotId());
short targetReplication = (short) Math.max(
replication, file.getPreferredBlockReplication());
...
return file.getBlocks();
}
在此方法中,HDFS直接從路徑物件中取出檔案INode物件,然後對其進行副本數的設定,從中並沒有對符號連結進行解析的操作。我們重新把目光回到之前FSDirAttrOp的setReplication方法,在此方法中的fsd.resolvePathForWrite操作是否會有這樣的解析動作呢,從方法名上看也確實包含了解析的單詞。這裡進入FSDirectory的resolvePathForWrite方法,
INodesInPath resolvePathForWrite(FSPermissionChecker pc, String src)
throws UnresolvedLinkException, FileNotFoundException,
AccessControlException {
// 此處呼叫同名方法,這裡的第三個布林引數代表解析到符號連結的INode物件時是否丟擲異常
return resolvePathForWrite(pc, src, true);
}
此方法中間會呼叫層層方法,最終呼叫INodesInPath類內部的resolve方法。此方法執行過程如下,
static INodesInPath resolve(final INodeDirectory startingDir,
final byte[][] components, final boolean isRaw,
final boolean resolveLink) throws UnresolvedLinkException {
Preconditions.checkArgument(startingDir.compareTo(components[0]) == 0);
INode curNode = startingDir;
int count = 0;
int inodeNum = 0;
INode[] inodes = new INode[components.length];
boolean isSnapshot = false;
int snapshotId = CURRENT_STATE_ID;
while (count < components.length && curNode != null) {
final boolean lastComp = (count == components.length - 1);
inodes[inodeNum++] = curNode;
final boolean isRef = curNode.isReference();
final boolean isDir = curNode.isDirectory();
final INodeDirectory dir = isDir? curNode.asDirectory(): null;
if (!isRef && isDir && dir.isWithSnapshot()) {
...
} else if (isRef && isDir && !lastComp) {
...
}
// 如果當前INode是符號連結型別並且當前需要丟擲符號連結異常或當前不是最後一個INode物件時,
// 都丟擲異常
if (curNode.isSymlink() && (!lastComp || resolveLink)) {
final String path = constructPath(components, 0, components.length);
final String preceding = constructPath(components, 0, count);
final String remainder =
constructPath(components, count + 1, components.length);
// 獲取符號連結地址
final String link = DFSUtil.bytes2String(components[count]);
// 獲取符號連結真實指向地址
final String target = curNode.asSymlink().getSymlinkString();
if (LOG.isDebugEnabled()) {
LOG.debug("UnresolvedPathException " +
" path: " + path + " preceding: " + preceding +
" count: " + count + " link: " + link + " target: " + target +
" remainder: " + remainder);
}
// 丟擲無法解析異常
throw new UnresolvedPathException(path, preceding, remainder, target);
}
...
return new INodesInPath(inodes, components, isRaw, isSnapshot, snapshotId);
}
從上面的操作方法中,我們可以得出兩個結論:
- 第一點,HDFS符號連結的解析邏輯並不是實現在NameNode服務端的處理程式碼中。
- 第二點,NameNode服務端中解析符號連結的目的在於幫助丟擲解析異常資訊,以此告訴使用者當前處理的路徑是一個符號連結地址。
那麼這部分的解析邏輯到底發生在了什麼地方呢?這個答案我們可以從符號連結的單元測試例項中進行尋找,範例程式碼如下(同樣以設定副本數操作為例):
@Test
/** setReplication affects the target not the link */
public void testSetReplication() throws IOException {
Path file = new Path(testBaseDir1(), "file");
Path link = new Path(testBaseDir1(), "linkToFile");
createAndWriteFile(file);
fc.createSymlink(file, link, false);
// 此處通過fc物件進行設定,fc指的是檔案上下文物件
fc.setReplication(link, (short)2);
assertEquals(0, fc.getFileLinkStatus(link).getReplication());
assertEquals(2, fc.getFileStatus(link).getReplication());
assertEquals(2, fc.getFileStatus(file).getReplication());
}
上面符號連結的設定副本操作是通過fc物件進行的,fc物件的意思是檔案上下文,此物件的初始化操作如下:
@BeforeClass
public static void testSetUp() throws Exception {
Configuration conf = new HdfsConfiguration();
conf.setBoolean(DFSConfigKeys.DFS_PERMISSIONS_ENABLED_KEY, true);
conf.set(FsPermission.UMASK_LABEL, "000");
cluster = new MiniDFSCluster(conf, 1, true, null);
// 測試例項初始化操作中,得到檔案上下文物件
fc = FileContext.getFileContext(cluster.getURI());
}
至此,這也就是為什麼我們不能直接用FileSystem物件進行符號連結相關操作執行的原因。
下面我們來聊聊FileContext到底是如何幫助使用者完成符號連結的解析過程的。比如說,現在我已經初始化好了一個FileContext物件,然後呼叫了setReplication操作,接著會觸發到下面的方法,
public boolean setReplication(final Path f, final short replication)
throws AccessControlException, FileNotFoundException,
IOException {
final Path absF = fixRelativePart(f);
// 呼叫解析器物件進行解析
return new FSLinkResolver<Boolean>() {
@Override
public Boolean next(final AbstractFileSystem fs, final Path p)
throws IOException, UnresolvedLinkException {
// next方法執行時,p已經代表了目標真實檔案地址
return fs.setReplication(p, replication);
}
}.resolve(this, absF);
}
所以以上的實現關鍵點在於FSLinkResolver的解析過程。我們進入此類,解析過程如下:
public T resolve(final FileContext fc, final Path path) throws IOException {
int count = 0;
T in = null;
Path p = path;
// 獲取初始檔案系統
AbstractFileSystem fs = fc.getFSofPath(p);
// 進行迴圈解析,直到所有的符號連結地址都被解析成功
for (boolean isLink = true; isLink;) {
try {
// 如果當前的路徑解析成功,則更新標記為false,跳出迴圈,代表當前路徑已解析成功
in = next(fs, p);
isLink = false;
} catch (UnresolvedLinkException e) {
...
// 否則出現符號連結時,解析當前第一層符號連結地址,fs.getLinkTarget(p)會獲取當前符號連結地址所指向的真實地址
p = qualifySymlinkTarget(fs.getUri(), p, fs.getLinkTarget(p));
fs = fc.getFSofPath(p);
// 然後用新的路徑和檔案系統物件進行下一次的操作執行
}
}
return in;
}
fs.getLinkTarget(p)操作方法的實現,大家可以從DistributedFileSystem中檢視其具體實現。
所以,HDFS符號連結的總過程呼叫如下圖所示:
圖 1-1 HDFS符號連結解析過程
HDFS硬連結的原型設計
鑑於前面花了大量的篇幅介紹了HDFS符號連結(軟連結)的內容,最後簡單介紹介紹HDFS硬連結的內容。因為此功能目前在HDFS中並未實現,只有原型設計,所以這裡也將只會介紹社群上對於此功能的一個設計。
HDFS硬連結的一個核心使用點在於它可以在無需拷貝真實資料的情況下,實現資料的共享。為了實現這樣的功能,在HDFS-3370中的設計文件中,設計者引入了INodeHardLinkFile物件來代表當前的一個硬連結。這些物件共享HardLinkFileInfo物件。在HardLinkFileInfo物件中,會保持有當前的引用計數值,表示當前引用此檔案的硬連結數,與Linux作業系統中的硬連結類似。
下面通過圖形展示的方式大致介紹一下HDFS硬連結的原型設計:
首先,假設當前存在一個已存在的檔案File1,如圖1-2。
圖 1-2 HDFS初始檔案
此時對檔案File1建立一個硬連結,同時名稱也為File1不變,File1的INodeFile物件將會轉化為INodeHardLinkFile物件,同時引用計數為1,此過程見圖1-3。
圖 1-3 HDFS硬連結的建立
在此基礎上,如果還要對此檔案進行硬連結的建立,即為INodeHardLinkFile的再次建立,但是引用的HarLinkFileInfo還是同一個物件,HarLinkFileInfo引用計數此時遞增 2,此過程見圖1-4。
圖 1-4 HDFS硬連結的再建立
這個於Linux硬連結中的檔案inode引用原理是完全一致的。當個別檔案硬連結物件的刪除將不會刪除其真實的資料,除非硬連結檔案當前沒有其他的引用物件了。
此設計文件的作者是來自於Facebook的某位工程師,本人估測此功能在Facebook內部已經早已實現,HDFS硬連結更多設計細節,可以閱讀參考資料中的連結地址。