CarbonData典型应用场景之明细数据查询:点查+过滤条件

背景

​ 本文主要描述在使用CarbonData进行明细数据查询时,如何设置建表、加载、查询时的参数,以提升对应的性能。具体来说,包括在建表时如何选择合适的字典、SORT_COLUMNS、SORT_SCOPE等配置,同时我们还给出了验证场景下的环境规格、数据规格等,同时给出了配置项及对应达到的数据加载和查询性能,以便大家根据自己的业务特点和场景选择进行参数调整。

​ 本文中用例数据表特点:

​ 1.表记录数都比较大,范围从数千万到数百亿。

​ 2.列数比较多,大约在100-600列之间。

​ 使用特点:

​ 1.主要是进行点查和过滤,没有汇聚计算,偶尔有关联维表的场景。

​ 2.数据入库采取分批入库的方式,周期约为5分钟,按天建表。

​ 3.查询时可能有不少于20的并发查询。

典型的查询的使用框架,其中第五个求sum仅为作性能对比。

编号描述查询SQL样例
1点查select * from table where id_a=' ' limit 1000;
2模糊查询select * from table where id_a like ‘1234%’ limit 1000;
3求记录总数select count(1) from table;
4求最大/最小值select max(id_a), min(id_a) from table;
5求sum(仅为了做性能对比)select sum(id_a) from table;

数据的特点,列主要是以int, bigint, string列构成,描述一些号码列,时间列,ID列等,无复杂数据类型。

测试环境

集群CPUvCoreMemory硬盘描述
Hadoop集群Gold 6132 CPU@2.60GZ56256GBSATA磁盘,4T数据盘2个namenode,6个datanode, 查询队列分配1/6的资源,等同于一个节点

建表 Create table

建表 Create table 时字典的选择

​ 建表时用户可以选择使用本地字典或者不使用字典。通常情况下使用本地字典数据加载时比较耗时,但是查询时较快; 而不使用本地字典时加载较快,查询不如使用本地字典。用户在实际使用时可以根据自身业务的诉求来选择合适的模式。关于CarbonData本地字典的具体描述可以参考官网的文档

​ 为此这里做了一个验证,分别对不使用字典,使用本地字典的数据表进行加载和点查操作,分析其加载和查询性能。

建表语句结构如下:

1)不使用字典:

create table if not exists test.detail_benchmark1
('id', BIGINT, 'imsi' STRING,'msisdn' STRING, `imei` STRING, ...)
TBLPROPERTIES ( 'LOCAL_DICTIONARY_ENABLE'='false', 'table_blocksize'='256')
  1. 使用本地字典:
create table if not exists test.detail_loacl_dict
('id', BIGINT, 'imsi' STRING,'msisdn' STRING, `imei` STRING, ...) 
TBLPROPERTIES ( 'LOCAL_DICTIONARY_INCLUDE'='IMSI,MSISDN,IMEI','table_blocksize'='256')

使用16亿数据量样本作为表数据,分168个批次分别加载到如上表中,其加载性能如下:

数据量加载耗时 秒
test.detail_benchmark116131495481109
test.detail_loacl_dict16131495481876

下面列表记录了每一个批次的加载耗时

