转自:http://bigdatadecode.club/HDFS%E7%A7%9F%E7%BA%A6%E8%A7%A3%E6%9E%90.html
租约(Lease)是一种广泛应用与分布式系统领域的协议,主要用来维护分布式系统的一致性。
租约是在解决缓存一致性时被提出的。所谓租约,其实就是一个合同,即服务器给予客户端在一定期限内可以控制修改操作的权力。如果服务器要修改数据,首先要征求拥有这块数据的租约的客户端的同意,之后才可以修改。客户端从服务器读取数据时往往就同时获取租约,在租约期限内,如果没有收到服务器的修改请求,就可以保证当前缓存中的内容就是最新的。如果在租约期限内收到了修改数据的请求并且同意了,就需要清空缓存。在租约过 期以后,客户端如果还要从缓存读取数据,就必须重新获取租约,我们称这个操作为续约。
租约特性
租约的一个重要的属性就是期限,一般情况下,应当选择较短的租约期限。与长租约相比,短租约有三个优点。首先,在失效情况下修改操作往往需要等待租约过期,因此短租约就意味着更短的失效延迟 。其次,就算一个客户端已经不再需要读取数据,但在其租约过期前,任何的修改操作仍然需要征求它的同意,这种情况叫做“假共享”,显然租约期限越长,这个问题就越严重。最后,短租约也使得服务器要维护的客户端信息更少。然而短租约也意味着更大的续约开销,因此对于要反复读取却很少修改的数据,长租约会更有效。因此,对租约期的选择要权衡失效延迟、假共享开销和续约开销等多个因素,服务器可以根据数据访问特性和客户端的性质灵活设置期限。事实上,如果我们把租约期限设为零,就相当于轮询,此时修改操作随时可以进行,而读取数据总是要联系服务器。如果把租约期 限设为无限长,就相当于回调。
除了期限的选择,还有很多管理选项。对客户端来说,可以选择是否续约、何时续约以及是否同意修改等。比如为了减少读取延迟,客户端可以在租约过期前就续约,不过这样可能加重服务器的负担。对服务器来说,可以选择是否发放租约、租约覆盖粒度以及对如何进行修改操作。比如在收到修改请求后,服务器可以不征求客户端同意,而是简单的等待所有租约过期(等待时不再发放新租约以避免无限期的延迟)。对于“安装文件”,也就是修改极少的文件(比如头文件、库文件),服务器可以用一个租约来覆盖一批文件,同时定期广播续约通知来节省开销,如果需要修改数据,就停止广播并等待租约过期即可。
在很多时候,租约的定义似乎很模糊,有的时候租约类似心跳,有的时候又类似于锁(读锁和写锁)。到底租约的本质是什么呢?
回到租约最原始的定义:租约就是在一定期限内给予持有者特定权力的协议。我觉得这里的期限就是租约的根本特性,正是这一特性使得租约可以容忍机器失效和网络分割。在期限之内,租约其实就是服务器和客户端之间的协议,而这个协议的内容可以五花八门。如果协议内容是服务器确认客户端还存活,那么这个租约的功能就相当于心跳;如果协议内容是服务器保证内容不会被修改,那么这个租约就相当于读锁;如果协议内容是服务器保证内容只能被这个客户端修改,那么这个租约就相当于写锁。租约这种灵活性和容错性,使其成为了维护分布式系统一致性的有效工具。
租约在HDFS中的应用–写锁
hdfs支持write-once-read-many,也就是说不支持并行写,那么对读写的互斥同步就是靠Lease实现的。Lease说白了就是一个有时间约束的锁。客户端写文件时需要先申请一个Lease,持有该租约的客户端才可以对相应的文件进行块的添加。
与租约相关的类有:
Server端:
LeaseManager – 管理写文件相关的租约
LeaseManager.Monitor – 监控租约是否过期(主要检查hardLimit)
LeaseManager.Lease – 租约实体类,管理某个客户端持有的所以写锁
Client端:
LeaseRenewer – 客户端续约更新类
下面先简单介绍下各类的内部结构
Lease
Lease是LeaseManager的内部类,其实例对应一个租约,租约中包含持有者信息、租约期限和该租约对应的文件。
|
|
一个客户端对应一个租约,一个客户端可以同时写很多个文件,这些文件放在paths
中,租约维护着这些文件的写权限,并对这些文件统一续约,并不是对某个文件单独续约,不需要对某个文件进行操作之后直接从paths
中移除,如果paths
为null,则回收此租约。
LeaseManager
LeaseManager是租约管理类,其内部主要维护了3个集合列表(leases、sortedLeases和sortedLeasesByPath)和两个变量(softLimit和hardLimit)
|
|
在softLimit期限内,该客户端拥有对这个文件的独立访问权,其他客户端不能剥夺该客户端独占写这个文件的权利。
softLimit过期后,任何一个客户端都可以回收lease,继而得到这个文件的lease,获得对这个文件的独占访问权。
hardLimit过期后,namenode强制关闭文件,撤销lease。
sortedLeases中存放这从nn发出的所有租约,其中Lease按照时间顺序排序,Monitor检查hardLimit时,从sortedLeases中按照顺序拿出Lease检查就可以了。
Monitor
Monitor是一个Runnable类,主要用来检测Lease是否超过了hardLimit期限。在run中调用LeaseManager.checkLeases方法进行检测。其周期性是(2s)
LeaseRenewer
LeaseRenewer是client端更新自己租约。其中有个线程检测租约的softLimit期限,其周期性(1s)的调用LeaseRenewer.run()方法对租约过半的lease进行续约。
LeaseRenewer是一个单例,并通过工厂来实例化。这里的单例是一个user一个LeaseRenewer,但是服务器端是一个DFSClient对应一个lease,一个user可能会实例化多个DFSClient,则LeaseRenewer会有个list属性来存储多个DFSClient,这个list就是dfsclients。
|
|
租约 – 写锁流程
新增写租约
充当写锁的租约是client发起写请求时,一起跟nn申请的(其具体的写操作流程请看之前的文章HDFS write解析),client向nn申请写操作的流程为:
FileSystem.create() --> DistributedFileSystem.create() --> FileSystemLinkResolver.resolve() --> doCall() --> dfs.create() --> DFSOutputStream.newStreamForCreate() --> dfsClient.namenode.create() --> namesystem.startFile() -> startFileInt() -> startFileInternal()
。在startFileInternal中通过newNode = dir.addFile(src, permissions, replication, blockSize, holder, clientMachine);
向namenode中添加一个文件,并将clientname对src的租约进行存储leaseManager.addLease(newNode.getFileUnderConstructionFeature().getClientName(), src);
,这里addLease就是client申请租约,看下代码逻辑:
|
|
在nn端一个Lease对应一个DFSClient(DFSClient是由ugi构造的,不是指hadoop集群的client那台机器),Lease是由holder标识的,holder的值就是DFSClient.clientName,clientName在DFSClient的构造函数中初始化,代码如下:
|
|
clientName是由taskId、随机数和currentThread.Id拼起来的,所以每次写请求的clientName是不一样的,则Lease也是不一样的。
addLease的逻辑是先从LeaseManager.leases(holder和lease映射)中查找是否存在holder对应的lease,不存在则由LeaseManager创建一个lease,存在则更新lease。LeaseManager通过实例化Lease类来创建租约,Lease的构造方法如下:
|
|
new出lease后,将其放入LeaseManager中的三个集合中,并把此租约对应的path放入lease的paths中。
租约添加完成。
客户端续约
客户端在dfs.create()中调用beginFileLease()对租约进行续约。
|
|
客户端续约是通过LeaseRenewer来实现的,LeaseRenewer是由存放namenode信息的authority和user信息的ugi来实例化的。
|
|
LeaseRenewer的实例化是通过Factory实例化的,Factory先去renewers中查找是否有当前user的LeaseRenewer,没有则new一个,有则直接返回已有的LeaseRenewer,然后在getInstance中,将DFSClient的实例dfsc放入LeaseRenewer的dfsclients的list中。user对应的LeaseRenewer对象初始化完毕。
然后调用put方法将文件标识Id、对应的文件流和DFSClient实例传入LeaseRenewer中,
|
|
在put中有个守护线程,在守护线程中调用LeaseRenewer.run
方法对租约进行check然后renew,这里check的是softLimit。守护线程只有在daemon为null或者dfsclients为空的时间超过了gracePeriod时才需要重新new一个daemon线程。
|
|
run中调用renew()进行续约,这里续约是对当前user的所有DFSClient(也就是当前user的所有Lease)进行续约。
|
|
在renew中,先对dfsclients中的DFSClient进行排序,主要是为了将重复发clientName放在一起,renew时只对其中一个clientName进行更新,调用c.renewLease进行续约
|
|
在renewLease中远程调用LeaseManager.renewLease,其调用流程为NameNodeRpcServer.renewLease --> FSNamesystem.renewLease --> LeaseManager.renewLease(holder)
,
|
|
客户端通过LeaseRenewer调用LeaseManager.renewLease进行续约,续约逻辑是先从leases中get到clientName对应的lease,然后从sortedLeases中移除该lease,调用lease.renew对lease的lastUpdate进行更新,最后将lease再放入sortedLeases中。sortedLeases中的lease是按照lease的lastUpdate进行排序的,到此客户端续约的流程结束。
nn端周期性check lease
nn端由LeaseManager.Monitor周期性check lease是否超期,这里check hardLimit。Monitor的逻辑比较简单,主要逻辑是在其run方法中。Monitor是一个Runnable类,在active nn启动的时候调用leaseManager.startMonitor()
启动一个Monitor的守护线程。其run方法如下:
|
|
run中调用checkLeases进行hardLimit检查
|
|
释放租约or租约回收or租约恢复
对于超过hardLimit的租约进行释放,对于租约的释放不能简单的remove掉,逻辑比较复杂,有的需要block恢复,其具体实现方法是internalReleaseLease
|
|
此方法主要是检查是否需要真正的进入block recovery阶段,这个阶段需要datanode的参与。下面函数的主要逻辑
- 先检查此file的block是否都是completed状态,如果都是completed,则直接调用finalizeINodeFileUnderConstruction(file正常关闭的逻辑,在下一节的租约关闭中会介绍)关闭file,return true。如果有未completed的,则执行第二步
- 判断未completed的block的索引。如果存在未完成的block,则此block只能是最后一个block或者倒数第二个block,当未完成的block是倒数第二个block时,倒数第二个block的状态必须是COMMITTED,符合此条件执行第三步。如果不是这两种情况,即存在别的block未完成,则抛出异常,在checkLeases中捕获。
- 在switch中判断最后一个block的状态,如果是
COMMITTED
,并且该文件的最后两个block都满足最小副本数要求,则调用finalizeINodeFileUnderConstruction关闭文件,return true。否则抛出异常。如果是UNDER_CONSTRUCTION
或者UNDER_RECOVERY
,并且最后一个block没有任何datanode汇报上来,很有可能是pipeline还没建立起来,客户端就宕机了,这种情况下,只需要把最后一个block从INode中移出,并且关闭文件。否则的话进入block recovery阶段(这一阶段再次处不展开,以后再分析)。
关闭租约
文件写完之后调用FSDataOutputStream.close()
关闭写入流,FSDataOutputStream.close()的具体实现是由其几个子类实现的,这里看下DFSOutputStream.close()
,close中调用completeFile --> NameNodeRpcServer.complete --> FSNamesystem.completeFile --> completeFileInternal --> finalizeINodeFileUnderConstruction --> leaseManager.removeLease
,在finalizeINodeFileUnderConstruction中调用leaseManager.removeLease关闭租约,
|
|
关闭租约的逻辑比较简单,只是关闭租约时并不是简单的把该租约从各个集合中移除,而是只是将关闭src的记录从各个集合中移除,如果租约lease的paths中的src记录都被移除掉,则该租约就可以关闭。
总结
正常情况下,客户端向集群写文件前需要向NameNode的LeaseManager申请Lease;写文件过程中定期更新Lease时间,以防Lease过期,周期与softLimit相关;写完数据后申请释放Lease。
整个过程可能发生两类问题:(1)写文件过程中客户端没有及时更新Lease时间;(2)写完文件后没有成功释放Lease。两个问题分别对应为softLimit和hardLimit。两种场景都会触发LeaseManager对Lease超时强制回收。如果客户端写文件过程中没有及时更新Lease超过softLimit时间后,另一客户端尝试对同一文件进行写操作时触发Lease软超时强制回收;如果客户端写文件完成但是没有成功释放Lease,则会由LeaseManager的后台线程LeaseManager.Monitor检查是否硬超时后统一触发超时回收。不管是softLimit还是hardLimit超时触发的强制Lease回收,处理逻辑都一样:FSNamesystem.internalReleaseLease,逻辑本身比较复杂,已在上面详细介绍。简单的说先对Lease过期前最后一次写入的Block进行检查和修复,之后释放超时持有的Lease,保证后面其他客户端的写入能够正常申请到该文件的Lease。
----------------------------------------
转自:http://blog.youkuaiyun.com/androidlushangderen/article/details/52850349
前言
在HDFS中,当每次客户端用户往某个文件中写入数据的时候,为了保持数据的一致性,此时其它客户端程序是不允许向此文件同时写入数据的。那么HDFS是如何做到这一点的呢?答案是租约(Lease)。换句话说,租约是HDFS给予客户端的一个写文件操作的临时许可证,无此证件者将不被允许操作此文件。本文我们将要深入分析HDFS内部的租约机制,包括租约的添加、移除、管理操作等等。
HDFS租约的概念
HDFS租约可能很多使用HDFS的人都或多或少都知道一些,大致的理解一般如下:“客户端在每次读写HDFS文件的时候获取租约对文件进行读写,文件读取完毕了,然后再释放此租约”。但是是否有人仔细研究过这个租约内部到底包含了什么信息呢,它与租约持有者也就是客户端用户是一种怎样的逻辑关系呢?首先我们需要了解的就是这个问题。下面是HDFS租约的相关定义:
- 每个客户端用户持有一个租约。
- 每个租约内部包含有一个租约持有者信息,还有此租约对应的文件Id列表,表示当前租约持有者正在写这些文件Id对应的文件。
- 每个租约内包含有一个最新近更新时间,最近更新时间将会决定此租约是否已过期。过期的租约会导致租约持有者无法继续执行写数据到文件中,除非进行租约的更新。
综合上述3点,租约的结构关系如图1-1所示。
图 1-1 租约内部结构
租约类代码中的定义如下:
class Lease {
// 租约持有者
private final String holder;
// 最近更新时间
private long lastUpdate;
// 当前租约持有者打开的文件
private final HashSet<Long> files = new HashSet<>();
...
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
每次当客户端用户新写入一个文件的时候,它会将此文件Id加入到它所对应的租约中,同时更新lastUpdateTime值。讲述完租约的概念,下面我们在来看租约的管理。
HDFS租约的管理
在HDFS中,每天会有许许多多的应用程序在读写文件,于是就会有各个租约的生成。那么这些租约是如何管理的呢?如果有些客户端用户写某文件后未及时关闭此文件,导致此租约一直未释放,从而造成其他用户无法对此文件进行写操作,面对这种情况,HDFS中的做法是怎样的呢?以上提到的问题就是租约管理的内容了。
LeaseManager租约管理器
HDFS租约管理的操作集中在一个类上:LeaseManager。它与CacheManager(缓存管理类),SnapshotManager(快照管理类)类似,是一个中心管理类,运行在Active NameNode的服务中。租约类的定义就是在LeaseManager中的。在LeaseManager租约管理器中,它所做的事情主要归纳为两类。
第一个,维护HDFS内部当前所有的租约,并以多种映射关系进行保存。保存的映射关系分为如下3种:
- 租约持有者对租约的映射关系。
- 文件Id对租约的映射关系。
- 按照时间排序进行租约集合的保存,此关系并不是一种映射关系。
以上3种关系的代码定义如下:
public class LeaseManager {
...
// 租约持有者对租约的映射图
private final SortedMap<String, Lease> leases = new TreeMap<>();
// 按照时间进行排序的租约队列
private final PriorityQueue<Lease> sortedLeases = new PriorityQueue<>(512,
new Comparator<Lease>() {
@Override
public int compare(Lease o1, Lease o2) {
return Long.signum(o1.getLastUpdate() - o2.getLastUpdate());
}
});
// 文件Id对租约的映射图
private final HashMap<Long, Lease> leasesById = new HashMap<>();
...
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
图形展示效果如图1-2所示。
图 1-2 HDFS内部保存的租约映射关系
HDFS保存多种映射关系是为了方便租约的多维度查询,至少目前来看,按照租约持有者,正在写的文件Id都可以直接查到对应的租约对象。
在LeaseManager租约管理器中,还有一件重要的事情是定期释放过期的租约对象。这个操作可以避免文件租约长期不释放导致其他客户端文件无法写文件的问题。
因为在某些异常情况下,客户端程序可能在写完文件后,没有正常关闭文件,导致文件始终处于正在写的状态中,此文件在对应的租约中没有被真正的移除掉。
LeaseManager中的解决办法是启动一个定时的监控线程,来释放过期的租约。周期检测线程主方法如下:
public void run() {
for(; shouldRunMonitor && fsnamesystem.isRunning(); ) {
boolean needSync = false;
try {
fsnamesystem.writeLockInterruptibly();
try {
// 如果当前NameNode已经离开安全模式
if (!fsnamesystem.isInSafeMode()) {
// 则进行租约进行检测操作
needSync = checkLeases();
}
} finally {
...
}
// 进行租约间隔检测时间的睡眠,默认2秒
Thread.sleep(fsnamesystem.getLeaseRecheckIntervalMs());
} catch(InterruptedException ie) {
if (LOG.isDebugEnabled()) {
LOG.debug(name + " is interrupted", ie);
}
} catch(Throwable e) {
LOG.warn("Unexpected throwable: ", e);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
从上面的代码我们可以看出,这是一个持续运行的操作,我们进入checkLease方法,先来看checkLease的头几行代码的执行逻辑,
synchronized boolean checkLeases() {
boolean needSync = false;
assert fsnamesystem.hasWriteLock();
// 获取租约检测的起始时间
long start = monotonicNow();
// 满足一下3个条件,则进入租约释放操作:
// 1.如果租约队列不为空
// 2.租约队列中最老的租约已经出现了超时
// 3.没到租约检测的最大时间期限
while(!sortedLeases.isEmpty() && sortedLeases.peek().expiredHardLimit()
&& !isMaxLockHoldToReleaseLease(start)) {
// 获取更新时间最老的租约,同样也是已过期的租约时间
Lease leaseToCheck = sortedLeases.peek();
LOG.info(leaseToCheck + " has expired hard limit");
...
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
因为sortedLeases租约队列已经是按最近更新时间值排序好的,所以取出的Lease对象就是最旧的一个租约。在这里还要介绍上面的第3个条件的意思,也就是下面这行代码的意思:
isMaxLockHoldToReleaseLease(start)
- 1
- 1
因为HDFS为了避免每次租约检测花费过长的时间,在此进行租约检测时间的判断,如果时间超过了,则终止当前的操作,等待下一次的checkLease操作。
我们继续来看while循环内下半部分的代码:
...
final List<Long> removing = new ArrayList<>();
// 获取待释放租约中包含的文件Id
Collection<Long> files = leaseToCheck.getFiles();
Long[] leaseINodeIds = files.toArray(new Long[files.size()]);
FSDirectory fsd = fsnamesystem.getFSDirectory();
String p = null;
// 遍历这些文件Id
for(Long id : leaseINodeIds) {
try {
// 获取这些文件Id对应的INode path对象
INodesInPath iip = INodesInPath.fromINode(fsd.getInode(id));
p = iip.getPath();
// Sanity check to make sure the path is correct
if (!p.startsWith("/")) {
throw new IOException("Invalid path in the lease " + p);
}
// 进行文件的关闭,在此过程中,此文件Id将从此租约中移除
boolean completed = fsnamesystem.internalReleaseLease(
leaseToCheck, p, iip,
HdfsServerConstants.NAMENODE_LEASE_HOLDER);
...
} catch (IOException e) {
LOG.error("Cannot release the path " + p + " in the lease "
+ leaseToCheck, e);
// 如果在关闭文件的过程中发生异常,则将文件Id加入到移除列表中
removing.add(id);
}
// 如果发现租约检测时间到了,则终止当前操作
if (isMaxLockHoldToReleaseLease(start)) {
LOG.debug("Breaking out of checkLeases after " +
fsnamesystem.getMaxLockHoldToReleaseLeaseMs() + "ms.");
break;
}
}
// 从租约中移除异常文件Id
for(Long id : removing) {
// 如果此租约中已无文件Id,则此租约将从HDFS中彻底移除
removeLease(leaseToCheck, id);
}
}
return needSync;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
通过上述代码,租约检测的操作可以归纳为如下步骤:
- 第1步,获取最老的已过期的租约。
- 第2步,得到此租约中保存的文件Id。
- 第3步,关闭这些文件Id对应的文件,并将这些文件Id从此租约中移除。
- 第4步,如果此租约中已经没有打开的文件Id,则将此租约从系统中进行移除。
租约检测过程如图1-3所示。
图 1-3 HDFS租约的周期性检测过程
LeaseRenewer租约更新器
LeaseRenewer对象的作用在于定时更新DFSClient用户所持有的租约。每个用户对应一个LeaseRenewer更新器对象,而每个LeaseRenewer对象内部会维护一个DFSClient客户端列表。在LeaseRenewer的主方法中,会定期的执行DFSClient客户端对应租约的renew操作。当DFSClient端所操作的文件都被关闭了,此DFSClient将从LeaseRenewer的客户端列表中进行移除,这就意味着此DFSClient所对应的租约将不再被更新,最后将会被LeaseManager进行过期移除操作。
HDFS租约的添加、检测、释放
讲述完租约的概念以及管理之后,我们来分析租约的添加到释放的过程。以我们对于租约的一个传统概念应该是这样一个过程:首先在进行文件写操作时,进行租约的添加,然后操作结束之后,进行租约的释放。但是猜想归猜想,事实上究竟是否如此呢?下面我们从HDFS代码层面对此进行分析。
首先是HDFS租约的添加,租约的添加的确是在每次HDFS写文件操作的时候进行的,以追加写操作为例:
static LocatedBlock prepareFileForAppend(final FSNamesystem fsn,
final INodesInPath iip, final String leaseHolder,
final String clientMachine, final boolean newBlock,
final boolean writeToEditLog, final boolean logRetryCache)
throws IOException {
assert fsn.hasWriteLock();
final INodeFile file = iip.getLastINode().asFile();
final QuotaCounts delta = verifyQuotaForUCBlock(fsn, file, iip);
file.recordModification(iip.getLatestSnapshotId());
file.toUnderConstruction(leaseHolder, clientMachine);
// 在追加写操作之前进行租约的添加
fsn.getLeaseManager().addLease(
file.getFileUnderConstructionFeature().getClientName(), file.getId());
...
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
类似的方法还有FSDirWriteFileOp的startFile方法。对于租约的移除,本人在查阅相关代码时,并没有明显发现在关闭文件操作的时候进行租约的移除动作。所以租约的移除并不是一个简单的过程,此过程的移除还是依赖于LeaseManager的租约过期移除操作。文件在关闭的过程中,会将自身从相应的DFSClient客户端对象中进行移除,继而使得此DFSClient从LeaseRenewer对象中移除,最后让它的租约不再更新。此过程原理见上小节LeaseRenewer对象的原理介绍。在DFSClient端的写文件操作方法中,会执行LeaseRenewer的添加动作,代码如下:
private DFSOutputStream append(String src, int buffersize,
EnumSet<CreateFlag> flag, String[] favoredNodes, Progressable progress)
throws IOException {
checkOpen();
final DFSOutputStream result = callAppend(src, flag, progress,
favoredNodes);
// 将当前文件Id加入,
beginFileLease(result.getFileId(), result);
return result;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
这里的beginFileLease操作的意思不是添加新租约的意思,而是说开始对此文件所属的租约开启定时更新操作,执行的更新操作是LeaseRenewer的run方法。
最后我们来看租约的检查,我们看看HDFS如何利用租约来保证只有一个客户端程序可以写数据到某个文件的。HDFS租约的检查方法为FSNamesystem的checkLease方法。此方法在getAdditionalDatanode和fsync方法中被调用,这表明了租约检查发生以下在两个时候:
- 第一个,为新写的block选择目标存储节点时,进行租约的检查。
- 第二个,进行数据同步到磁盘的时候,又一次进行租约的检查。
这里我们以getAdditionalDatanode方法为例:
LocatedBlock getAdditionalDatanode(String src, long fileId,
final ExtendedBlock blk, final DatanodeInfo[] existings,
final String[] storageIDs,
final Set<Node> excludes,
final int numAdditionalNodes, final String clientName
) throws IOException {
...
readLock();
try {
...
//进行租约的检查
final INodeFile file = checkLease(iip, clientName, fileId);
...
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
最后进入checkLease租约检查方法,
INodeFile checkLease(INodesInPath iip, String holder, long fileId)
throws LeaseExpiredException, FileNotFoundException {
String src = iip.getPath();
INode inode = iip.getLastINode();
assert hasReadLock();
if (inode == null) {
throw new FileNotFoundException("File does not exist: "
+ leaseExceptionString(src, fileId, holder));
}
...
// 获取当前文件的操作者即租约持有者
final String owner = file.getFileUnderConstructionFeature().getClientName();
// 如果当前操作者不是租约持有者,则抛出异常
if (holder != null && !owner.equals(holder)) {
throw new LeaseExpiredException("Client (=" + holder
+ ") is not the lease owner (=" + owner + ": "
+ leaseExceptionString(src, fileId, holder));
}
return file;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
所以当我们在HDFS的日志中看到诸如“Client (=xxx) is not the lease owner…”这种错误的时候,就表明当前有多个客户端程序同时在写某个文件。
OK,以上就是本文所要讲述的HDFS租约的相关内容了,希望本文能让大家对HDFS租约有一个更深入的了解。个人感觉HDFS的整个租约逻辑还是有一定复杂度的,还需要大家进行反复地阅读,理解。