本文讲述HDFS的lease机制,写此博客的目的是想记录下之前在公司测试环境下保存原始数据到HDFS时,发生的一个异常。
一、异常原因及分析
异常描述
此异常发生在消费kafka数据并追加写入到HDFS中的程序中,异常发生的具体场景为:程序第一次启动时,创建并追加写入HDFS数据没问题,一直运行也正常。但是当程序停止,第二次启动时,程序抛出了如下异常:
原因分析
写入HDFS数据所使用的的API的类主要是FileSystem和FSDataOutputStream,一般是通过FileSystem.create 或 FileSystem.append方法获取FSDataOutputStream,然后调用FSDataOutputStream的write(byte[])方法写数据。
Lease机制
hdfs支持write-once-read-many,也就是说不支持并行写,那么对读写的互斥同步就是靠Lease实现的。Lease说白了就是一个有时间约束的锁。客户端写文件时需要先申请一个Lease,对应到namenode中的LeaseManager,客户端的client name就作为一个lease的holder,即租约持有者。LeaseManager维护了文件的path与lease的对应关系,还有clientname->lease的对应关系。LeaseManager中有两个时间限制:softLimit 和 hardLimit。
softLimit的默认值为hdfs.regionserver.lease.period=60000默认值 60000ms,即一分钟。hardLimit的默认值为1h。
关于softLimit和hardLimit
软限制就是写文件时规定的租约超时时间。
硬限制则是考虑到文件close时未来得及释放lease的情况强制回收租约。
LeaseManager中还有一个Monitor线程来检测Lease是否超过hardLimit。而软租约的超时检测则在DFSClient的LeaseChecker中进行。
create和append方法
当客户端(DFSClient)create一个文件的时候,会通过RPC 调用 namenode 的createFile方法来创建文件。进而又调用FSNameSystem的startFile方法,然后调用 LeaseManager 的addLease方法为新创建的文件添加一个lease。如果lease已存在,则更新该lease的lastUpdate (最近更新时间)值,并将该文件的path对应应该lease上。之后DFSClient 将该文件的path 添加 LeaseChecker中。文件创建成功后,守护线程LeaseChecker会每隔一定时间间隔renew该DFSClient所拥有的lease。
当客户端调用append方法时,也会维护一个该文件的path对应的lease。
查看我当时写的代码:
1 | if (cache == null) { |
释放Lease的时机
- 手动调用FSDataOutputStream的close方法。
- 等待softLimit时长过后。
- 等待hardLimit时长过后。
结合代码和Lease释放的时机,此时可以得出结论,代码中创建目录时及时的将FSDataOutputStream资源close了,但是append方法的FSDataOutputStream在第一次程序退出时并未close,因为当时的逻辑是按日期进行存储原始数据到HDFS,而从kafka拉取一次的数据中每条的时间不确定,因此我在缓存中维护了path和FSDataOutputStream的关系,如果缓存中有对应path的FSDataOutputStream资源则直接使用,否则根据path重新创建一个,但是程序退出时并未进行任何close操作,导致了该path的Lease一直被占用,第二次程序启动的时候必须等过去softLimit时长,也就是一分钟后才可以继续写数据。所以说假如在第一次程序退出时一分钟之内再启动程序,就会抛出如上的异常!
二、异常解决方式
1. 减小softLimit参数值
理论上来讲,减小softLimit参数值是可以解决这个问题的,比如设置成1s,则第二次启动程序时各path的对应lease应该均处于close状态了,但是考虑到各种原因,当时我并未对这个参数做调整,生怕再有其它问题出现。
2. 在程序退出时关闭缓存中的所有FSDataOutputStream
采用Java中的hook技术,在main函数中加入如下代码,以保证JVM退出时执行close操作。
1 | Runtime.getRuntime().addShutdownHook(new Thread() { |
注意:此处的JVM退出,两种方式终止程序时会执行hook代码,一个是kill pid,还有一个是有异常抛出的时候终止时也会调用hook代码。但是还有两种方式JVM退出时不执行hook代码,导致资源还是未close掉,一个是IDEA的小红方块,还一个是kill -9 pid强制杀死程序,这两种方式都不会执行hook代码。
附带一个kill和kill -9的区别:https://www.cnblogs.com/flashfish/p/10930974.html
三、总结
发生此异常的根本原因还是自己的程序不够严谨,考虑的不够周到,大家以后在编码时一定要做到严谨,避免这样的低级错误。