加载批次耗时 秒 detail_benchmark耗时 秒 detail_loacl_dict
16.58419.31
210.25521.406
35.07921.619
45.00421.538
510.87718.191
67.86817.457
75.29520.919
84.4317.048
95.37318.584
106.07417.668
115.88919.553
124.814.819
134.97212
145.14411.909
155.40318.428
165.31419.536
174.51922.373
189.32724.507
195.2777.219
2010.8826.552
214.7411.911
225.3811.908
239.0467.076
244.3996.391
255.0797.697
264.1776.114
274.4416.349
284.9517.034
295.0913.404
304.2867.455
317.82113.776
324.6237.025
339.3047.273
344.80613.23
355.4696.562
3610.0456.581
374.61911.716
385.8075.939
399.1016.531
404.9686.675
414.6546.389
424.7636.774
434.1556.203
444.6726.288
455.05212.845
464.8126.185
477.9416.653
485.4015.775
494.6845.776
504.69810.646
514.6046.359
528.8435.899
534.78511.284
544.9697.116
557.6346.291
564.4246.534
575.08611.372
584.4615.613
597.80919.169
604.8527.34
618.4867.182
624.41210.598
634.16817.974
644.2876.998
658.8676.526
664.376.457
674.2312.335
685.2813.638
697.9457.597
704.64741.695
714.42922.057
728.65911.863
7310.06217.561
745.5148.366
757.60814.011
764.5867.205
777.6926.602
785.3196.103
794.88418.043
804.136.89
818.51111.561
824.9837.434
838.4865.779
845.1586.033
854.2246.359
864.4446.736
874.5466.397
884.7996.904
893.9677.01
904.4375.612
914.54110.904
924.6496.64
934.6656.508
9462.92517.063
954.3357.104
967.3667.699
975.0286.881
985.0975.803
994.3456.36
1004.2519.569
1014.6716.121
1027.4325.491
1034.5086.412
1044.8666.104
1054.5812.243
1064.946.362
1077.8425.697
1084.33211.934
1094.64629.469
1104.53414.249
1119.20613.585
1124.80210.479
1134.1198.238
1144.1399.334
1154.5888.438
1164.5378.621
1174.6887.742
1184.0558.27
1194.7036.958
1204.5123.409
1214.1789.167
1229.226.772
1234.35912.718
1244.9267.611
1254.48714.572
1264.57113.044
1278.1088.975
1284.47218.712
1294.6645.553
1307.79112.255
1318.56913.897
1325.67112.073
1339.4321.305
1344.0156.391
1358.96147.173
1365.9559.856
1377.0956.996
1384.64111.802
1395.1566.072
1407.7576.365
1414.7110.944
1424.3467.061
1438.6636.19
1444.64113.49
1454.8646.598
1467.9327.13
1474.5611.359
1483.9416.113
1497.72910.987
1504.77.347
1518.0897.309
1524.01612.875
1534.4687.873
1549.1366.559
1555.6916.854
1564.2536.499
1578.2235.983
1584.73316.357
1594.34715.032
1604.27613.735
16117.0213.094
1624.76522.683
1635.94112.033
16466.87711.795
16515.1016.563
1668.7489.21
1675.41210.025
16811.6928.565
总计1109.7421876.579

​ 从数据中可以看出,使用本地字典在数据加载时将花费更多的时间,不使用字典时耗时明显少,从168个批次的统计数据看使用本地字典将比不使用本地字典多消耗70%的加载时长,但是在查询性能上又有提升,详细见后续介绍。

查询性能的对比

​ 这里使用了一组点查的SQL对使用本地字典、全局字典和不使用字典的数据表进行了查询统计,记录一些统计结论。

点查询SQL分别使用

select * from test.detail_benchmark where MSISDN="13900000000" limit 2000;
select * from test.detail_loacl_dict where MSISDN="13900000000" limit 2000;

模糊查询SQL分别使用

select * from test.detail_benchmark where MSISDN like "1361%" limit 2000;
select * from test.detail_loacl_dict where MSISDN like "1391%" limit 2000;

记录查询耗时情况如下:

查询用例耗时 秒 detail_benchmark耗时 秒 detail_loacl_dict
点查询五次平均值18.514.543
模糊查询五次平均值10.8324.118

​ 从结果可以看出,使用本地字典查询较不使用本地字典点查的时候性能有较大的提升。使用本地字典点查询性能提升在30%左右,模糊查询性能提升超过1倍。

​ 用户在选择字典的使用上时,可以根据自身对数据加载的要求和查询要求来选择字典,本案例由于对数据入库的时效性有较高要求,为了避免数据加载的延迟,因此在建表的时候选择无字典的方案,牺牲了一些查询性能。

