【spark】存储数据到hdfs,自动判断合理分块数量(repartition和coalesce)(二)

本人菜鸡一只,如果有说的不对的地方,还请批评指出!

 

该系列暂有2篇文章(本文为第2篇):

【spark】存储数据到hdfs,自动判断合理分块数量(repartition和coalesce)(一):https://blog.csdn.net/lsr40/article/details/84968923

【spark】存储数据到hdfs,自动判断合理分块数量(repartition和coalesce)(二):

https://blog.csdn.net/lsr40/article/details/85078499

 

上一篇解释了为什么要设置分块数量和怎样的分块才算合理,相信大家都有一个大概的概念,那么接下来来说说具体如何实现?

我接下来的所说的实现方法,还是要根据不同业务不同场景来选择,并不存在一种一劳永逸的最高效的方法。

实现方式:

方法一:人工判断,(适合场景:过滤日志)

有这么种情况:日志通过某些框架按照某些压缩格式(比如.gz,或者bzip2)聚合到了hdfs上成为数据块,然后我们通过spark来做ETL,做数据清洗,过滤掉不符合条件或者字段缺失的数据。这种情况下数据条数会减少,但是减少的量的百分比是我们可以根据历史任务或者测试环境推断出来的,举个例子,比如能过滤20%,剩下80%的数据量,所以可以通过hdfs的命令或者hadoop的api来查看某个文件块的大小,就可以知道输入数据量,接着把输入数据量*0.8/128M,就可以得到数据应该分为几块了。

获得文件块大小的API如下:(作者:superlxw1234)https://superlxw1234.iteye.com/blog/1514586

当然,其他场景也可以通过历史情况+人为判断得到应该存多少文件块,但是需要考虑的是:输入的文件块大小的判断方式。

例如(提供几个方案):

1、约定好每个spark任务从哪里读入数据,在代码中写好动态的路径,运行代码的时候判断该路径下文件的大小。

2、在启动spark任务的shell中,通过执行hadoop的命令,获取文件块的大小(如果多块可以相加),然后当成参数传入到main方法中

优点:不需要使用多余的资源来计算输入数据量或者输出数据量

缺点:如上提供的方案所示,需要一套合理的方式来判断输入的数据大小,甚至有时候通过某些框架在往hdfs写数据的时候,会在hdfs上有临时文件,一直到整个文件块写好,才会有.gz(或者其他压缩格式)结尾的文件,所以输入数据的时候甚至需要判断文件名。

方法二:保存数据之前判断(抽样)

spark数据全部计算完之后,在保存之前读取100条数据(当然也可以更多,但是由于这100条数据的会返回到spark的driver的,避免driver端内存溢出,所以。。。),然后计算这100条数据占用多少空间。计算整个要保存的数据的总条数。

应该分的文件块个数 =(总条数/100*100条数据的byte大小/128M)+1(请记得换算到相同单位)

代码如下(java版本):

public long saveDataTable(Dataset<Row> data, String dataBase,
String tableName, String format,String... colNames) throws UnsupportedEncodingException {
		int takeN = 100;
        //因为这部分数据要处理多次,所以先缓存数据
        data.cache();
        //获得数据的总条数
        long cn = data.count();
        if (cn == 0l ){
            data.unpersist();
        //如果数据条数为0,直接不保存数据
            return 0l;
        }
        // 取出前100条
        List<Row> firstRow = data.takeAsList(takeN);
        //创建long来存储100条数据的byte
        long byteSize = 0l;
        //遍历100条数据,计算出100条数据的byte
        for (Row row : firstRow) {
            byteSize+=row.toString().getBytes("utf-8").length;
        }
        //该数据作为字符串占用的空间大小
        long dataByte = cn/takeN*byteSize;
        //由于表可能会有压缩或者不同的格式,所以做数据量的缩放
        dataByte=tableInputFormatType(dataBase,tableName,dataByte,format);
        //计算应分区数
        int numPartition = (int)(dataByte/byteToMB/blockSize)+1;
        // 如果分区字段个数为0,那就不做partitionBy
        if (colNames.length == 0 ) {
            data.repartition(numPartition).write().mode(SaveMode.Append)
                    .format(format)
                    .saveAsTable(dataBase + "." + tableName);
        }else {
            data.repartition(numPartition).write().mode(SaveMode.Append)
                    .partitionBy(colNames)
                    .format(format)
                    .saveAsTable(dataBase + "." + tableName);
        }
        //数据保存之后,释放缓存
        data.unpersist();
        //返回该数据总条数,打印到日志
        return cn;
    }
