CarbonData源码阅读(2)-Hadoop InputFormat

Presto Integration之前已经读过了:http://blog.csdn.net/bhq2010/article/details/72972278
这个里面沿着CarbondataPlugin –> CarbondataConnectory –> CarbondataConnector –> CarbondataMetadata (以及org.apache.carbondata.presto包下面的其他类) –> CarbonTableReader这条路径追下去,会发现最后涉及到的很多逻辑都在:

org.apache.carbondata.core.datastore.filesystem.CarbonFile 
//及其具体实现HDFSCarbonFile、AlluxioCarbonFile、LocalCarbonFile,
// 一个CarbonFile可能是存储系统中的一个文件或者目录,一个CarbonFile实例上主要的操作listFile,即列出子文件和目录
org.apache.carbondata.core.datastore.impl.FileFactory // 在存储系统上读写CarbonFile
org.apache.carbondata.core.datastore.block.* // 数据块、索引相关的代码
org.apache.carbondata.core.metadata.CarbonMetadata // 一个单例,封装了Carbondata元数据相关的操作
org.apache.carbondata.core.metadata.schema.table.CarbonTable 
// 对一个Carbondata表的抽象,关于一张表的各种操作都在这里
org.apache.carbondata.scan.filter.FilterExpressionProcessor 
// 一个查询过滤条件的表达式,用来在读取Carbondata时过滤掉不相关的数据块
org.apache.carbondata.hadoop.CacheClient 
// 一个命名很神奇的类,大概就是一个segment在内存中的B+tree,确切说是一个segment对应的多棵B+tree。
org.apache.carbondata.hadoop.util.CarbonInputFormatUtil 
// 这个命名也比较容易误会,其实是一个和查询(也就是根据条件读取Carbondata中的数据,不是SQL层面的那种复杂查询)相关的工具类,
// 里面一大堆静态方法,比如生成查询计划、生成CarbondataInputFormat实例等等
org.apache.carbondata.hadoop.readsupport.CarbonReadSupport 
// 这个命名也是有点不知所云,其实是负责将RecordReader读上来的数据整成row的形式

综上:
1. 应该先读一下carbondata hadoop module下面的代码,这样有助于理解Carbondata在HDFS和Mapreduce环境下的工作原理
2. hadoop module下面代码的命名比较奇葩,不知道是谁写的,哈哈,不仔细读代码你很可能想不到一个类、一个对象到底是干嘛用的

所以就来读一读。

CarbonInputFormat

CarbonInputFormat的定义如下,它实现了Hadoop FileInputFormat模板,可以作为MapReduce的输入格式。

public class CarbonInputFormat<T> extends FileInputFormat<Void, T>

一个CarbonInputFormat实例对应一张Carbondata表。这里面有如下的一些carbondata需要的配置项,以及初始化和往Hadoop Configuration中添加这些配置项的方法。

  // comma separated list of input segment numbers
  public static final String INPUT_SEGMENT_NUMBERS =
      "mapreduce.input.carboninputformat.segmentnumbers";
  // comma separated list of input files
  public static final String INPUT_FILES =
      "mapreduce.input.carboninputformat.files";
  private static final String FILTER_PREDICATE =
      "mapreduce.input.carboninputformat.filter.predicate";
  private static final String COLUMN_PROJECTION = "mapreduce.input.carboninputformat.projection";
  private static final String CARBON_TABLE = "mapreduce.input.carboninputformat.table";
  private static final String CARBON_READ_SUPPORT = "mapreduce.input.carboninputformat.readsupport";

Hadoop Configuration中存放的是String类型的key-value,因此,像CARBON_TABLE之类的自定义类型的value会被ObjectSerializationUtil序列化成一个字符串,需要的时候再反序列化回来。ObjectSerializationUtil是org.apache.carbondata.hadoop.util下的一个工具类。

Populate Carbon Table