建表 Create table 时SORT_COLUMNS和SORT_SCOPE的选择

​ 关于SORT_COLUMNS的配置可以参考CarbonData官网的说明, 当用户不指定SORT_COLUMNS时,默认将不建立SORT_COLUMNS,当前的SORT_COLUMNS只支持:string, date, timestamp, short, int, long, byte, boolean类型。SORT_COLUMNS 在用户指定的列上建立MKD索引,将有助于查询性能的提升,但是将稍微影响加载性能,因此只需要给需要的列上设置SORT_COLUMNS即可,无需给所有列都设置SORT_COLUMNS。

SORT_SCOPE参数用户在指定了SORT_COLUMNS后,数据加载时排序的设置,当前支持的设置有:

NO_SORT: 对加载的数据不排序

LOCAL_SORT: 加载的数据在本地排序

GLOBAL_SORT: 加载的数据全局排序,它提升了高并发下点查的效率,对加载的性能的影响较大。

在建表时,对使用SORT_COLUMNS时“NO_SORT”,“LOCAL_SORT”, “GLOBAL_SORT”的性能进行分析。

建表语句:

基准表

create table if not exists test.detail_benchmark1
('id', BIGINT, 'imsi' STRING,'msisdn' STRING, `imei` STRING, ...)
TBLPROPERTIES ( 'LOCAL_DICTIONARY_ENABLE'='false', 'table_blocksize'='256')

全局排序

create table if not exists test.detail_global_sort
('id', BIGINT, 'imsi' STRING,'msisdn' STRING, `imei` STRING, ...) 
TBLPROPERTIES ( 'LOCAL_DICTIONARY_ENABLE'='false', 'table_blocksize'='256', 'SORT_COLUMNS'='msisdn,req_time_sec,req_succed_flag', 'SORT_SCOPE'='GLOBAL_SORT')

本地排序

create table if not exists test.detail_local_sort
('id', BIGINT, 'imsi' STRING,'msisdn' STRING, `imei` STRING, ...) 
TBLPROPERTIES ( 'LOCAL_DICTIONARY_ENABLE'='false', 'table_blocksize'='256', 'SORT_COLUMNS'='msisdn,req_time_sec,req_succed_flag', 'SORT_SCOPE'='LOCAL_SORT')

不排序

create table if not exists test.detail_no_sort
('id', BIGINT, 'imsi' STRING,'msisdn' STRING, `imei` STRING, ...) 
TBLPROPERTIES ( 'LOCAL_DICTIONARY_ENABLE'='false', 'table_blocksize'='256', 'SORT_COLUMNS'='msisdn,req_time_sec,req_succed_flag', 'SORT_SCOPE'='NO_SORT')

验证其加载性能如下:

数据量加载耗时 秒
test.detail_benchmark16131495481109
test.detail_global_sort16131495486674
test.detail_local_sort16131495482473
test.detail_no_sort16131495481576

​ 通过验证的结果可以看出,在SORT_COLUMNS配置后,使用GLOBAL_SORT加载耗时最高,约为local_sort的2.7倍,约为no_sort的4.2倍;使用NO_SORT加载耗时最低,LOCAL_SORT的耗时略高于NO_SORT高约50%左右。实际中采取哪种策略还要考虑对查询的诉求。

下面给出了基于上述几种场景下的点查和模糊查询的性能对比。

查询时使用的SQL:

select * from test.detail_global_sort where MSISDN="13900000000" limit 2000;
select * from test.detail_global_sort where MSISDN like "1391%" limit 2000;

select * from test.detail_locall_sort where MSISDN="13900000000" limit 2000;
select * from test.detail_locall_sort where MSISDN like "1391%" limit 2000;

select * from test.detail_no_sort where MSISDN="13900000000" limit 2000;
select * from test.detail_no_sort where MSISDN like "1391%" limit 2000;

验证后性能结果如下:

