前言
HIVE 作为大数据生态的数仓解决方案,因为历史的原因在很多行业很多公司都有着广泛的应用。对于比较复杂的业务逻辑,HIVE SQL 往往比较难以表达,此时大家在开发中往往会辅以 HIVE UDF。所以充分理解和掌握 HIVE UDF正确的表写和使用方式,是大数据从业人员必不可少的一项技能。
关于HIVE UDF 的使用,明哥在前段时间发过两篇博文,分别是 “如何在 hive udf 中访问配置数据-方案汇总与对比” 和 “浅析 hive udaf 的正确编写方式- 论姿势的重要性" ,两篇博文描述的都是 HIVE UDF 在编写使用过程中容易犯的错误。
但 UDF 编写使用过程中遇到的问题往往很多远远不止以上两个,所以明哥决定编写一个系列 - “浅析 hive udf 的正确编写和使用方式 - 论姿势的重要性“,以上两篇博文可以算做这个系列中的系列一和系列二,本文是该系列的系列三。以下是正文。
时间紧张,急于知道结论的小伙伴,可以直接看最后一部分,问题总结。
问题现象与初步分析
产品部人员反馈,某 HIVE UDF 通过 hive 的旧客户端即 hive service --cli方式可以正常使用,但使用新客户端 beeline 时却会报错,客户端的报错信息没有啥明确的有意义的信息,如下图图一和图二所示:
beeline
hive
咨询产品部开发人员,该 UDF 的功能是返回给定业务日期的下一个业务日期,在背后会读取 HDFS上的一个日期类配置文件;经查看该 UDF 源码,发现该日期类配置文件的路径,使用的是相对路径,如下图所示:
code-relative-path
熟悉 HDFS 的小伙伴都知道 HDFS 有相对路径的概念,即代码中用相对路径方式指定的文件, 在不同用户执行作业时会被解析为不同用户的根目录下的文件,比如相对路径 dir1/fileA, 使用hive 用户执行时作业时会被解析为 /user/hive/dir1/fileA, 使用xyz用户执行作业时会被解析为 /user/xyz/dir1/fileA, 很多 hdfs 上的文件找不到的问题都是因为该原因。因为有的环境有的用户能执行成功而另外的环境另外的用户执行却会失败,我们往往戏谑是人品问题,哈哈。
所以顺藤摸瓜,看到这里有使用相对路径指定文件,我们一个自然的思路是查看日志验证问题。需要注意,这里要查看的是服务端的日志,即 beeline 连接的 hiveserver2 实例的日志,在 cdh 中一般是 /var/log/hive下。果不其然,看到了熟悉的报错信息:
hiveserver2-log
进一步咨询产品部人员,他们把该配置文件上传到了 /user/root/ 目录下,没有上传过其它目录。这也解释了,为什么 他们 hive service --cli 方式能够成功,而 beeline方式如 beeline -u jdbc:hive2://xxx:10000/default -n userA -p passwd 方式却会失败:因为他们使用前者时是固定在 root 登录用户的身份下提交的作业(这其实不太合规范,一般不建议用root身份运行应用程序),而使用后者时实际生效的用户是 -n 参数指定的用户而不是当前登录用户!(没有启用用户身份认证或启用ldap认证时,都是通过 -n 参数指定用户身份;启用kerberos认证时,通过kerberos的 principal指定用户身份)。这里还有个小细节,如果没有启用身份认证且 beeline后没有使用 -n参数指定用户,真正生效的用户时anonyous匿名用户,对应的 home directory 是 /user/anonymous.
分析到这里,我们觉得只需要上传该配置文件到对应用户的 home directory下,即可解决该问题了。但实际验证发现,啪啪打脸,同样的问题仍然存在!
问题进一步分析与解决
再次仔细查看相关代码,发现了问题所在:该 UDF 读取配置文件的内容,使用的是类的静态代码块(不是静态方法)。为什么使用静态代码块会引起问题?要回答这个问题,需要比较扎实的 JAVA 功底, 和对 UDF 执行机制的深刻理解。小伙伴们可以下想下。
code-static
不卖关子,直接说原因:我们知道 SQL 和 UDF 的解析编译优化和生成 mr/tez/spark 任务是在 hiveserver2 中进行的,但并不是所有的 sql 和 udf 都会生成 mr/tez/spark 任务,比如这里该 UDF就不会生成 mr/tez/spark 任务,也不需要向 yarn 申请资源获得 container 容器,而是直接在 hiveserver2 中执行的。所以前几次 UDF的失败调用时,该 hiveserver2 这个jvm 已经加载了对应的类,此次再次调用该 UDF 时不需要重新加载该类,自然也不会重新执行类的静态代码块,所以没有重新读取配置文件的内容,所以没有更新对应的配置变量,执行也就失败了。
重新启动该 hiveserver2 实例后,再次提交该 udf,会重新加载对应的类,并执行其中的静态代码块读取配置文件的内容,读取成功后会更新对应的配置变量,最后作业执行成功。
问题总结 HDFS 有相对路径的概念,即代码中用相对路径方式指定的文件, 在不同用户执行作业时会被解析为不同用户的根目录下的文件,比如相对路径 dir1/fileA, 使用hive 用户执行时作业时会被解析为 /user/hive/dir1/fileA, 使用xyz用户执行作业时会被解析为 /user/xyz/dir1/fileA, 很多 hdfs 上的文件找不到的问题都是因为该原因; Hive SQL 和 UDF 的解析编译和优化是在 hiveserver2 中进行的,解析编译和优化的结果一般是生成 mr/tez/spark 任务,这些 mr/tez/spark 任务是在向 yarn 申请获得的 container 容器对应的 jvm 中执行的;但并不是所有的 sql 和 udf 都会生成 mr/tez/spark 任务,此时其真正的执行就是直接在 hiveserver2 这个已经存在的 jvm 中执行的,该 hiveserver2 这个 jvm 的生命周期跟 udf 的执行无关,如果涉及到配置环境变量,系统参数,或加载类及执行静态代码块,要尤其小心,必要时需要重启 hiveserver2;(udf 中需要谨慎只用静态代码块,因为静态代码块只有在初次加载类的时候才会执行) udf中读取配置文件,有多种方式,常见的有:
配置文件使用绝对路径指定,且在代码中写死绝对路径;
配置文件使用绝对路径,但在代码中通过读取本地配置文件获取hdfs上配置文件的最终绝对路径;
配置文件使用相对路径,在客户现场部署时需要确定执行作业的真正用户身份,并上传该配置文件到对应用户的跟目录下的特定路径;