作者:梁伟雄
作者简介:Spark爱好者
背景
在数据仓库建设的过程中,会产生越来越多的目录和文件。随着这些大文件、小文件的野蛮生长,我们需要思考,如何保证集群的持续健康?
假设在集群资源有限的情况下,集群资源已通过各种手段最大化被利用。那么,我们可以考虑针对存储文件本身对文件进行“瘦身”,降低磁盘的使用率。可以从以下三个点进行考虑:
- 选择高效的列式存储和压缩方式
- 确定数据冷、热分界线,对冷数据采取降副本、迁移到冷存储等策略
- 对临时数据、无用数据进行删除
需求描述
针对以上提出的三点,首先我们需要把集群所有文件的元数据信息统计出来。
从文件大小的维度,确定大于50G的文件为大文件。从冷热数据的维度,确定一年前都没被访问过的数据为冷数据。从这两个维度出发,获取符合条件的“问题”文件。
针对从以上两个维度所定位出的“问题”文件,可以先查看该文件在生产环境中的使用情况,如果只是开发人员开发过程中产生的临时表且长时间不使用,则直接删除,腾出磁盘空间。
解决方案
1. 解析fsimage文件
目前,测试集群总的目录数和文件数一共两千多万,而这些目录和文件的元数据信息存储在NameNode所在节点的fsimage文件上。我们只需对fsimage文件进行解析就能得到HDFS文件系统下所有目录和文件的元数据信息,然后对其进行分析处理。
HDFS客户端命令提供 oiv 子命令,可以对二进制fsimage文件进行解析,命令如下:
hdfs oiv -i fsimage_0000000006992296147 -o fsimage.csv -p Delimited
附上Hadoop官网关于FsImage的链接,查看各参数意义。
https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/HdfsImageViewer.html
其中,上述命令的fsimage_0000000006992296147是输入文件fsimage_0000000006992296147,也就是我们即将要解析的fsimage文件。fsimage.csv为输出文件,也就是把解析后的结果输出到fsimage.csv。
接着,我们需要定位到fsimage文件在NameNode节点所在目录,以Ambari为例,fsimage文件的路径在HDFS的NameNode directories这个配置参数中。fsimage文件的路径需要根据每个集群NameNode的配置目录确定,每个集群都不一样。
进入fsimage文件所在目录,会发现有很多edits文件和两个fsimage文件,fsimage_0000000005478996249和fsimage_0000000007047407074,其中fsimage_0000000006992296147是我们后续需要解析的文件。我们只需选取文件名下划线右边数字较大的文件作为解析对象。该文件包含了HDFS文件系统所有目录和文件的元数据信息。
我们切换到终端执行上述oiv命令。不建议在NameNode节点执行该命令,可以先把要解析的fsimage文件scp到其他服务器进行解析操作。
有可能会出现以下报错:
莫慌!分析一些报错原因,初步判断应该是内存不够。我们知道HDFS的API本质上也是Java进程,比较粗暴的解决方案是直接修改Java进程的启动参数。查看hdfs脚本。通读脚本后,直接划到最后脚本最后一行,如下图:
修改脚本前请先对原文件进行备份!
原来:exec "$JAVA" -Dproc_$COMMAND $JAVA_HEAP_MAX $HADOOP_OPTS $CLASS "$@"
修改为:exec "$JAVA" -Dproc_$COMMAND $JAVA_HEAP_MAX
-Xmx4096m
$HADOOP_OPTS $CLASS "$@"
再次执行oiv命令,经过漫长的几分钟的等待,fsimge文件已经被解析成fsimage.csv文件。解析后的fsimage.csv文件的大小比原来未被解析的fsimage.csv文件增长了四到五倍。接着,我们把解析后的fsimage.csv文件上传到HDFS。命令如下:
hdfs dfs -put fsimage.csv hdfs:///user/fsimage_dir/2020-06-16/fsimage.csv
2. 使用Spark SQL对fsimage.csv进行分析
代码如下:
package com.miniimport com.mini.utils.FsImageParserimport org.apache.spark.internal.Loggingimport org.apache.spark.sql.{SaveMode, SparkSession}object FsImageApp extends Logging { def main(args: Array[String]): Unit = { val appName = "FsImageApp" val spark = SparkSession .builder() .enableHiveSupport() .appName(appName) .getOrCreate() val sc = spark.sparkContext val start = System.currentTimeMillis() val totals = sc.longAccumulator("totals") val errors = sc.longAccumulator("errors") val inputPath = args(0) val outputPath = args(1) val fsImageRdd = sc.textFile(inputPath) if (fsImageRdd.isEmpty) { logError(s"\n------ The file is empty ------\n") System.exit(1) } val fsImageDf = spark.createDataFrame( fsImageRdd.map(x => { FsImageParser.parseLog(x, totals, errors) }).filter(_.length != 1), FsImageParser.struct ) import spark.sql fsImageDf.createTempView("fs_image_info") val resultSQL = s""" |select *, fileSize / 1024 / 1024 / 1024 as fileSizeG |from fs_image_info where fileSize / 1024 / 1024 / 1024 > 50 order by -fileSize |""".stripMargin sql(resultSQL).coalesce(1) .write .option("header", true) .csv(outputPath) // 计算总的条数还有错误的条数 val totalValues = totals.count val errorValues = errors.count val timeDuration = (System.currentTimeMillis() - start) / 1000 logInfo(s"\n------ $appName App 执行成功,耗时:${timeDuration}秒, 总记录数:$totalValues, 错误数:$errorValues ------\n") spark.stop() }}
简单解释上面主干代码:
- 创建SparkContext并读取fsimage.csv生成RDD
- 把RDD转化为DataFrame并创建临时表fs_image_info
- 编写SQL语句过滤出文件大小超过50G的文件并根据文件大小降序排序
下面代码是以编程的方式将RDD转为DataFrame的具体过程,附上Spark官网的转化步骤,如下图:
package com.mini.utilsimport com.mini.DateUtilsimport org.apache.spark.sql.Rowimport org.apache.spark.sql.types.{LongType, StringType, StructField, StructType, _}import org.apache.spark.util.LongAccumulatorobject FsImageParser { val struct = StructType( Array( StructField("path", StringType), StructField("replication", StringType), StructField("modificationTime", StringType), StructField("accessTime", StringType), StructField("preferredBlockSize", LongType), StructField("blocksCount", LongType), StructField("fileSize", LongType), StructField("nsQuota", LongType), StructField("dsQuota", LongType), StructField("permission", StringType), StructField("userName", StringType), StructField("groupName", StringType), StructField("day", StringType) ) ) def parseLog(log: String, totals: LongAccumulator, errors: LongAccumulator) = { try { totals.add(1) val logSplits = log.split("\t") println(logSplits.length) val path = logSplits(0) val replication = logSplits(1) val modificationTime = logSplits(2) val accessTime = logSplits(3) // 此处需要做try toLong检查 val preferredBlockSize = logSplits(4).toLong val blocksCount = logSplits(5).toLong val fileSize = logSplits(6).toLong val nsQuota = logSplits(7).toLong var dsQuota = logSplits(8).toLong val permission = logSplits(9) val userName = logSplits(10) val groupName = logSplits(11) val day = DateUtils.getNowDate() Row(path, replication, modificationTime, accessTime, preferredBlockSize, blocksCount, fileSize, nsQuota, dsQuota, permission, userName, groupName, day) } catch { case e: Exception => e.printStackTrace() errors.add(1) Row(0) } }}
这里需要解决一个问题,我们如何知道fsimage.csv文件的每个字段各代表什么意思?继续查看Hadoop官网,如下图:
这样,我们就可以确定每个StructField的字段
编写好代码,对代码进行打包,并上传到服务器执行。命令如下:
/usr/ndp/current/spark2_client/bin/spark-submit \--class com.mini.FsImageApp \--master yarn \--deploy-mode cluster \--driver-cores 2 \--driver-memory 8192M \--executor-cores 4 \--num-executors 4 \--executor-memory 8192M \hdfs-profile-1.0.jar \hdfs:///user/fsimage_dir/2020-06-16/fsimage.csv hdfs:///user/fsimage_dir/result/2020-06-16
稍等片刻,hdfs:///user/fsimage_dir/result/2020-06-16目录下已经生成结果文件。
打开结果文件并查看:
上图因为涉及敏感信息已对path等字段做处理,开发运维人员可以根据实际情况对占用磁盘空间大并且毫无用处的文件进行处理。这里还可以根据文件的访问时间和修改时间进行排序,这样就可以明确有多少文件是长时间都没有被访问或修改过的。