查询五次平均值detail_benchmarkdetail_global_sortdetail_local_sortdetail_no_sort
点查询18.51.3572.4575.847
模糊查询10.8321.7851.8352.164

​ 通过查询结果可以看出,点查询和模糊查询,global_sort最快,local_sort次之,no_sort最差。点查时no_sort耗时约为global_sort的4.2倍,约为local_sort的2.3倍;global_sort比local_sort快80%左右。模糊查询时global_sort与local_sort差别不大,但是与no_sort有20%以上的提升。

​ 综合考虑global_sort虽然查询最快,但是加载耗时也最高,相比local_sort在加载耗时与no_sort差别在50%左右,点查询性能提升在2倍以上,本案例最终使用local_sort作为排序方式。

​ 在业务具体选择时需根据自身的诉求,结合加载和查询性能诉求综合选择采用哪种排序方式。

总结:

  用户在选择字典和设置SORT_COLUMNS、SORT_SCOPE的时候需要结合自身的加载和查询诉求,综合考虑使用哪种方案。

  本例中,明细数据查询场景对数据加载入库有较强的时效性要求,数据是一批次一批次的到来,一个批次一个批次进行加载,因此加载不可以延迟,否则将导致数据积压无法入库的问题。但是对查询也有一定的性能要求,希望用户的点查要尽可能的快。因此在综合考虑和分析后采用不建立字典,设置SORT_COLUMNS为最长查询的三个字段,SORT_SCOPE设置为LOCAL_SORT,这样的组合性能是能满足本例业务诉求。

create table IF NOT EXISTS example_table
(
`SID`        BIGINT,
`IMSI`       STRING,
`MSISDN`     STRING,
`IMEI`       STRING,
`MS_IP`      STRING,
`TMSI`       STRING,
`TRANS_TYPE`      INT,
`REQ_TIME_SEC`    BIGINT,
`REQ_SUCCED_FLAG` INT
.....
) STORED AS carbondata 
TBLPROPERTIES ( 'LOCAL_DICTIONARY_ENABLE'='false','SORT_COLUMNS'='msisdn,req_time_sec,req_succed_flag', 'SORT_SCOPE'='LOCAL_SORT' )

加载 Load data

加载样例:

LOAD DATA INPATH 'hdfs://hacluster/somepath'
INTO TABLE example_table 
OPTIONS('DELIMITER'='|', 'QUOTECHAR'='|', 'BAD_RECORDS_ACTION'='FORCE', 
'BAD_RECORDS_LOGGER_ENABLE'='false', 
'FILEHEADER'='SID,IMSI,MSISDN,IMEI,MS_IP,TMSI,TRANS_TYPE,
REQ_TIME_SEC,REQ_SUCCED_FLAG', 
'MULTILINE'='true', 'ESCAPECHAR'='\')

​ 数据入库的时候是采用分批入库,每一个批次对应一个文件夹,加载时没有什么特殊的,主要是一些与数据相关的参数的配置。

这里面有一些参数起使用和含义如下:

DELIMITER:加载命令中可以指定数据的分隔符,本案例中是以“|”为分割,使用中可以根据数据中的具体格式来指定。

QUOTECHAR:加载命令中可以指定数据的引用字符,引用字符内的分割符将被忽略。

BAD_RECORDS_ACTION:对于坏记录,BAD_RECORDS_ACTION 属性主要有四种操作类型:FORCE, REDIRECT, IGNORE 和 FAIL。这里使用了 FORCE选项。对于该参数的其他几种选项在中文文档中有如下说明:

​ a) FAIL 是其默认的值。如果使用 FAIL 选项,则如果发现任何坏记录,数据加载将会失败。

​ b) 如果使用了 REDIRECT 选项, CarbonData 会将所有坏记录添加到单独的 CSV 文件中。 但是,在后续的数据加载我们不能使用这个文件,因为其内容可能与原始记录不完全匹配。建议你事先对原始源数据进行清理,以进行下一步的数据处理。此选项主要用于提醒你哪些记录是坏记录.