注意CarbonInputFormat中一个重要的方法:

  /**
   * this method will read the schema from the physical file and populate into CARBON_TABLE
   * @param configuration
   * @throws IOException
   */
  private static void populateCarbonTable(Configuration configuration) throws IOException {
    String dirs = configuration.get(INPUT_DIR, "");
    String[] inputPaths = StringUtils.split(dirs);
    if (inputPaths.length == 0) {
      throw new InvalidPathException("No input paths specified in job");
    }
    AbsoluteTableIdentifier absoluteTableIdentifier =
        AbsoluteTableIdentifier.fromTablePath(inputPaths[0]);
    // read the schema file to get the absoluteTableIdentifier having the correct table id
    // persisted in the schema
    CarbonTable carbonTable = SchemaReader.readCarbonTableFromStore(absoluteTableIdentifier);
    setCarbonTable(configuration, carbonTable);
  }

看注释就知道它的用途,主要注意其中用到的两个类:AbsoluteTableIdentifier和SchemaReader。
AbsoluteTableIdentifier是org.apache.carbondata.core.metadata下的类,里面保存这一个表的绝对路径、是否本地文件、以及一个对应的CarbonTableIdentifier实例(里面有table id,table name,database name)。
SchemaReader也是org.apache.carbondata.hadoop.util下的一个工具类。SchemaReader上面那句注释不知道在说啥,其实readCarbonTableFromStore就是用了一个ThriftReader去读取一个carbon table的schema file,得到一个org.apache.carbondata.core.metadata.schema.table.TableInfo(区别于thrift的TableInfo)对象,然后创建出一个CarbonTable对象。ThriftReader是由Thrift根据carbondata源码的format module下的thrift文件生成出来的。生成出来的Java代码编译打包之后就是carbondata-format-xxx.jar。但是carbondata源码在编译的时候并不会去用thrift生成源码并编译,而是直接通过maven获取carbondata-format-xxx的依赖。

TableInfo

org.apache.carbondata.core.metadata.schema.table.TableInfo是个有意思的类,其中除了databaseName、storePath(表数据文件存储路径)、metadataFilePath(元数据文件的存储路径)以及一些时间戳之外,还有tableUniqueName、factTable、aggregateTableList,其中tableUniqueName的注释写到:

  /**
   * table name to group fact table and aggregate table
   */
  private String tableUniqueName;

可以猜测:carbondata用了类似星型模型的数据模型,一张事实表周围有一些维表,只是维表不叫dimension table,而是叫aggregate table,可能是因为维表上的字段通常会出现在group by中。可是table unique name的命名很费解,注释也很费解,看来hadoop module代码的特点在core中也有体现 : ) 通过TableInfo中setTableUniqueName()方法的usage可以看出来,其实tableUniqueName就是用下划线把database name和table name拼接起来,比如这样:

info.setTableUniqueName("carbonTestDatabase_carbonTestTable");
wrapperTableInfo.setTableUniqueName(dbName + "_" + tableName);

也就是说,TableInfo中不光有一张表的基本信息,还有这张表所属的事实表,以及事实表对应的维表的信息。
有个疑问是:Carbondata中事实表和维表是怎么存储的?他们的元数据又是怎么存储的?这个可以去看format module下面的thrift代码,schema.thrift文件中TableInfo的定义和core中的TableInfo是对应的,可以帮助理解。目前猜测是:carbondata将一个database的元数据单独存储在一个文件中,这个文件对应的就是TableInfo,也就是说TableInfo是一个database中的事实表和维表的信息集合,那么carbondata就是采用了严格的星型模型,那么tableUniqueName到底是啥东西?factTable的名字?此外,从carbondata.thrift可以看出,在Carbondata的某个表的数据文件中,有file header和file footer,这里面有column schema信息。

TableSchema

