Lease机制导致的追加写入文件异常分析

本文讲述HDFS的lease机制,写此博客的目的是想记录下之前在公司测试环境下保存原始数据到HDFS时,发生的一个异常。

一、异常原因及分析

异常描述

此异常发生在消费kafka数据并追加写入到HDFS中的程序中,异常发生的具体场景为:程序第一次启动时,创建并追加写入HDFS数据没问题,一直运行也正常。但是当程序停止,第二次启动时,程序抛出了如下异常:

avatar

原因分析

写入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方法

  1. 当客户端(DFSClient)create一个文件的时候,会通过RPC 调用 namenode 的createFile方法来创建文件。进而又调用FSNameSystem的startFile方法,然后调用 LeaseManager 的addLease方法为新创建的文件添加一个lease。如果lease已存在,则更新该lease的lastUpdate (最近更新时间)值,并将该文件的path对应应该lease上。之后DFSClient 将该文件的path 添加 LeaseChecker中。文件创建成功后,守护线程LeaseChecker会每隔一定时间间隔renew该DFSClient所拥有的lease。

  2. 当客户端调用append方法时,也会维护一个该文件的path对应的lease。

查看我当时写的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (cache == null) {
// 缓存中不存在,直接查询文件并加载到缓存
Path path = new Path(pathStr);
if (!fileSystem.exists(path)) {
fileSystem.create(path).close();
LOG.info("Create file success, path:{}", pathStr);
}
cache = new TransferCache(pathStr);
cache.setTime(System.currentTimeMillis());
cache.setFlushBatchSize(0);
FSDataOutputStream dataOutputStream = fileSystem.append(path);
cache.setDataOutputStream(dataOutputStream);
put(pathStr, cache);
LOG.info("Create new transfer cache, path: {}", pathStr);
}

释放Lease的时机

  1. 手动调用FSDataOutputStream的close方法。
  2. 等待softLimit时长过后。
  3. 等待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
2
3
4
5
6
7
8
9
10
11
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
LOG.info("Shutdown hook execute!");
TransferCacheFactory.instance.close();
} catch (Exception e) {
e.printStackTrace();
}
}
});

注意:此处的JVM退出,两种方式终止程序时会执行hook代码,一个是kill pid,还有一个是有异常抛出的时候终止时也会调用hook代码。但是还有两种方式JVM退出时不执行hook代码,导致资源还是未close掉,一个是IDEA的小红方块,还一个是kill -9 pid强制杀死程序,这两种方式都不会执行hook代码。

附带一个kill和kill -9的区别:https://www.cnblogs.com/flashfish/p/10930974.html

三、总结

发生此异常的根本原因还是自己的程序不够严谨,考虑的不够周到,大家以后在编码时一定要做到严谨,避免这样的低级错误。

坚持原创技术分享,您的支持将鼓励我继续创作!

------本文结束 感谢您的阅读------