SparkSQL的UDF大数据量执行结果和HiveSQL的UDF不一致

摘要: SparkSQL的UDF大数据量执行结果和HiveSQL的UDF不一致
关键词: 大数据 SparkSQL UDF 线程安全

一、软件版本:

1.Hive版本

Hive 1.2.1000.2.6.5.0-292

2.Spark版本

Spark version 2.3.0.2.6.5.0-292

二、项目场景:

交付项目上基本所有的脚本任务,都是使用Hive脚本的方式生成数据,但是dolphinscheduler的数据质量sql,是基于SparkSQL构建的。
当我们有一些复杂逻辑的时候,我们会编写UDF,我们在Hive里注册UDF,然后在Spark里使用

三、问题描述:

1.UDF代码

package com.bigdata.udf.data_quality;


import org.apache.hadoop.hive.ql.exec.Description;
import org.apache.hadoop.hive.ql.exec.UDF;
import org.apache.hadoop.io.Text;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
 * 日期格式检查
 * create function UDFDateCheck as 'com.iflytek.hive.DateCheckUDF' using jar 'hdfs:///user/hive/udf/check_udf-1.0-SNAPSHOT.jar';
 * */
@Description(
        name = "dateCheck",
        value = "_FUNC_(date) - check date or datetime format",
        extended = "select dateCheck('2012-05-20') from  dual; \n"+
                "result is '1' \n" +
                "select dateCheck('2020-05-20 12:20:56') from dual; \n"+
                "result is '1' \n"+
                "select dateCheck('abcd-ef-gh') from dual; \n"+
                "result is '0'"
)
public class DateCheckUDF extends UDF {

    public static SimpleDateFormat DATE_FORMAT_NUMS = new SimpleDateFormat("yyyyMMdd");
    public static SimpleDateFormat DATETIME_FORMAT_NUMS = new SimpleDateFormat("yyyyMMddHHmmss");

    //当前只判断这三种格式
    public static SimpleDateFormat DATE_FORMAT_SPILT_2Y = new SimpleDateFormat("yy-MM-dd");
    public static SimpleDateFormat DATE_FORMAT_SPILT_4Y = new SimpleDateFormat("yyyy-MM-dd");
    public static SimpleDateFormat DATETIME_FORMAT_SPILT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    /**
     * YYYYMMDD日期 考虑平年闰年,考虑大小月
     */
    public static final Pattern pYearMonthDay =  Pattern.compile("((\\d{3}[1-9]|\\d{2}[1-9]\\d|\\d[1-9]\\d{2}|[1-9]\\d{3})(((0[13578]|1[02])(0[1-9]|[12]\\d|3[01]))|((0[469]|11)(0[1-9]|[12]\\d|30))|(02(0[1-9]|[1]\\d|2[0-8]))))|(((\\d{2})(0[48]|[2468][048]|[13579][26])|((0[48]|[2468][048]|[3579][26])00))0229)");

    /**
     * HHmmss时间
     */
    public static final Pattern pHourMinuteSecond =  Pattern.compile("^(20|21|22|23|[0-1]\\d)[0-5]\\d[0-5]\\d$");

    private static final String result_0 = "0";
    private static final String result_1 = "1";
    /**
     * 返回值 0 校验不通过
     * */
    private static final Text RESULT_0  = new Text("0");

    public Text evaluate(Text date){
        return dateTimeCheck(date);
    }




    //日期检查
    public static boolean dateCheck(String dateStr){
        Matcher matcher = pYearMonthDay.matcher(dateStr);
        return matcher.matches();
    }

    //时间检查HHmmss (HH:mm:ss去掉了冒号)
    public static boolean timeCheck(String timeStr){
        Matcher matcher = pHourMinuteSecond.matcher(timeStr);
        return matcher.matches();
    }

    public static Text dateTimeCheck(Text date){
        if(date == null ){
            return RESULT_0;
        }

        String input = date.toString();
        String result = "";

        switch (input.length()) {
            case 8:
            case 10:
            case 14:
            case 19:{
                try {
                    if(input.length() == 8){
                        if(input.contains("-")) {
                            Date formatDate = DATE_FORMAT_SPILT_2Y.parse(input);
                            String tmp = input.replaceAll("-", "");
                            String numsDate = DATE_FORMAT_NUMS.format(formatDate);
                            if (!numsDate.contains(tmp)) {
                                result = result_0;
                                break;
                            }
                            if (dateCheck(numsDate)) {
                                result = result_1;
                            }
                        }else {
                            Date numsDate = DATE_FORMAT_NUMS.parse(input);
                            String numsDateStr = DATE_FORMAT_NUMS.format(numsDate);

                            if(dateCheck(numsDateStr)){
                                result =  result_1;
                            }else {
                                result =  result_0;
                            }
                        }
                    }


                    if(input.length() == 10){
                        Date formatDate = DATE_FORMAT_SPILT_4Y.parse(input);
                        String tmp = input.replaceAll("-","");
                        String numsDate = DATE_FORMAT_NUMS.format(formatDate);
                        if(!numsDate.equals(tmp)){
                            result = result_0;
                            break;
                        }
                        if(dateCheck(tmp)){
                            result =  result_1;
                        }else {
                            result =  result_0;
                        }
                    }

                    if(input.length() == 14){
                        Date numsDate = DATETIME_FORMAT_NUMS.parse(input);
                        String numsDateStr = DATETIME_FORMAT_NUMS.format(numsDate);

                        String dateStr = numsDateStr.substring(0,8);
                        String timeStr = numsDateStr.substring(8);
                        if(dateCheck(dateStr) && timeCheck(timeStr)){
                            result = result_1;
                        }else {
                            result =  result_0;
                        }
                    }

                    if(input.length() == 19){
                        Date formatDate = DATETIME_FORMAT_SPILT.parse(input);
                        String tmp = input.replaceAll("-","")
                                .replaceAll(" ","")
                                .replaceAll(":","");
                        String numsDate = DATETIME_FORMAT_NUMS.format(formatDate);
                        if(!numsDate.equals(tmp)){
                            result = result_0 ;
                            break;
                        }
                        String dateStr = tmp.substring(0,8);
                        String timeStr = tmp.substring(8);
                        if(dateCheck(dateStr) && timeCheck(timeStr)){
                            result = result_1;
                        }
                    }
                    break;
                }catch (Exception e){
                    result = result_0;
                    break;
                }
            }
            default:{
                result = result_0;
            }
        }
        return new Text(result);
    }

}