TableInfo中用到了TableSchema来表示一个表的schema,这各类在org.apache.carbondata.core.metadata.schema.table中,但是对应这schema.thrift中的TableSchema。TableSchema中有这么两个东西:

  /**
   * Information about bucketing of fields and number of buckets
   */
  private BucketingInfo bucketingInfo;

  /**
   * Information about partition type, partition column, numbers
   */
  private PartitionInfo partitionInfo;

这两个怎么理解?其实在CarbonTable当中也有BucketingInfo和PartitionInfo,那么这里的Bucket是否和之前Presto中发现的SegmentTaskIndexStore.TaskBucketHolder有关?先放着,后面慢慢探索。

getSplits

作为Hadoop的InputFormat,最重要的方法就是getSplits。CarbonInputFormat中也实现了这个方法,并且除了上面说的配置相关的代码,其他代码都是在实现这个方法,用了大几百行的代码,里面的逻辑和presto connector中的CarbonTableReader差不多,如下:

1. 从configuration中拿到一个table identifier
2. 通过table identifier拿到cache client,cache client详见http://blog.csdn.net/bhq2010/article/details/72972278#t4
3. 通过table identifier拿到valid segments,放到configuration中备用
4. 从cache client中去除invalid segments,备用
5. 从configuration中拿到predicate(也就是查询的过滤条件)和carbon table,借助org.apache.carbondata.hadoop.util.CarbonInputFormatUtil和org.apache.carbondata.core.scan.filter.resolver.FilterResolverIntf应用过滤条件,过滤掉不相关的**partition(这个和Hive中的partition有对应关系吗?后面再继续读)**,并从相关的partitions中读取blocks,一个block对应一个CarbonInputSplit。然后为每一个CarbonInputSplit标记出invalid segments,这样task(暂时我也不知道task是啥。。。)就可以知道哪些segments是invalid的(我也不知道为啥不直接过滤掉这些invalid segments,以及为啥通过CarbonInputSplit这么个东西来传递这个信息,感觉carbondata的代码耦合比较紧,有点乱,看着心里发毛)。

getSplits返回了一个CarbonInputSplit的List,更具体的实现就不仔细看了,有点头疼。。。

CarbonInputSplit

终于看到这里了。严格来讲,CarbonInputSplit对应于一个HDFS file split,通常是一个只有一个block的HDFS file。

TaskId和BucketId

在CarbonInputSplit的一个私有构造函数中,可以稍微了解一点我们一直想知道的Task和bucket是怎么回事:

  private CarbonInputSplit(String segmentId, Path path, long start, long length, String[] locations,
      ColumnarFormatVersion version, String[] deleteDeltaFiles) {
    super(path, start, length, locations);
    this.segmentId = segmentId;
    String taskNo = CarbonTablePath.DataFileUtil.getTaskNo(path.getName());
    if (taskNo.contains("_")) {
      taskNo = taskNo.split("_")[0];
    }
    this.taskId = taskNo;
    this.bucketId = CarbonTablePath.DataFileUtil.getBucketNo(path.getName());
    this.invalidSegments = new ArrayList<>();
    this.version = version;
    this.deleteDeltaFiles = deleteDeltaFiles;
  }

CarbonTablePath.DataFileUtil.getTaskNo(path.getName());这一句其实就是把carbondata的数据表的数据文件的一个block的文件名中第二和第三个’-‘之间的sub string。这个block的文件名到底是从哪读出来的?从CarbonInputFormat中创建CarbonInputSplit的代码看出来,这个文件名是TableBlockInfo中的file path,通过查找TableBlockInfo构造函数的usage可以发现,这个文件名是这样的:
part-0-0_batchno0-0-1498503709664.carbondata
但是getTaskNo的注释说这个TaskNo是个update timestamp,晕了。。。Anyway,最后是把split(Carbondata中一个split就是一个HDFS file)文件名中取出了一小段作为taskId。

同样滴,我们也能发现,bucket id也是从split文件名中取出来的一小段,即第三和第四个’-‘之间的sub string。

