【概述】
HDFS客户端在写文件之前需要先获得租约,该租约充当文件的锁,以防止多个客户端对该文件的同时写入。
只要HDFS客户端持有文件的租约,就不允许其他客户端写入该文件。
如果客户端没有在预定义的时间内续订租约,则租约到期,这个时候HDFS服务端将关闭并释放租约,以便其他客户端的写入操作可以获取租约,这个过程也称之为租约恢复。
【租约】
在HDFS内部,租约实现为一个类(Lease),在该类中主要包括这么几个成员
holder:租约持有者(也就是HDFS客户端)
lastUpdate:租约最后一次更新时间
files:该租约持有者打开的文件集合
上面提到了租约的最后更新时间,那就不提到另一个概念:租约的软限制和硬限制。
租约被客户端持有后,并不是就一直归该客户端拥有,客户端必须不断的续约才能持续的持有。
如果超过一段时间没有续约,HDFS允许其他客户端抢占租约并对文件进行操作,租约的软限制和硬限制指的就是这个超时时间。
当前时间减去租约的最后更新时间超过软限制,允许其他HDFS客户端抢占该租约当前持有者打开的文件(默认1分钟)。
当前时间减去租约的最后更新超过硬限制,租约管理线程会强制该租约回收销毁(默认1小时)。
租约 和 HDFS客户端的对应关系为一对一,即:在HDFS服务端,为每个客户端建立一个租约。
【租约的管理】
有租约自然就有租约管理,在HDFS中,LeaseManager就是租约管理的实现类。
在LeaseManager内部,维护客户端和租约的映射关系;维护了文件和租约的映射关系;同时启动独立线程对租约进行生命周期和状态的管理。
具体包括:
- 创建租约或正常情况下的销毁租约
- 赋予文件权限给租约(撤销FilePath,如执行文件流的关闭方法)
- 接收续约请求,对租约进行续约处理
- 对硬超时的租约进行销毁处理
【FSNamesystem】
LeaseManager用来管理租约,那么,FSNamesystem用来协调租约。
FSNamesystem中和租约相关最核心的一个方法是recoverLeaseInternal。
创建文件(调用startFile)、追加写文件(调用appendFile)和租约恢复(调用recoverLease)都会调用该方法,该方法主要功能有:
- 验证ReCreate
如果待操作的文件已经存在于该客户端租约的文件集合中,
则抛AlreadyBeingCreatedException,并提示 "current leaseholder is trying to recreate file"
- 验证OtherCreate
如果待操作的文件已经在其他客户端租约的文件集合中,此时有两种策略:
如果该文件持有者的租约已经超过软限制,系统会尝试进行Lease-Recovery,然后把文件从那个持有者的租约中移出,这样,新的客户端便可以获取该文件的租约并进行操作。
如果持有者的租约还未超过软限制
则抛出AlreadyBeingCreatedException,提示"because this file is already being created by..."
- 验证Recovery
如果待操作的文件还处于租约的Recovery状态
则抛异常RecoveryInProgressException,提示稍后重试
- ForceRecovery
recoveryLeaseInternal方法提供了force参数,如果force为true,系统会强制进行Lease-Recovery
【实战】
实际对相关内容进行测试验证,基本都符合上面的总结描述,但也出现了一个奇怪的现象。
测试的具体流程为
1)在同一个客户端中,同时打开FileA,FileB,FileC 三个文件,并分别写入数据。
2)不关闭文件,直接退出程序。
3)再次启动程序,并依次追加写FileA,FileB,FileC 三个文件。
按照上面的分析,等1分钟(超过租约的软限制)后, 三个文件应该都可以正常写入。
而实际上,FileA正常可写入等了1分钟,FileB正常可写入等了2分钟,FileC正常可写入等了3分钟。
为什么会出现这种情况呢?
按照逻辑,三个文件的租约持有者是同一个客户端,一旦超过软限制,应该都进行租约恢复,允许被后面的客户端抢占租约可写才对啊
通过源码分析,以及HDFS的日志,最后发现:
HDFS在进行租约恢复的时候,内部对文件租约的原来持有者进行最后时间的更新,这样就导致每个文件都需要再多等1分钟
相关源码:
代码语言:javascript复制boolean recoverLeaseInternal(...) {
...
if (lease.expiredSoftLimit()) {
...
// 注意, 第4个参数为NULL !!
if (internalReleaseLease(lease, src, iip, null)) {
}
}
...
}
boolean internalReleaseLease(
Lease lease,
String src,
INodesInPath iip,
String recoveryLeaseHolder) throws IOException {
...
// 从lease中删除该文件, 并为该文件添加新的租约(新的持有者)
// 但是如果 recoveryLeaseHolder 为 null, 则直接返回lease
lease = reassignLease(lease, src, recoveryLeaseHolder, pendingFile);
if(copyOnTruncate) {
lastBlock.setGenerationStamp(blockRecoveryId);
} else if(truncateRecovery) {
recoveryBlock.setGenerationStamp(blockRecoveryId);
}
uc.initializeBlockRecovery(lastBlock, blockRecoveryId, true);
// 更新租约的lastUpdate
leaseManager.renewLease(lease);
...
}
private Lease reassignLease(
Lease lease,
String src,
String newHolder,
INodeFile pendingFile) {
assert hasWriteLock();
if(newHolder == null)
return lease;
// The following transaction is not synced. Make sure it's sync'ed later.
logReassignLease(lease.getHolder(), src, newHolder);
return reassignLeaseInternal(lease, newHolder, pendingFile);
}
解决办法:
想到的一种可行的办法是避免同一个客户端同时打开多个文件进行写操作,也就是 对每个文件都采用独立的一个客户端进行写操作,这样异常后,每个文件最多等1分钟可写。