spark读取文件机制 源码剖析
Spark数据分区调研
Spark以textFile方式读取文件源码
textFile方法位于
spark-core_2.11/org.apache.spark.api.java/JavaSparkContent.scala
参数分析:
path :String是一个URI,可以是HDFS、本地文件、或者其他Hadoop支持的文件系统
minPartitions:用于指定分区数,具体代码如下,
其中,defaultParallelism对应的就是spark.default.parallelism。如果是从HDFS上
面读取文件,其分区数为文件分片数(Hadoop 1.x中默认为64MB/片,在Hadoop
2.x中默认为128MB/片)。
在textFile中进入hadoopFile,源码如下,
上面代码做了几件事情:
1、获取hadoopConfiguration,并将hadoopConfiguration进行广播;
2、设置任务的文件读取路径;
3、实例化HadoopRDD。
进入到HadoopRDD中,找到getPartitions()方法,源码如下,
getPartitions()方法做了三件事情:
1、获取JobConf,并将其添加信任凭证;
2、获取输入路径格式,并将其按照minPartitions进行split;
3、根据输入的split的个数创建对应的HadoopPartition。
其中getSplits方法的源码如下,
源码在org.apache.hadoop.mapred.FileInputFormat中268行,具体实现如下,
/** Splits files returned by {@link #listStatus(JobConf)} when * they're too big.*/ public InputSplit[] getSplits(JobConf job, int numSplits)throws IOException { //FileStatus对象封装了文件系统中文件和目录的元数据,包括文件的长度、块大小、备份数、修// 改时间、所有者以及权限等信息 FileStatus[] files = listStatus(job); // 从job中获取输入文件状态信息 // Save the number of input files for metrics/loadgen job.setLong(NUM_INPUT_FILES, files.length); //将输入文件个数保存到job中 long totalSize = 0; // compute total size for (FileStatus file: files) { // check we have valid files if (file.isDirectory()) { throw new IOException("Not a file: "+ file.getPath()); } // 遍历当前作业的所有输入文件,然后将累积这些文件的字节数并保存到变量totalSize中 totalSize += file.getLen(); } // 按用户要求划分输入,确定每个split的目标大小goalSize long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits); // minSplitSize 是FileInputFormat类的成员 ,允许的分区的最小的大小,默认为1B //job.getLong("mapred.min.split.size", 1)是获取配置文件中设置的值,若没有设置,则取1 long minSize = Math.max(job.getLong(org.apache.hadoop.mapreduce.lib.input. FileInputFormat.SPLIT_MINSIZE, 1), minSplitSize); // generate splits // 申请一个初始大小为numSplits的数组,来存放划分结果 ArrayList<FileSplit> splits = new ArrayList<FileSplit>(numSplits); // 申请一个网络拓扑,用于划分过程中保存整个网络的拓扑结构 NetworkTopology clusterMap = new NetworkTopology(); for (FileStatus file: files) { Path path = file.getPath(); // 获取文件路径 long length = file.getLen(); // 文件长度(字节数) if (length != 0) { FileSystem fs = path.getFileSystem(job); // 获得hdfs文件系统中的路径信息 BlockLocation[] blkLocations; if (file instanceof LocatedFileStatus) { blkLocations = ((LocatedFileStatus) file).getBlockLocations(); } else { // 获得此文件每个block所在位置(节点),可能存在于不同的节点上,所以是个数组 blkLocations = fs.getFileBlockLocations(file, 0, length); } if (isSplitable(fs, path)) { // 调用文件的getBlockSize方法,获取文件的块大小并存储在变量blockSize中 long blockSize = file.getBlockSize(); // 计算分片的大小,具体实现见后文 long splitSize = computeSplitSize(goalSize, minSize, blockSize); long bytesRemaining = length; // 文件剩余字节数 // 文件剩余大小 大于 切片大小的1.1倍才会继续切片 while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) { // 获得此split所在的主机位置 String[] splitHosts = getSplitHosts(blkLocations, length-bytesRemaining, splitSize, clusterMap); // 添加分片到结果集,splitHosts表示此文件(path指定)的此分片 //(length-bytesRemaining和splitSize指定)所在的hosts splits.add(makeSplit(path, length-bytesRemaining, splitSize, splitHosts)); bytesRemaining -= splitSize; // 剩余大小 } // 将文件的最后一部分作为一个split if (bytesRemaining != 0) { String[] splitHosts = getSplitHosts(blkLocations, length - bytesRemaining, bytesRemaining, clusterMap); splits.add(makeSplit(path, length - bytesRemaining, bytesRemaining, splitHosts)); } } else { // 文件不可分片,则将整个文件作为一个分片 String[] splitHosts = getSplitHosts(blkLocations,0,length,clusterMap); splits.add(makeSplit(path, 0, length, splitHosts)); } } else { // 文件长度为0,则生成一个空分片 //Create empty hosts array for zero length files splits.add(makeSplit(path, 0, length, new String[0])); } } LOG.debug("Total # of splits: " + splits.size()); return splits.toArray(new FileSplit[splits.size()]); }
其中,每个分片的大小计算方法如下,
goalSize:是根据用户期望的分区数算出来的,每个分区的大小,总文件大小/用户期望分区数
minSize :InputSplit的最小值,由配置参数mapred.min.split.size(在/conf/mapred-site.xml文件中配置)确定,默认是1(字节)
blockSize :文件在HDFS中存储的block大小(在/conf/hdfs-site.xml文件中配置),不同文件可能不同,默认是64MB或者128MB。
结论
Spark从HDFS上读取文件时,按照文件的大小对数据进行分区,具体每个分区的大小通过上述cpmputeSplitSize(long goalSize,long minSize,long blockSize)方法进行计算,一般情况都是取goalSize和blockSize中较小的值。对于不同的文件形式具体可以从以下几个方面进行考虑 :
数据以单文件且文件内容以一行的形式表现
在上述getSplits方法中输出类型InputSplit是对文件进行处理和运算的输入
单位,只是一个逻辑概念,每个InputSplit并没有对文件进行实际的切割,只是记录了要处理的数据的位置(包括文件的path和hosts)和长度(由start和length决定)。因此以行记录形式的文本,可能存在一行记录被划分到不同的block,甚至不同的DataNode上去。通过分析getSplits方法,可以得出,某一行记录同样也可能被划分到不同的InputSplit。但是不会对map之类的操作造成影响,原因是与FileInputFormat关联的RecordReader中的readLine方法在读取数据的时候不会受到InputSplit划分的限制,当读取一行的时候会一直读取直到读取到行结束符为止,因此其能够读取不同的InputSplit,直到把这一行数据读取完成。且当下一次在另外一个InputSplit中再次读取到该行的数据时不会再次进行计算。
数据以单文件且文件内容以多行的形式表现
如果一条请求的内容以多行的形式存储在一个文件中,一条请求的内容可能
会被划分开,出现解析错误,不能保证信息的完整性。
数据以多文件的形式表现
根据getSplits方法的具体实现,由于数据划分的时候是以文件为单位进行遍历的,当一个文件最后的部分不足splitSize时,将单独组成一个InputSplit,不会与下一个文件中的数据进行合并,因此不同文件中的数据不会被分到同一个InputSplit中。