CarbonInputSplit中还有创建Block的方法,就是从一个split创建出一个TableBlockInfo对象。

CarbonInputSplit有两个重要的方法是readFields和write,这是InputSplit子类必须实现的方法。这实际上这是一对反序列化和序列化方法,可以从输入中读取出split实例中各个字段的值,也可以将一个split实例序列化到输出。至于输入和输出是啥,暂时我也不知道。

其他的方法都没啥,随便看看就好了。

CarbonMultiBlockSplit和QueryModel

此外,还有一个CarbonMultiBlockSplit,就是把同一个HDFS节点上的split(HDFS block)包装成一个大的split,实际上是对并发做优化,因为在机器计算资源有限、且并发查询较多时,split太多反而会增加额外的开销。这个优化在CarbonInputFormat的getQueryModel方法中用到了。

public QueryModel getQueryModel(InputSplit inputSplit, TaskAttemptContext taskAttemptContext)

这个方法中解析projection信息得到一个CarbonQueryPlan实例。CarbonQueryPlan当中的注释写得比较好,有例子,一个select语句可以被解析成一个CarbonQueryPlan实例。然后通过这个实例创建一个QueryModel实例。

CarbonRecordReader

在CarbonInputFormat中的createRecordReader方法为一个task在一个split上创建了一个record reader:

  @Override public RecordReader createRecordReader(InputSplit inputSplit,
      TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
    Configuration configuration = taskAttemptContext.getConfiguration();
    QueryModel queryModel = getQueryModel(inputSplit, taskAttemptContext);
    CarbonReadSupport readSupport = getReadSupportClass(configuration);
    return new CarbonRecordReader(queryModel, readSupport);
  }

这个方法是CarbonInputFormat中一个非常重要的方法。可以看出,CarbonRecordReader拿到了一个queryModel,即查询计划;以及一个readSupport、用来将文件中读出来的数据转换成row的形式。

在CarbonRecordReader的构造方法中,除了set queryModel和readSupport之外,创建了一个QueryExecutor实例:

  public CarbonRecordReader(QueryModel queryModel, CarbonReadSupport readSupport) {
    this.queryModel = queryModel;
    this.readSupport = readSupport;
    this.queryExecutor = QueryExecutorFactory.getQueryExecutor(queryModel);
  }

这个接口在org.apache.carbondata.core.scan.executor包下,定义如下:

/**
 * Interface for carbon query executor.
 * Will be used to execute the query based on the query model
 * and will return the iterator over query result
 */
public interface QueryExecutor<E> {

  /**
   * Below method will be used to execute the query based on query model passed from driver
   *
   * @param queryModel query details
   * @return query result iterator
   * @throws QueryExecutionException if any failure while executing the query
   * @throws IOException if fail to read files
   */
  CarbonIterator execute(QueryModel queryModel)
      throws QueryExecutionException, IOException;

  /**
   * Below method will be used for cleanup
   *
   * @throws QueryExecutionException
   */
  void finish() throws QueryExecutionException;
}

注释说的比较清楚了。这个接口的实现在org.apache.carbondata.core.scan.executor.impl包下面,主要就是实现execute方法,返回一个CarbonIterator实例。CarbonIterator定义在org.apache.carbondata.common包下面,实现很多,到处都是。CarbonRecordReader里面用到的是VectorDetailQueryExecutor或者DetailQueryExecutor,对应得到的也就是VectorDetailQueryResultIterator和DetailQueryResultIterator。至于这里面的具体实现,暂时先不看了。大概看了下,Iterator里面貌似会用一个线程(ExecutorService.submit(…))去读数据。所以猜测Iterator可能是异步I/O的。

CarbonRecordReader其他就没啥了,在initialize方法中初始化iterator,其他方法就是用iterator和readSupport去读取和解析数据了。

你可能感兴趣的:(数据库,Web/数据/云计算)