protected long tableInputFormatType(String dataBase, String tableName,long dataByte,String format) {
        format = format.toLowerCase();
        //如果保存的时候选择orc或者parquet,证明hive表事先不存在,所以spark会自动建表
        if (format.equals("orc") || format.equals("parquet")) {
            //spark的orc默认采用snappy压缩,所以压缩比和parquet差不多
            compressionRatio = 0.4;
        } else if(format.equals("csv") || format.equals("json")){
            compressionRatio = 1.1;
        } else if(format.equals("hive")) {
            //format=hive的时候,证明表有可能事先存在,spark会自动读取hive表信息,来做文件格式的转换
            //所以先判断表stored,通过show create table的命令来判断表数据的存储格式
            //获得建表语句
            //如果表不存在,就不用获得建表语句了,直接保存数据为text格式
            if (spark.catalog().tableExists(dataBase, tableName)) {
                String createtabStmt = spark.sql("show create table " + dataBase + "." + tableName)
                        .takeAsList(1).get(0).getAs(0);
                if (createtabStmt.contains("TextInputFormat")) {
                    compressionRatio = 1;
                } else if (createtabStmt.contains("Orc")) {
                    compressionRatio = 0.2;
                    //TextInputFormat,Orc,Parquet
                } else if (createtabStmt.contains("Parquet")) {
                    compressionRatio = 0.4;
                } else {
                    //可能是json或者其他的数据类型(可能性较低)
                    compressionRatio = 1.1;
                }
            }else{
                //如果事先表不存在,直接保存数据为text格式
                compressionRatio = 1.0;
            }
        }
        dataByte*=compressionRatio;
        return dataByte;
    }

需要考虑的点是:

1、压缩:前100条数据是按照字符串格式计算大小的,但是压缩之后数据会远远小于这个值,所以代码中有两个方法,第二个方法就是用来判断压缩比的(但是我只是按照一个大概的压缩比来写百分比,可能并不会特别准确,还请大家用的时候自行测试)

2、这份数据执行了两个action,一个是count,一个是save,所以数据要先cache,避免这两个算子都从源头拉数据来计算

优点:不需要考虑数据如何输入,只要在保存数据的时候做判断,很通用,特别是对于一些聚合操作,你不知道聚合后数据条数会有怎样的骤减,这种方式很好用。

缺点:

1、压缩保存的时候,文件大小会判断不准

2、需要多消耗一份资源来count数据总条数(但是这个其实也还好,因为每天这个count可以在日志里打印出来,监控每天生成多少条数据)

方法三:写成一段专门的代码,类似小工具,来调用

这个方法跟方法二有点类似,但是其实又不太一样,听我细细说来,是这样的。

可以写一个专门的jar包,类似工具一样,用来做小文件合并数据(合并各种hdfs上的数据),传参可以是目录,文件大小,压缩格式等,可以参照网上的一些小文件合并的mr来写,也可以使用spark来写,我就不具体留代码了(因为我也没实现这个功能)

优点:完全是一个单独的工具,不用嵌套在spark代码中,对于hive生成或者其他框架生成的hdfs小文件都可以处理,可以直接用shell命令调用,不会写代码的人也可以使用

缺点:

1、要单独维护一个工具

2、如果要做的通用必须有更多格式的兼容和匹配

3、不能防范于未然,只能事后诸葛亮(小文件已有的情况下再来处理)

 

总结:以上的方法各有优缺点,还是要按照各自的需求和场景使用,所以,程序员的价值就体现在这里了。用人脑来想解决方法,选择最适合的方案,愿每一个程序员都有价值!

本人菜鸡一只,也正在学习中,如果我有什么写的不对或者不清晰的地方,希望大家指出,或者大家有什么更好的方法,欢迎评论!我会写明是你的方案,然后加入到文章中!

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值