​ c) 如果使用了 FORCE 选项,那么会在加载数据之前将坏记录存储为 NULL,然后再存储

​ d) 如果使用了 IGNORE 选项,那么坏记录不会被加载,也不会写到单独的 CSV 文件中。

​ e) 在加载的数据中,如果所有记录都是坏记录,则 BAD_RECORDS_ACTION 选项将会无效,加载操作会失败。

BAD_RECORDS_LOGGER_ENABLE: 这里配置false是为了提交加载性能,遇到坏记录的时候不记录。

FILEHEADER:是对应的文件的文件头,也就是列名,实际上在加载csv文件的时候,如果源文件不存在头信息,可以在加载命令中增加该选项,来提供文件头。当加载不带头文件的csv文件,并且文件头和表的模式一致时,可以在加载时选择 ‘HEADER’=‘false’,这样就可以不指定FILEHEADER了。

MULTILINE: 设置为true,表示CSV 引号中带有换行符。

ESCAPECHAR:如果用户希望严格验证 CSV 文件中的转义字符,可以提供转义字符。

除此以外还有一些控制参数,详细可以参考:http://carbondata.iteblog.com/data-management-on-carbondata.html 中的描述。

下面给出一些加载队列的主要配置参数以供参考,更详细的信息可以参考CarbonData的官方文档:

http://carbondata.apache.org/configuration-parameters.html

参数名设置数值参数描述
enable.unsafe.sorttrue指定在数据加载期间是否使用unsafe api,提升内存操作的性能。
carbon.use.local.dirtrue在数据加载期间,CarbonData在将文件复制到HDFS之前将文件写入本地临时目录。此配置用于指定CarbonData是否可以本地写入容器的tmp目录或YARN应用程序目录。
carbon.number.of.cores.while.loading15该值默认为2,增加加载时的核数有助于提升数据加载的效率。
carbon.detail.batch.size100存储记录的缓冲区大小,这里是从bolck扫描返回的数据量。该值的设置需要根据查询是需要返回的数据量的大小,当查询时返回数据限制是1000条,参数配置为3000条就有数据浪费了。
carbon.sort.file.buffer.size50排序期间使用的文件读取缓冲区大小(以MB为单位):MIN=1:MAX=100。
max.query.execution.time60查询执行的最大时间,该值的单位是分钟。
carbon.unsafe.working.memory.in.mb16000该数值是CarbonData支持将数据存储在堆外内存中,以便在数据加载和查询期间执行某些操作。这有助于避免Java GC,从而提高整体性能。建议的最小值为512MB。低于此值的任何值都将重置为默认值512MB。官网有公式介绍了如何计算对外内存的大小:(carbon.number.of.cores.while.Loading)(要并行加载的表数)(off heap.sort.chunk.size.in mb+carbon.blockletgroup.size.in.mb+carbon.blockletgroup.size.in.mb/3.5)
carbon.compaction.level.threshold6,0配置此参数用于将小文件合并,提升查询性能。6表示第一阶段压缩,每6个文件将触发一次level1的压缩,这里并没有使用level2压缩,为了节省入库时间。配置时由于有大表,有小表,因此对于合并是单表控制的,未设置全局开关。详见社区配置说明中Table Compaction Configuration章节。
carbon.number.of.cores.while.compacting15在压缩过程中写数据使用到的核数,这里默认数值是2。
carbon.sort.storage.inmemory.size.inmb1024CarbonData在数据加载期间将每个carbon.sort.size记录数写入中间临时文件,以确保内存占用在限制范围内。启用enable.unsafe.sort配置时,将使用内存中占用的大小(本参数)来确定何时将数据页刷新到中间临时文件,而不是使用基于行数的carbon.sort.size。此配置确定用于在内存中存储数据页的内存。注意:配置一个更高的值可以确保在内存中保留更多的数据,从而由于IO减少或没有IO而提高数据加载性能。根据集群节点中的内存可用性,相应地配置这些值。