2.注册UDF

代码很长,主要是判断各种日期格式,然后在Hive的default库里注册UDF为UDFDateCheck函数,然后因为Spark可以直接读取Hive的Metastore,所以也可以在Spark的default库使用UDFDateCheck函数

3.执行SQL

select UDFDateCheck(update_time),update_time 
  from u_ods.ods_u_st_qjysjzx_vh_check
WHERE 1 = 1
  and update_time is not null
  and update_time<> ''
  and UDFDateCheck(update_time) = 0
limit 10;

1).HiveSQL里执行

返回结果为空

2).在SparkSQL里执行

返回结果

1	20230630083553
0	20231228103659
0	20231226104024
0	20231227161317
1	20231119003340
1	20230105230002
1	20231119003149
1	20231220154554
1	20230321140113
1	20231226132732

且多次执行,第一列返回不同,UDFDateCheck(update_time)有时为1有时为0,而我的筛选条件是UDFDateCheck(update_time) = 0,这里已经异常了

四、原因分析:

1.分析原因可能是Spark内存不够

提升Spark内存之后,仍然没有效果

2.分析Spark和Hive特点

1).Spark UDF特点

当在Spark中使用UDF时,同一个UDF实例可能会被不同的线程调用,特别是在进行数据分区处理时。
这意味着如果UDF中存在共享状态或全局变量,并且没有正确地管理这些资源的话,就可能导致线程安全问题。
如果UDF中包含非线程安全的操作,例如修改静态变量或实例变量,那么在高并发的情况下(如处理大数据量时),就可能会遇到数据竞争条件(race conditions),从而导致错误的结果或异常

数据竞争条件解释

数据竞争条件指的是,在并发环境中,多个线程试图访问并修改同一份数据,但由于缺乏适当的同步控制,导致数据处于不一致的状态。这种情况通常发生在以下几个场景中:

共享静态变量:如果UDF修改了一个静态变量,并且这个变量没有被适当地同步,那么多个线程同时访问和修改这个变量时,可能会导致不可预测的行为。

实例变量:如果UDF是一个类的方法,并且该方法修改了类的实例变量,而这些实例变量又是在多个线程间共享的,那么同样可能会引发数据竞争。

外部资源:如果UDF需要访问一些外部资源(如数据库连接、文件系统等),并且这些资源的访问没有被正确地同步,也可能会导致数据竞争条件。

2).Hive UDF特点

Hive是一个基于磁盘的数据仓库技术,它的查询执行引擎更倾向于批处理模型。在 Hive中,每个UDF通常在一个单独的JVM进程中运行,这意味着即使UDF不是线程安全的,也不会像在Spark中那样容易出现问题,因为每个查询都会创建一个新的UDF实例

3.分析结果总结

因为Spark是线程不安全的,所以如果UDF使用了非线程安全的操作,那么就会导致不可预测行为,Hive是每个UDF在单独的JVM里执行,就会好很多。
从我们代码中可以看出我们使用了线程不安全的操作

public static SimpleDateFormat DATE_FORMAT_NUMS = new SimpleDateFormat("yyyyMMdd");

SimpleDateFormat类在Java中是非线程安全的。这是因为SimpleDateFormat的状态可能会被多个线程同时访问和修改,特别是在使用format和parse方法时,如果多个线程同时调用同一个SimpleDateFormat实例,就有可能导致数据错误或异常行为

五、解决方案:

1.解决方案列举

1).避免共享状态:

尽量使UDF成为纯函数,即UDF的输出只依赖于输入参数,而不依赖于任何外部状态。

2).使用线程安全的数据结构:

如果必须使用共享资源,确保使用线程安全的数据结构或实现同步机制(如 ThreadLocal等)来保护共享资源。

3).利用Spark的累加器:

如果需要收集中间结果,可以使用Spark的累加器,这是一种只读于任务但写入于驱动程序的特殊变量,它们可以安全地在多个任务之间共享。

4).利用广播变量:

对于只读的共享变量,可以使用 Spark 的广播变量来减少数据的复制次数,提高性能,但需注意广播变量也是线程安全的。

2.解决方案选择

方案1需要修改太多,方案3和方案4需要去写Spark代码
所以最终选择方案2
使用ThreadLocal是最高效且线程安全的方法

private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT_NUMS = ThreadLocal.withInitial(() -> {
        return new SimpleDateFormat("yyyyMMdd");
    });

这里只举例修改,把代码中涉及SimpleDateFormat的全部使用了ThreadLocal
修改完成之后在Spark里执行UDF也能得到和Hive一样的结果了

六、总结:

编写UDF的时候,要注意线程安全的问题,尤其是在SparkSQL会使用这些UDF的时候

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

pengpenhhh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值