概述
HDFS 使用 Java 编写而成,默认提供 Java API,但有时候业务会需要 C API,在这种情况下,可以使用 libhdfs,libhdfs 随 Hadoop 一起发行,由 Hadoop 官方社区进行维护,其性能、稳定性都比较好,这篇文章介绍了 Hadoop libhdfs 的使用方法和性能测试结果。
libhdfs 使用步骤
-
在业务环境上安装好 Hadoop,注意以下几点:
- core-site.xml、hdfs-site.xml 等配置文件中,相关的必要配置项(特别是 HA、认证相关的配置项)需要先配置好。
- 最终请务必确保 HDFS 客户端(即 bin/hdfs dfs 系列命令)能正常工作。
-
配置通用环境变量(假设 hadoop 安装到了 /root/hadoop-2.7.2/)
export HADOOP_HOME=/root/hadoop-2.7.2 export CLASSPATH=$($HADOOP_HOME/bin/hadoop classpath --glob):$CLASSPATH export LD_LIBRARY_PATH=$HADOOP_HOME/lib/native:$LD_LIBRARY_PATH
-
配置业务相关环境变量
LIBHDFS_OPTS 这个环境变量是传给 libhdfs JVM 的参数,可以通过它配置 JVM参数、HDFS Java Client 日志参数等,根据业务场景的不同,可以有不同的选择,举两个例子:-
配置 Xmx 为 512M,配置 HDFS Java Client 的日志级别为 INFO,日志 appender 为 console(这是 log4j 的一个内部 appender,日志直接输出到 stderr):
export LIBHDFS_OPTS="-Xmx512m -Dhadoop.root.logger=INFO,console"
-
配置 JVM Xmx 为 512M,配置 HDFS Java Client 的日志级别为 INFO,日志 appender 为 DRFA(这是 hadoop 自带的一个 log4j appender,按照天为单位,每天生成一个日志文件),日志目录为 /tmp,日志文件为 log.txt(生产环境下,只需要按需调整日志目录和日志文件的配置即可):
export LIBHDFS_OPTS="-Xmx512m -Dhadoop.root.logger=INFO,DRFA -Dhadoop.log.dir=/tmp -Dhadoop.log.file=log.txt"
-
-
编译 .c 源文件并运行
gcc sample.c -I $HADOOP_HOME/include -L $HADOOP_HOME/lib/native -lhdfs -o sample ./sample
libhdfs 内存泄露风险
libhdfs 的一些 API 会在内部分配内存,并作为指针返回给上层业务,业务使用完这些指针后,必须手动 free,否则会造成内存泄露,这些关键点在各个 API 的使用注释中都写得很清楚(查看 libhdfs 自带的头文件 hdfs.h 即可获取详细信息),请务必注意。
这样的 API 主要有下面这几组:
- 获取配置项值的一组 API:
int hdfsConfGetStr(const char *key, char **val);
void hdfsConfStrFree(char *val);
- 获取文件信息、list 目录的一组 API:
hdfsFileInfo *hdfsListDirectory(hdfsFS fs, const char* path, int *numEntries);
hdfsFileInfo *hdfsGetPathInfo(hdfsFS fs, const char* path);
void hdfsFreeFileInfo(hdfsFileInfo *hdfsFileInfo, int numEntries);
- 获取文件 block 位置的一组 API:
char*** hdfsGetHosts(hdfsFS fs, const char* path, tOffset start, tOffset length);
void hdfsFreeHosts(char ***blockHosts);
- 创建 zero-copy 选项的一组 API:
struct hadoopRzOptions *hadoopRzOptionsAlloc(void)
void hadoopRzOptionsFree(struct hadoopRzOptions *opts);
libhdfs 性能
问题来源
libhdfs 对外提供 C 接口,具体实现是通过 JNI 的方式调用 HDFS 的 Java 接口,这样的话在读写过程中就会涉及内存拷贝,有以下两种情况:
- 写过程中,需要将用户 C 代码中的 buffer 数组内容,拷贝到 JVM 中,然后调用 HDFS Java 接口执行写入。
- 读过程中,需要先调用 HDFS Java 接口执行读取,然后将 JVM 中的数组内容,拷贝到用户 C 代码的 buffer 数组中。
内存拷贝可能会对读写性能产生一定的影响,具体影响多少需要进行测试。
性能测试
测试环境
使用现网一台空闲机器进行测试,使用该机器搭建单节点的 HDFS 集群,并在本机器上进行读写,这样就排除了网络因素的影响。具体配置如下:
项目 | 配置 |
---|---|
CPU | 16核 X64 Xeon 2.10GHz |
RAM | 64GB |
磁盘 | 7.3TB HDD * 12块 |
测试结果
-
两个前提
- 无论是 Java 测试代码,还是 libhdfs 测试代码,内部都使用 4K 的读写 buffer,这也是一个典型的 buffer 大小。
- 每次执行测试项目前,都需要执行
sync && echo 3 > /proc/sys/vm/drop_caches
释放文件系统缓存,保证多次测试的独立性。
写测试
文件大小 | Java 客户端耗时 | libhdfs 客户端耗时 | libhdfs 相比 Java |
---|---|---|---|
2GB | 4.403s | 4.269s | +3.12% |
5GB | 7.858s | 7.599 | +3.41 |
10GB | 13.099s | 13.371s | -2.08 |
50GB | 58.403s | 59.597s | -2.04% |
- 读测试
文件大小 | Java 客户端耗时 | libhdfs 客户端耗时 | libhdfs 相比 Java |
---|---|---|---|
2GB | 11.006s | 10.9s | +0.96% |
5GB | 25.001s | 24.331s | +2.68% |
10GB | 46.691 | 46.385 | +0.66% |
50GB | 3m54.302s | 3m54.122s | +0.08% |
- 结论
可以看到,Java 客户端和 libhdfs 客户端的性能非常接近,基本可以认为相同。