Carbondata源码系列(一)文件生成过程
在滴滴的两年一直在加班,人也变懒了,就很少再写博客了,最近在进行Carbondata和hive集成方面的工作,于是乎需要对Carbondata进行深入的研究。
于是新开一个系列,记录自己学习Carbondata的点点滴滴,希望对大家也有所帮助。
1、环境准备
当前版本是1.2.0-SNAPSHOT
git clone https://github.com/apache/carbondata.git
先用IDEA打开carbondata的代码,点击上方的View -> Tool Windows -> Maven Projects, 先勾选一下需要的profile和编译format工程,如下图所示:

2、探寻代码入口
我们先打开入口类CarbonDataFrameWriter,找到writeToCarbonFile这个方法
private def writeToCarbonFile(parameters: Map[String, String] = Map()): Unit = {
val options = new CarbonOption(parameters)
val cc = CarbonContext.getInstance(dataFrame.sqlContext.sparkContext)
if (options.tempCSV) {
loadTempCSV(options, cc)
} else {
loadDataFrame(options, cc)
}
}
它有两个方式,loadTempCSV和loadDataFrame。
loadTempCSV是先生成CSV文件,再调用LOAD DATA INPATH...的命令导入数据。
这里我们之研究loadDataFrame这种直接生成数据的方式。
一路点进去,目标落在carbonTableSchema的LoadTable的run方法里,接着就是洋洋洒洒的二百行的set代码。它是核心其实是构造一个CarbonLoadModel类。
val carbonLoadModel = new CarbonLoadModel()
carbonLoadModel.setTableName(relation.tableMeta.carbonTableIdentifier.getTableName)
carbonLoadModel.setDatabaseName(relation.tableMeta.carbonTableIdentifier.getDatabaseName)
carbonLoadModel.setStorePath(relation.tableMeta.storePath)
val table = relation.tableMeta.carbonTable
carbonLoadModel.setAggTables(table.getAggregateTablesName.asScala.toArray)
carbonLoadModel.setTableName(table.getFactTableName)
val dataLoadSchema = new CarbonDataLoadSchema(table)
// Need to fill dimension relation
carbonLoadModel.setCarbonDataLoadSchema(dataLoadSchema)
这些代码为了Load一个文本文件准备的,如果是用dataframe的方式则不需要看了。直接略过,直接调到if (carbonLoadModel.getUseOnePass)这一句。
这个跟字典的生成方式有关,这个值默认是false,先忽略true的过程吧,看主流程就行,下面这哥俩才是我们要找的。
// 生成字典文件
GlobalDictionaryUtil
.generateGlobalDictionary(
sparkSession.sqlContext,
carbonLoadModel,
relation.tableMeta.storePath,
dictionaryDataFrame)
// 生成数据文件
CarbonDataRDDFactory.loadCarbonData(sparkSession.sqlContext,
carbonLoadModel,
relation.tableMeta.storePath,
columnar,
partitionStatus,
None,
loadDataFrame,
updateModel)
3、字段生成过程
先看GlobalDictionaryUtil.generateGlobalDictionary方法
if (StringUtils.isEmpty(allDictionaryPath)) {
LOGGER.info("Generate global dictionary from source data files!")
// load data by using dataSource com.databricks.spark.csv
var df = dataFrame.getOrElse(loadDataFrame(sqlContext, carbonLoadModel))
var headers = carbonLoadModel.getCsvHeaderColumns
headers = headers.map(headerName => headerName.trim)
val colDictFilePath = carbonLoadModel.getColDictFilePath
if (colDictFilePath != null) {
// generate predefined dictionary
generatePredefinedColDictionary(colDictFilePath, carbonTableIdentifier,
dimensions, carbonLoadModel, sqlContext, storePath, dictfolderPath)
}
if (headers.length > df.columns.length) {
val msg = "The number of columns in the file header do not match the " +
"number of columns in the data file; Either delimiter " +
"or fileheader provided is not correct"
LOGGER.error(msg)
throw new DataLoadingException(msg)
}
// use fact file to generate global dict
val (requireDimension, requireColumnNames) = pruneDimensions(dimensions,
headers, df.columns)
if (requireDimension.nonEmpty) {
// select column to push down pruning
df = df.select(requireColumnNames.head, requireColumnNames.tail: _*)
val model = createDictionaryLoadModel(carbonLoadModel, carbonTableIdentifier,
requireDimension, storePath, dictfolderPath, false)
// combine distinct value in a block and partition by column
val inputRDD = new CarbonBlockDistinctValuesCombineRDD(df.rdd, model)
.partitionBy(new ColumnPartitioner(model.primDimensions.length))
// generate global dictionary files
val statusList = new CarbonGlobalDictionaryGenerateRDD(inputRDD, model).collect()
// check result status
checkStatus(carbonLoadModel, sqlContext, model, statusList)
} else {
LOGGER.info("No column found for generating global dictionary in source data files")
}
} else {
generateDictionaryFromDictionaryFiles(sqlContext,
carbonLoadModel,
storePath,
carbonTableIdentifier,
dictfolderPath,
dimensions,
allDictionaryPath)
}
包含了两种情况:不存在字典文件和已存在字段文件。
先看不存在的情况
// use fact file to generate global dict
val (requireDimension, requireColumnNames) = pruneDimensions(dimensions,
headers, df.columns)
if (requireDimension.nonEmpty) {
// 只选取标记为字典的维度列
df = df.select(requireColumnNames.head, requireColumnNames.tail: _*)
val model = createDictionaryLoadModel(carbonLoadModel, carbonTableIdentifier,
requireDimension, storePath, dictfolderPath, false)
// 去重之后按列分区
val inputRDD = new CarbonBlockDistinctValuesCombineRDD(df.rdd, model)
.partitionBy(new ColumnPartitioner(model.primDimensions.length))
// 生成全局字段文件
val statusList = new CarbonGlobalDictionaryGenerateRDD(inputRDD, model).collect()
// check result status
checkStatus(carbonLoadModel, sqlContext, model, statusList)
} else {
LOGGER.info("No column found for generating global dictionary in source data files")
}
先从源文件当中读取所有维度列,去重之后按列分区,然后输出,具体输出的过程请看CarbonGlobalDictionaryGenerateRDD的internalCompute方法。
val dictWriteTask = new DictionaryWriterTask(valuesBuffer,
dictionaryForDistinctValueLookUp,
model.table,
model.columnIdentifier(split.index),
model.hdfsLocation,
model.primDimensions(split.index).getColumnSchema,
model.dictFileExists(split.index)
)
// execute dictionary writer task to get distinct values
val distinctValues = dictWriteTask.execute()
val dictWriteTime = System.currentTimeMillis() - t3
val t4 = System.currentTimeMillis()
// if new data came than rewrite sort index file
) {
val sortIndexWriteTask = new SortIndexWriterTask(model.table,
model.columnIdentifier(split.index),
model.primDimensions(split.index).getDataType,
model.hdfsLocation,
dictionaryForDistinctValueLookUp,
distinctValues)
sortIndexWriteTask.execute()
}
val sortIndexWriteTime = System.currentTimeMillis() - t4
CarbonTimeStatisticsFactory.getLoadStatisticsInstance.recordDicShuffleAndWriteTime()
// After sortIndex writing, update dictionaryMeta
dictWriteTask.updateMetaData()
字典文件在表目录的下的Metadata目录下,它需要生成三种文件
1、字段文件,命令方式为 列ID.dict
2、sort index文件,命令方式为 列ID.sortindex
3、字典列的meta信息,命令方式为 列ID.dictmeta
4、数据生成过程
请打开CarbonDataRDDFactory,找到loadCarbonData这个方法,方法里面包括了从load命令和从dataframe加载的两种方式,代码看起来是有点儿又长又臭的感觉。我们只关注loadDataFrame的方式就好。
def loadDataFrame(): Unit = {
try {
val rdd = dataFrame.get.rdd
// 获取数据的位置
val nodeNumOfData = rdd.partitions.flatMap[String, Array[String]]{ p =>
DataLoadPartitionCoalescer.getPreferredLocs(rdd, p).map(_.host)
}.distinct.size
// 确保executor数量要和数据的节点数一样多
val nodes = DistributionUtil.ensureExecutorsByNumberAndGetNodeList(nodeNumOfData,
sqlContext.sparkContext)
val newRdd = new DataLoadCoalescedRDD[Row](rdd, nodes.toArray.distinct)
// 生成数据文件
status = new NewDataFrameLoaderRDD(sqlContext.sparkContext,
new DataLoadResultImpl(),
carbonLoadModel,
currentLoadCount,
tableCreationTime,
schemaLastUpdatedTime,
newRdd).collect()
} catch {
case ex: Exception =>
LOGGER.error(ex, "load data frame failed")
throw ex
}
}
打开NewDataFrameLoaderRDD类,查看internalCompute方法,这个方法的核心是这句话
new DataLoadExecutor().execute(model, loader.storeLocation, recordReaders.toArray)
打开DataLoadExecutor,execute方法里面的核心是DataLoadProcessBuilder的build方法,根据表不同的参数设置,DataLoadProcessBuilder的build过程会有一些不同
public AbstractDataLoadProcessorStep build(CarbonLoadModel loadModel, String storeLocation,
CarbonIterator[] inputIterators) throws Exception {
CarbonDataLoadConfiguration configuration = createConfiguration(loadModel, storeLocation);
SortScopeOptions.SortScope sortScope = CarbonDataProcessorUtil.getSortScope(configuration);
if (!configuration.isSortTable() || sortScope.equals(SortScopeOptions.SortScope.NO_SORT)) {
// 没有排序列或者carbon.load.sort.scope设置为NO_SORT的
return buildInternalForNoSort(inputIterators, configuration);
} else if (configuration.getBucketingInfo() != null) {
// 设置了Bucket的表
return buildInternalForBucketing(inputIterators, configuration);
} else if (sortScope.equals(SortScopeOptions.SortScope.BATCH_SORT)) {
// carbon.load.sort.scope设置为BATCH_SORT
return buildInternalForBatchSort(inputIterators, configuration);
} else {
return buildInternal(inputIterators, configuration);
}
}
下面仅介绍标准的导入过程buildInternal:
private AbstractDataLoadProcessorStep buildInternal(CarbonIterator[] inputIterators,
CarbonDataLoadConfiguration configuration) {
// 1. Reads the data input iterators and parses the data.
AbstractDataLoadProcessorStep inputProcessorStep =
new InputProcessorStepImpl(configuration, inputIterators);
// 2. Converts the data like dictionary or non dictionary or complex objects depends on
// data types and configurations.
AbstractDataLoadProcessorStep converterProcessorStep =
new DataConverterProcessorStepImpl(configuration, inputProcessorStep);
// 3. Sorts the data by SortColumn
AbstractDataLoadProcessorStep sortProcessorStep =
new SortProcessorStepImpl(configuration, converterProcessorStep);
// 4. Writes the sorted data in carbondata format.
return new DataWriterProcessorStepImpl(configuration, sortProcessorStep);
}
主要是分4个步骤:
1、读取数据,并进行格式转换,这一步骤是读取csv文件服务的,dataframe的数据格式都已经处理过了
2、根据字段的数据类型和配置,替换掉字典列的值;非字典列会被替换成byte数组
3、按照Sort列进行排序
4、把数据用Carbondata的格式输出
下面我们从第二步DataConverterProcessorStepImpl开始说起,在getIterator方法当中,会发现每一个CarbonRowBatch都要经过localConverter的convert方法转换,localConverter中只有RowConverterImpl一个转换器。
RowConverterImpl由很多的FieldConverter组成,在initialize方法中可以看到它是由FieldEncoderFactory的createFieldEncoder方法生成的。
public FieldConverter createFieldEncoder(DataField dataField,
Cache<DictionaryColumnUniqueIdentifier, Dictionary> cache,
CarbonTableIdentifier carbonTableIdentifier, int index, String nullFormat,
DictionaryClient client, Boolean useOnePass, String storePath, boolean tableInitialize,
Map<Object, Integer> localCache, boolean isEmptyBadRecord)
throws IOException {
// Converters are only needed for dimensions and measures it return null.
if (dataField.getColumn().isDimension()) {
if (dataField.getColumn().hasEncoding(Encoding.DIRECT_DICTIONARY) &&
!dataField.getColumn().isComplex()) {
return new DirectDictionaryFieldConverterImpl(dataField, nullFormat, index,
isEmptyBadRecord);
} else if (dataField.getColumn().hasEncoding(Encoding.DICTIONARY) &&
!dataField.getColumn().isComplex()) {
return new DictionaryFieldConverterImpl(dataField, cache, carbonTableIdentifier, nullFormat,
index, client, useOnePass, storePath, tableInitialize, localCache, isEmptyBadRecord);
} else if (dataField.getColumn().isComplex()) {
return new ComplexFieldConverterImpl(
createComplexType(dataField, cache, carbonTableIdentifier,
client, useOnePass, storePath, tableInitialize, localCache), index);
} else {
return new NonDictionaryFieldConverterImpl(dataField, nullFormat, index, isEmptyBadRecord);
}
} else {
return new MeasureFieldConverterImpl(dataField, nullFormat, index, isEmptyBadRecord);
}
}
从这段代码当中可以看出来,它是分成了几种类型的
1、维度类型,编码方式为Encoding.DIRECT_DICTIONARY的非复杂列,采用DirectDictionaryFieldConverterImpl (主要是TIMESTAMP和DATE类型),换算成值和基准时间的差值
2、维度类型,编码方式为Encoding.DICTIONARY的非复杂列,采用DictionaryFieldConverterImpl (非高基数的字段类型),把字段换成字典中的key(int类型)
3、维度类型,复杂列,采用ComplexFieldConverterImpl (复杂字段类型,Sturct和Array类型),把字段转成二进制
4、维度类型,高基数列,采用NonDictionaryFieldConverterImpl,原封不动,原来是啥样,现在还是啥样
5、指标类型,采用MeasureFieldConverterImpl (值类型,float、double、int、bigint、decimal等),原封不动,原来是啥样,现在还是啥样
第三步SortProcessorStepImpl,关键点在SorterFactory.createSorter是怎么实现的
public static Sorter createSorter(CarbonDataLoadConfiguration configuration, AtomicLong counter) {
boolean offheapsort = Boolean.parseBoolean(CarbonProperties.getInstance()
.getProperty(CarbonCommonConstants.ENABLE_UNSAFE_SORT,
CarbonCommonConstants.ENABLE_UNSAFE_SORT_DEFAULT));
SortScopeOptions.SortScope sortScope = CarbonDataProcessorUtil.getSortScope(configuration);
Sorter sorter;
if (offheapsort) {
if (configuration.getBucketingInfo() != null) {
sorter = new UnsafeParallelReadMergeSorterWithBucketingImpl(configuration.getDataFields(),
configuration.getBucketingInfo());
} else {
sorter = new UnsafeParallelReadMergeSorterImpl(counter);
}
} else {
if (configuration.getBucketingInfo() != null) {
sorter =
new ParallelReadMergeSorterWithBucketingImpl(counter, configuration.getBucketingInfo());
} else {
sorter = new ParallelReadMergeSorterImpl(counter);
}
}
if (sortScope.equals(SortScopeOptions.SortScope.BATCH_SORT)) {
if (configuration.getBucketingInfo() == null) {
sorter = new UnsafeBatchParallelReadMergeSorterImpl(counter);
} else {
LOGGER.warn(
"Batch sort is not enabled in case of bucketing. Falling back to " + sorter.getClass()
.getName());
}
}
return sorter;
}
居然还可以使用堆外内存sort,设置enable.unsafe.sort为true就可以开启了。我们看默认的ParallelReadMergeSorterImpl吧。
超过100000条记录就要把数据排序,然后生成一个文件,文件数超过20个文件之后,就要做一次文件合并。
规则在NewRowComparatorForNormalDims当中,从规则上可以看出来,需要排序的列一定是在所有数据的前N列,而不会是随机散落的
public int compare(Object[] rowA, Object[] rowB) {
;
; i < numberOfSortColumns; i++) {
int dimFieldA = (int)rowA[i];
int dimFieldB = (int)rowB[i];
diff = dimFieldA - dimFieldB;
) {
return diff;
}
}
return diff;
}
相关参数:
carbon.sort.size 100000
carbon.sort.intermediate.files.limit 20
到最后一步了,打开DataWriterProcessorStepImpl类,它是通过CarbonFactHandlerFactory.createCarbonFactHandler生成一个CarbonFactHandler,通过CarbonFactHandler的addDataToStore方法处理CarbonRow
addDataToStore的实现很简单,当row的数量达到一个blocklet的大小之后,就往线程池里提交一个异步的任务Producer进行处理
public void addDataToStore(CarbonRow row) throws CarbonDataWriterException {
dataRows.add(row);
this.entryCount++;
// if entry count reaches to leaf node size then we are ready to write
// this to leaf node file and update the intermediate files
if (this.entryCount == this.blockletSize) {
try {
semaphore.acquire();
producerExecutorServiceTaskList.add(
producerExecutorService.submit(
new Producer(blockletDataHolder, dataRows, ++writerTaskSequenceCounter, false)
)
);
blockletProcessingCount.incrementAndGet();
// set the entry count to zero
processedDataCount += entryCount;
LOGGER.info("Total Number Of records added to store: " + processedDataCount);
dataRows = new ArrayList<>(this.blockletSize);
;
} catch (InterruptedException e) {
LOGGER.error(e, e.getMessage());
throw new CarbonDataWriterException(e.getMessage(), e);
}
}
}
这里用到了生产者消费者的模式,Producer的处理是多线程的,Consumer是单线程的;Producer主要是负责数据的压缩,Consumer负责进行输出,数据的交换通过blockletDataHolder。
相关参数:
carbon.number.of.cores.while.loading 2 (Producer的线程数)
carbon.blocklet.size 120000
文件生成主要包含以上过程,限于文章篇幅,下一章再继续接着写Carbondata的数据文件格式细节。
Carbondata源码系列(一)文件生成过程的更多相关文章
- Carbondata源码系列(二)文件格式详解
在上一章当中,写了文件的生成过程.这一章主要讲解文件格式(V3版本)的具体细节. 1.字典文件格式详解 字典文件的作用是在存储的时候将字符串等类型转换为int类型,好处主要有两点: 1.减少存储占用空 ...
- 大白话Vue源码系列(02):编译器初探
阅读目录 编译器代码藏在哪 Vue.prototype.$mount 构建 AST 的一般过程 Vue 构建的 AST 题接上文,上回书说到,Vue 的编译器模块相对独立且简单,那咱们就从这块入手,先 ...
- 大白话Vue源码系列(03):生成AST
阅读目录 AST 节点定义 标签的正则匹配 解析用到的工具方法 解析开始标签 解析结束标签 解析文本 解析整块 HTML 模板 未提及的细节 本篇探讨 Vue 根据 html 模板片段构建出 AST ...
- 大白话Vue源码系列(03):生成render函数
阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ...
- 大白话Vue源码系列(04):生成render函数
阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ...
- MyBatis 源码分析 - 映射文件解析过程
1.简介 在上一篇文章中,我详细分析了 MyBatis 配置文件的解析过程.由于上一篇文章的篇幅比较大,加之映射文件解析过程也比较复杂的原因.所以我将映射文件解析过程的分析内容从上一篇文章中抽取出来, ...
- Ioc容器beanDefinition-Spring 源码系列(1)
Ioc容器beanDefinition-Spring 源码系列(1) 目录: Ioc容器beanDefinition-Spring 源码(1) Ioc容器依赖注入-Spring 源码(2) Ioc容器 ...
- Chromium源码系列一:Chromium简介及源代码获取和编译
Chromium源码系列一:Chromium简介及源代码获取和编译 Chromium简介 Chromium是一个由Google主导开发的网页浏览器,以BSD许可证等多重自由版权发行并开放源代码.C ...
- 11 hbase源码系列(十一)Put、Delete在服务端是如何处理
hbase源码系列(十一)Put.Delete在服务端是如何处理? 在讲完之后HFile和HLog之后,今天我想分享是Put在Region Server经历些了什么?相信前面看了<HTab ...
随机推荐
- JAVA网络编程TCP通信
Socket简介: Socket称为"套接字",描述IP地址和端口.在Internet上的主机一般运行多个服务软件,同时提供几种服务,每种服务都打开一个Socket,并绑定在一个端 ...
- 如何有效的跟踪线上 MySQL 实例表和权限的变更
介绍 从系统管理员或 DBA 的角度来讲, 总期望将线上的各种变更限制在一个可控的范围内, 减少一些不确定的因素. 这样做有几点好处: . 记录线上的库表变更; . 对线上的库表变更有全局的了解; . ...
- javascript基础-DOM原理
解释清楚DOM原理并不是一件容易的事,但是任何一个前端工程师,都必须牢牢掌握它. DOM整体架构: 图解: DOM,即针对XML文档的应用程序编程接口(API).通俗一点说,HTML属于XML的一种, ...
- 常见类——Object
1.在Java类继承结构中Java.lang.Object类位于顶端 2.如果定义一个Object类没有使用extends关键字声明其父类,则其父类为Java.lang.Object类 3.O ...
- Spring学习(11)---JSR-250标准注解之 @Resource、@PostConstruct、@PreDestroy
1)@Resource(JSR-250标准注解,推荐使用它来代替Spring专有的@Autowired注解) Spring 不但支持自己定义的@Autowired注解,还支持几个由JSR-250规范定 ...
- HTML 简单了解
HTML 特别的通俗易懂!想学自己制作网页的,就来我这看看吧! 首先 我先介绍一下什么是HTML! HTML是用来描述网页的一种语言!他结合CSS样式之后会有非常炫酷的样式! 1.HTML是指一种超文 ...
- Chapter 1:Introduction
作者:桂. 时间:2017-05-24 08:06:45 主要是<Speech enhancement: theory and practice>的读书笔记,全部内容可以点击这里. 1. ...
- cron的用法
linux中的Cron命令是Linux的内置服务,用于定时的循环的服务. 1.启动.重启.关闭这个服务: /sbin/service crond start //启动服务 /sbin/service ...
- Bash命令行编辑
1.Readline库和命令行编辑 bash shell提供了两个内置编辑器:emacs和vi,利用它们可以以交互模式对命令行列表进行编辑,这项特性是通过Readline库的软件包实现的.当使用命令行 ...
- Tenacity——Exception Retry 从此无比简单
Python 装饰器装饰类中的方法这篇文章,使用了装饰器来捕获代码异常.这种方式可以让代码变得更加简洁和Pythonic. 在写代码的过程中,处理异常并重试是一个非常常见的需求.但是如何把捕获异常并重 ...