​ 在进行数据加载时,根据数据量可以划分多个加载队列,分配不同的资源,加载时根据入库的数据量选择不同的加载队列进行加载,这样可以有效避免大数据量表的加载阻塞加载队列,导致小数据量表入库延迟,这部分数据具体的业务规划和实现,就不在这里过多介绍了。

​ 数据加载耗时情况根据选择的配置参数不同而不同,详细数据参见建表章节的性能对比。

查询 Select

查询主要的测试用例在背景处已经介绍。

一些主要的查询配置:

参数名参数数值参数描述
spark.dynamicAllocation.enabledfalse是否开启动态资源配置参数,这里选择关闭,executor选择常驻方式启动,在查询时如果开启动态分配可能消耗掉额外的时间。
spark.sql.shuffle.partitions78该数值配置为spark_executor_instances*spark_executor_cores
spark.driver.cores3在集群模式下管理资源时,用于driver程序的CPU内核数量,这里设置为3。
spark.driver.maxResultSize4G存储分区中结果集大小。
spark.locality.wait500task在执行前都会获取数据的分区信息进行分配,总是会优先将其分配到它要计算的数据所在节点,尽可能的减少网络传输,一般在进行数据本地化分配的时候会选择PROCESS_LOCAL,一旦分配超时失败将降级分配,这里将超时时间改为0.5秒。
spark.speculationtrue启动spark推测执行,这里目的是为了解决集群中拖尾任务耗时较长的场景。
spark.speculation.quantile0.75同一个stage里面所有task完成的百分比。
spark.speculation.multiplier5这个参数是一个时间比例,让task完成了spark.speculation.quantile定义的比例的时候,取完成了的task的执行时间的中位数,再乘以这个时间比例,当未完成的时间超过这个乘积时,启动推测执行。
spark.blacklist.enabledfalse关闭黑名单,避免可用executor数量不足。

查询性能统计

单并发场景:

测试用例描述数据量并发数耗时(秒)
点查非SORT_COLUMNS字段27亿12.790
点查SORT_COLUMNS字段27亿13.047
模糊查询非SORT_COLUMNS字段27亿11.517
模糊查询SORT_COLUMNS字段27亿11.673
求记录总数27亿12.017
求最大最小值27亿12.567
求sum27亿13.413

多并发场景:

测试用例描述数据量并发数耗时(秒)
点查非SORT_COLUMNS字段27亿105.886
27亿208.657
点查SORT_COLUMNS字段27亿105.737
27亿208.564
模糊查询非SORT_COLUMNS字段27亿102.906
27亿203.957
模糊查询SORT_COLUMNS字段27亿102.794
27亿203.988
求记录总数27亿1012.556
27亿2022.660
求最大最小值27亿1018.201
27亿2033.917
求sum27亿1019.811
27亿2036.595

​ 通过测试结果数据可以看出,在27亿左右数据量场景下,单并发点查、模糊查询、记录数、最大、最小值、求和的耗时在4秒以内,模糊查询耗时短于精确查询;在多并发场景下,耗时较单并发有增加,在10并发时点查和模糊查询耗时增加一倍,求记录数,最大,最小值,求和的耗时增加比较明显,达到5-8倍。在20并发情况下,点查、模糊查询、求记录数、最大、最小、求和的耗时又在10并发的基础上增加接近1倍。但是即使在20并发情况下点查和模糊查询的耗时依然在10秒以内,还可用通过增加查询资源来提高并发查询的效率。

​ 本案例给出了一个使用CarbonData进行明细数据查询的场景的建表、加载、查询的整个流程,也对建表时字典和SORT_COLUMNS的配置及SORT_SCOPE的配置方式给出了介绍和对比。通过本文可以指导用户使用CarbonData进行明细数据的查询场景的方案设置和参数选择。从最终的结果看,CarbonData在明细数据查询的场景下也具有不错的性能表现。