一、概述
近年来,大数据技术如火如荼,如何存储海量数据也成了当今的热点和难点问题,而HDFS分布式文件系统作为Hadoop项目的分布式存储基础,也为HBASE提供数据持久化功能,它在大数据项目中有非常广泛的应用。
Hadoop分布式文件系统(Hadoop Distributed File System,HDFS)被设计成适合运行在通用硬件(commodity hardware)上的分布式文件系统。HDFS是Hadoop项目的核心子项目,是一种具有高容错性、高可靠性、高可扩展性、高吞吐量等特征的分布式文件系统,可用于云计算或其它大数据应用中海量数据的存储(主要为大文件的存储)。
本文结合作者本人及同事对HDFS的学习和实践的理解,首先介绍HDFS的特点和重要SHELL命令(hadoop和hdfs命令)的使用,接着介绍HDFS提供的C访问接口LIB HDFS及其跟普通文件系统的C API的异同,然后介绍如何利用LIB HDFS接口实现简单的HDFS客户端并列举相关应用实例,最后针对编写HDFS客户端中遇到的问题进行描述和分析。
二、HDFS简介
HDFS是Hadoop项目的核心子项目,是一种具有高容错性、高可靠性、高可扩展性、高吞吐量等特征的分布式文件系统。
1.HDFS特点
HDFS作为一种分布式文件系统,主要有以下特点:
1)主要用于存储和管理大数据文件(由于HDFS默认数据块为128M,所以它主要适合于存储百M级别及以上大小的文件)。
2)其数据节点可横向扩展,且可选择廉价的商业硬件。
3)设计理念为“一次写,多次读”。
4)当前不支持在文件任意位置修改文件内容,只能在文件尾部执行append操作。
5)不适合低延迟(几十毫秒)数据访问应用(低延迟应用可以考虑HBASE分布式数据库或者ES+分布式文件系统的架构)。
2.HDFS常用SHELL命令简介
hadoop有两个非常重要的SHELL命令:hadoop和hdfs。对于管理HDFS文件系统而言,hadoop和hdfs脚本功能有很大的重复性,下面以分别对这两个命令进行介绍。
1)hadoop命令使用
hadoop几乎所有的管理命令都被整合到了一个SHELL脚本中,即bin/hadoop脚本,通过执行带参数的hadoop脚本,就可以实现hadoop模块的管理工作。由于本文主要介绍HDFS文件系统,所以这里就主要介绍如何使用hadoop脚本操作HDFS文件系统,相关命令及参数见图1所示。
图1 hadoop fs命令及参数描述
下面举一个具体的例子。例如,要在HDFS文件系统中查看根目录包含的所有目录或文件,再创建test目录,并赋予777权限,最后删除该目录。实现以上操作的主要命令如下:
hadoop fs –ls /
hadoop fs -mkdir /test
hadoop fs –chmod –R 777 /test
hadoop fs –rmdir /test
hadoop fs命令能够实现用户组及权限的管理、目录和文件和管理、文件的上传和下载等功能,但对于HDFS文件系统的检查(包括坏块的清理)、节点管理、快照管理和格式化等深层次管理工作就无能为力了,这里就需要用到hdfs命令了。
2)hdfs命令使用
hdfs所有管理命令都被整合到了一个SHELL脚本中,即bin/hdfs脚本,通过执行带参数的hdfs脚本,就可以实现对HDFS文件系统的管理工作,包括基本的文件、目录、权限和属组操作以及数据块管理和格式化等功能。
hdfs脚本实现的功能非常强大,hdfs dfs命令跟hadoop fs命令功能完全一致,所以我们可以利用hdfs dfs命令并携带图1中的参数实现hadoop fs的所有功能。下面主要介绍下HDFS文件系统格式化和数据块管理操作。
格式化一个HDFS文件系统,使用如下命令:
hdfs namenode -format
删除HDFS文件系统中存在的坏块及相应已损坏的文件,使用如下命令:
hdfs fsck -delete -files /
三、LIB HDFS接口简介
Hadoop FileSystem APIs是JAVA CLIENT API,HDFS并没有提供原生的C语言访问接口。但HDFS提供了基于JNI的C调用接口LIB HDFS,为C语言访问HDFS提供了很大的便利。
LIB HDFS接口的头文件和库文件已包含在Hadoop发行版本中,可以直接使用。它的头文件hdfs.h一般位于 HADOOPHOME/include目录中,而其库文件libhdfs.so通常则位于 {HADOOP_HOME}/lib/native目录中。因为Hadoop版本一直在更新中,所以不同版本的LIB HDFS接口功能通常不太一样,主要表现为功能递增的现象。
通过LIB HDFS访问HDFS文件系统与使用C语言API访问普通操作系统的文件系统类似,但还存在一些不足的地方,具体如下所示:
a)LIB HDFS接口实现的功能只是JAVA CLIENT API功能的一个子集,且跟JAVA CLIENT API相比可能还存在不少BUG未被发现或修复,如多线程、多进程访问文件系统时句柄资源释放的问题。
b)另外由于是使用JNI方式调用JAVA CLASS,所以应用程序占用内存较多,而且该接口运行可能会产生大量异常日志,怎么管理这些日志是个问题。另外,当操作HDFS文件系统出错时,errno不一定会有正确的提示,也会增加排查问题的难度。
c)目前LIB HDFS可参考用例较少,而利用多线程等方式通过LIB HDFS大数据量读写HDFS文件系统的用例更是少之又少。
d)目前LIB HDFS不支持在任意位置修改文件内容,只能在文件末尾执行append操作,或对整个文件执行truncate操作,这个主要跟HDFS文件系统设计有关,大数据存储一般都缺乏更新功能的支持,这点我们只能通过业务层面来规避了。
e)由低到高Hadoop发行版本携带的LIB HDFS功能可能呈现递增的情况,所以每当Hadoop版本更新了,都需要重新编译我们的应用程序,增加升级难度。
目前虽然LIB HDFS接口还存在一些不足的地方,但相信未来随着该接口版本的不断更新,其功能和稳定性都会大大提高。
四、C语言访问HDFS应用实践
1.编译和运行环境搭建
为了成功编译C语言客户端程序,我们需要预先安装7.0及以上版本的JAVA JDK和Hadoop发行版,前者提供libjvm.so等库,后者则提供LIB HDFS连接所需要的库。
为了成功运行C语言客户端程序,除了预先安装上面提到的程序外,我们还需要正确地设置几个关键环境变量,包括LD_LIBRARY_PATH和CLASSPATH的设置。
关于LD_LIBRARY_PATH环境变量,主要是需要添加libjvm.so和libhdfs.so库所在路径;而针对CLASSPATH则需要囊括Hadoop提供的所有jar包的全路径信息(具体可通过find+awk组合命令来实现),否则C语言客户端程序总会报缺少某个类而无法运行的错误。
2.LIB HDFS接口简单应用实践
这里主要介绍部分API的使用示例。
1)获取HDFS文件系统的容量和已使用空间大小信息如GetHdfsInfo函数所示:
void GetHdfsInfo(void)
{
hdfsFS pfs = NULL;
int iRet = 0;
tOffset iTmp = 0;
pfs = hdfsConnect("hdfs://127.0.0.1:9000/", 0); // 与HDFS文件系统建立连接
if (NULL == pfs)
{
WRITELOGEX(LOG_ERROR, ("GetHdfsInfo(): hdfsConnect failed! errno=%d.", errno));
return;
}
WRITELOG(LOG_INFO, "GetHdfsInfo(): hdfsConnect success!");
iTmp = hdfsGetCapacity(pfs); // 获取HDFS文件系统容量
if (-1 == iTmp)
{
WRITELOGEX(LOG_ERROR, ("GetHdfsInfo(): hdfsGetCapacity failed! errno=%d.", errno));
hdfsDisconnect(pfs);
pfs = NULL;
return;
}
WRITELOGEX(LOG_INFO, ("GetHdfsInfo(): hdfsGetCapacity success! offset=%ld.", iTmp));
iTmp = hdfsGetUsed(pfs); // 获取HDFS文件系统中所有文件占用空间大小,即已使用量
if (-1 == iTmp)
{
WRITELOGEX(LOG_ERROR, ("GetHdfsInfo(): hdfsGetUsed failed! errno=%d.", errno));
hdfsDisconnect(pfs);
pfs = NULL;
return;
}
WRITELOGEX(LOG_INFO, ("GetHdfsInfo(): hdfsGetUsed success! offset=%ld.", iTmp));
iRet = hdfsDisconnect(pfs); // 关闭与HDFS文件系统的连接
if (-1 == iRet)
{
WRITELOGEX(LOG_ERROR, ("GetHdfsInfo(): hdfsDisconnect failed! errno=%d.", errno));
return;
}
WRITELOGEX(LOG_INFO, ("GetHdfsInfo(): hdfsDisconnect success! ret=%d.", iRet));
pfs = NULL;
return;
}
2)在HDFS文件系统中新增文件并写入数据如HdfsWriteTest函数所示:
void HdfsWriteTest(hdfsFS pfs)
{
int iRet = 0;
hdfsFile pfile = NULL;
char szTestFile[200] = "/test/ write.test";
if (NULL == pfs)
{
WRITELOG(LOG_ERROR, "HdfsWriteTest():pfs is null.");
return;
}
pfile = hdfsOpenFile(pfs, szTestFile, O_WRONLY || O_CREAT, 0, 0, 0); // 打开文件句柄
if (NULL == pfile)
{
WRITELOGEX(LOG_ERROR, ("HdfsWriteTest(): hdfsOpenFile failed! szFilePath=%s,errno=%d.", szTestFile, errno));
return;
}
WRITELOGEX(LOG_INFO, ("HdfsWriteTest(): hdfsOpenFile success! szFilePath=%s.", szTestFile));
iRet = hdfsWrite(pfs, pfile, "hello world!", strlen("hello world!")); // 写入数据
if (-1 == iRet)
{
WRITELOGEX(LOG_ERROR, ("HdfsWriteTest(): hdfsWrite failed! ret=%d,errno=%d.", iRet, errno));
hdfsCloseFile(pfs, pfile);
pfile = NULL;
return;
}
WRITELOGEX(LOG_INFO, ("HdfsWriteTest(): hdfsWrite success! ret=%d.", iRet));
iRet = hdfsHFlush(pfs, pfile); // 将缓冲区中数据写入磁盘
if (-1 == iRet)
{
WRITELOGEX(LOG_ERROR, ("HdfsWriteTest(): hdfsFlush failed! ret=%d,errno=%d.", iRet, errno));
hdfsCloseFile(pfs, pfile);
pfile = NULL;
return;
}
WRITELOGEX(LOG_INFO, ("HdfsWriteTest(): hdfsFlush success! ret=%d.", iRet));
iRet = hdfsCloseFile(pfs, pfile); // 关闭文件句柄,释放资源
if (-1 == iRet)
{
WRITELOGEX(LOG_ERROR, ("HdfsWriteTest(): hdfsCloseFile failed! ret=%d,errno=%d.", iRet, errno));
return;
}
WRITELOGEX(LOG_INFO, ("HdfsWriteTest(): hdfsCloseFile success! ret=%d.", iRet));
pfile = NULL;
return;
}
3.遇到的主要问题描述与分析
对于LIB HDFS接口的不足之处,在本文第三部分(LIB HDFS接口简介)已有大致描述。
在实际性能测试过程中,因LIB HDFS接口引起的问题主要包括:lease租约回收异常和程序句柄资源释放异常等两大类。
我们换了多种测试模型,基本确认LIB HDFS接口在某些异常情况下(如频繁对同一个文件执行append操作)会产生上述问题。所以如果在项目中需要实际应用LIB HDFS接口,就需要我们改进客户端程序处理流程,尽量规避和减少上述问题的产生。可以采用如下方法:
1)在客户端程序和HDFS文件系统间增加缓存的方式降低HDFS的读写密度;
2)减少对HDFS文件系统的更新操作,例如文件写入完成后就不再执行append操作,只执行read操作。
五、总结
本文对HDFS和用C语言访问HDFS的操作进行了详细的介绍,可供相关项目的开发人员参考。
HDFS作为一种分布式文件系统,并不是万能的,例如并不适合于存储量太小或要求低访问延迟的应用场景,又或者需要频繁更新数据的系统。即使应用了HDFS文件系统,为了发挥HDFS文件系统的最大效率,仍可能需要通过我们修改业务分层或逻辑实现等手段来规避HDFS的一些缺点。