ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Hive LOAD DATA 日批加载实战:分区表、幂等重跑与优化

Hive LOAD DATA 日批加载实战:分区表、幂等重跑与优化 1. 临时加载日批文件为什么LOAD DATA是最稳的入口日批数据落地到Hive分区表最常见的做法就是LOAD DATA。很多人一上来就想着写Spark任务或者Flink Sink觉得那样更工程化但真到了每天凌晨跑批、文件已经躺在HDFS某个目录里的场景LOAD DATA反而是最直接、最少依赖、最容易排查问题的方案。它不启动任何计算引擎不走MapReduce本质上就是一次HDFS层面的文件移动或引用操作秒级完成失败了日志也干净。先把场景说清楚上游系统每天凌晨把前一天的业务数据以文本文件CSV、TSV或者自定义分隔符推到HDFS的某个落地目录比如/data/landing/order/2024-06-01/下面可能有一个或多个文件。你需要把这些文件挂到Hive的分区表ods.order_di的对应分区dt2024-06-01下。这个动作每天重复要求稳定、可重跑、可回滚。LOAD DATA有两种语法形态这个必须分清楚因为选错了后面全是坑-- 语法一加载本地文件系统客户端所在机器的数据 LOAD DATA LOCAL INPATH /home/etl/order_20240601.csv INTO TABLE ods.order_di PARTITION (dt2024-06-01); -- 语法二加载HDFS上的数据 LOAD DATA INPATH /data/landing/order/2024-06-01/ INTO TABLE ods.order_di PARTITION (dt2024-06-01);带LOCAL的会把客户端本地文件先上传到Hive表所在HDFS目录不带LOCAL的则是把HDFS上的文件移动到表目录。注意这个移动是关键字——源文件加载后就不在原地了。这一点后面讲重跑的时候会重点说因为它直接决定了你的重跑策略。那为什么不用INSERT INTO ... SELECT因为那是走计算引擎的要起任务、要占资源、要等调度对于文件已经在HDFS上、格式和表结构对齐这种纯搬运场景属于杀鸡用牛刀。LOAD DATA的定位就是数据搬运工不负责转换不负责清洗快进快出。提示LOAD DATA不做任何数据校验文件里的字段顺序、分隔符、类型它一概不管加载进去是什么就是什么。所以格式对齐的活儿必须在加载前自己确认好。这里有个容易被忽略的细节LOAD DATA加载时Hive不会去读文件内容它只是把文件挪到分区目录下。这意味着即使你的文件是坏的、编码是乱的、字段数不对加载这一步照样成功。真正的报错会推迟到你第一次SELECT这张表的时候。所以千万别以为LOAD DATA返回成功就万事大吉后面必须跟一次验证查询。2. 分区表的目录结构决定了你的加载路径要玩转LOAD DATA得先理解Hive分区表在HDFS上到底长什么样。这不是理论是直接影响你操作的东西。一张分区表ods.order_di分区字段是dt那么它在HDFS上的目录结构是这样的/user/hive/warehouse/ods.db/order_di/ dt2024-06-01/ part-00000 part-00001 dt2024-06-02/ part-00000分区字段dt在目录里以dt值的形式体现这叫分区目录命名规范。LOAD DATA ... PARTITION (dt2024-06-01)做的事情就是把源文件挪进dt2024-06-01/这个目录。理解了这个结构很多问题就迎刃而解了。比如你想确认某个分区到底有没有数据不用查表直接hdfs dfs -ls那个分区目录就行hdfs dfs -ls /user/hive/warehouse/ods.db/order_di/dt2024-06-01/如果目录不存在或者为空那分区就是空的。这比SELECT count(*)快得多尤其在数据量大的时候。2.1 内部表和外部表加载行为完全不同这是个大坑。LOAD DATA对内部表和外部表的行为有本质区别表类型LOAD DATA 行为删除表时内部表MANAGED文件移动到表目录源文件消失表目录连同数据一起删除外部表EXTERNAL文件移动到表目录源文件消失只删元数据数据保留注意即使是外部表LOAD DATA也是移动文件不是复制。很多人误以为外部表加载会保留源文件这是错的。外部表的外部指的是删表时数据不受影响不代表加载时不移动文件。如果你确实需要保留源文件比如源目录还要给别的系统用有两个办法加载前先hdfs dfs -cp复制一份到临时目录再LOAD DATA那个临时目录或者干脆不用LOAD DATA用ALTER TABLE ... ADD PARTITION直接指向源目录这个后面单独讲。2.2 分区不存在时会发生什么LOAD DATA ... PARTITION (dt2024-06-01)如果这个分区在元数据里还不存在Hive会自动创建它。这是LOAD DATA的一个便利之处省去了手动ALTER TABLE ADD PARTITION的步骤。但这里有个隐患如果分区字段类型是int或者date而你写的是字符串2024-06-01Hive会尝试转换。转换失败的话分区值可能变成NULL或者报错。所以分区字段类型和加载时写的值类型要对齐。实践中我建议分区字段统一用string类型省去一堆类型转换的麻烦反正分区值就是个目录名。注意自动创建分区这个行为在开了hive.exec.dynamic.partition.modestrict的时候对动态分区加载会有额外限制静态分区加载不受影响。3. 从落地目录到分区目录一次完整的加载实操光讲原理没意思直接走一遍完整流程。假设上游把文件推到了/data/landing/order/2024-06-01/目录下有三个文件order_1.csv、order_2.csv、order_3.csv。3.1 加载前的三项检查别急着敲LOAD DATA先做三件事能省掉后面80%的麻烦。第一确认源目录和文件存在且非空。hdfs dfs -ls /data/landing/order/2024-06-01/ hdfs dfs -du -s -h /data/landing/order/2024-06-01/-du看总大小如果显示0字节说明上游推了个空目录这时候加载进去就是个空分区得先找上游确认。第二确认目标分区当前状态。hdfs dfs -ls /user/hive/warehouse/ods.db/order_di/dt2024-06-01/ 2/dev/null如果这个目录已经存在且有数据说明今天的分区可能已经加载过了。这时候直接LOAD DATA会导致数据重复——因为LOAD DATA是追加不是覆盖。这是重跑场景下最常踩的坑后面第5节专门讲。第三确认文件格式和表定义对齐。hdfs dfs -cat /data/landing/order/2024-06-01/order_1.csv | head -3看一眼分隔符是不是和建表时的ROW FORMAT DELIMITED FIELDS TERMINATED BY一致字段个数对不对。这一步花十秒钟能避免加载后查出一堆NULL。3.2 执行加载检查通过执行加载LOAD DATA INPATH /data/landing/order/2024-06-01/ INTO TABLE ods.order_di PARTITION (dt2024-06-01);注意路径写的是目录不是单个文件。LOAD DATA支持加载整个目录目录下所有文件都会被挪进分区目录。这比一个个文件加载方便得多。执行完源目录/data/landing/order/2024-06-01/会变空文件被移走了分区目录下出现三个文件。3.3 加载后的验证加载返回OK不代表数据可用必须验证。三步走-- 第一步确认分区有数据且行数合理 SELECT count(*) FROM ods.order_di WHERE dt2024-06-01; -- 第二步抽样看字段是否错位 SELECT * FROM ods.order_di WHERE dt2024-06-01 LIMIT 10; -- 第三步确认没有全NULL的字段格式错位的典型症状 SELECT count(*) AS total, count(order_id) AS order_id_cnt, count(user_id) AS user_id_cnt FROM ods.order_di WHERE dt2024-06-01;如果total和order_id_cnt差距很大说明order_id这一列大量为NULL八成是分隔符或者字段顺序对不上。这时候别犹豫把分区删了重新加载别在错误数据上做后续处理。-- 删分区重来 ALTER TABLE ods.order_di DROP PARTITION (dt2024-06-01);提示DROP PARTITION对内部表会连数据一起删对外部表只删元数据。重跑前想清楚你的表类型。4. 动态分区加载一次搞定多天数据的诱惑与代价静态分区一次只能加载一个分区如果你要补一周的数据就得写七条LOAD DATA。这时候有人会想到动态分区——用INSERT ... SELECT配合动态分区一次写入多个分区。但这里要泼盆冷水LOAD DATA本身不支持动态分区。动态分区是INSERT语句的能力不是LOAD DATA的。LOAD DATA的PARTITION子句里必须写死分区值。所以如果你的需求是把落地目录里按天分好的多个子目录一次性加载到对应分区LOAD DATA做不到得用别的方式-- 动态分区方式走计算引擎不是LOAD DATA SET hive.exec.dynamic.partitiontrue; SET hive.exec.dynamic.partition.modenonstrict; INSERT OVERWRITE TABLE ods.order_di PARTITION (dt) SELECT col1, col2, ..., dt FROM ods.order_di_staging WHERE dt BETWEEN 2024-06-01 AND 2024-06-07;这种方式的好处是能一次写多个分区坏处是要起计算任务慢而且INSERT OVERWRITE会覆盖目标分区用不好会误删数据。那有没有既快又能批量加载的办法有用ALTER TABLE ADD PARTITION直接挂载ALTER TABLE ods.order_di ADD IF NOT EXISTS PARTITION (dt2024-06-01) LOCATION /data/landing/order/2024-06-01/ PARTITION (dt2024-06-02) LOCATION /data/landing/order/2024-06-02/;这个操作不移动文件只是把分区元数据指向源目录。速度快源文件原地不动。但代价是数据目录和表目录分离管理起来容易乱而且如果源目录被清理表数据就没了。适合临时查询场景不适合长期稳定的数仓分层。4.1 三种加载方式怎么选把选择逻辑理清楚别凭感觉场景推荐方式理由单天数据文件已在HDFSLOAD DATA最快无计算开销多天数据目录已按天分好ALTER TABLE ADD PARTITION批量挂载不移动文件需要转换/清洗后再入表INSERT SELECT唯一能加工的方式源文件要保留给其他系统ADD PARTITION 或先cp再LOADLOAD会移走源文件我个人的习惯是日常日批用LOAD DATA补历史数据用ADD PARTITION需要加工的场景才上INSERT SELECT。三种方式各司其职别混用。5. 重跑与幂等LOAD DATA最大的坑在这里日批任务最怕的不是失败是重跑导致数据重复。LOAD DATA是追加语义同一个分区加载两次数据就翻倍。而日批任务因为上游延迟、网络抖动、调度重试重跑是家常便饭。所以幂等设计是必须的。5.1 重跑前先清分区最朴素也最可靠的做法重跑前先删分区再加载。ALTER TABLE ods.order_di DROP PARTITION (dt2024-06-01); LOAD DATA INPATH /data/landing/order/2024-06-01/ INTO TABLE ods.order_di PARTITION (dt2024-06-01);但这里有个致命问题LOAD DATA已经把源文件移走了重跑时源目录是空的所以这个方案只在源文件还在的情况下有效。5.2 源文件被移走后的补救如果第一次加载已经把文件移进了分区目录重跑时源目录空了怎么办两个思路思路一从分区目录把文件挪回去再重跑。# 把文件从分区目录挪回落地目录 hdfs dfs -mv /user/hive/warehouse/ods.db/order_di/dt2024-06-01/* \ /data/landing/order/2024-06-01/ # 然后删分区 hive -e ALTER TABLE ods.order_di DROP PARTITION (dt2024-06-01) # 再重新加载 hive -e LOAD DATA INPATH /data/landing/order/2024-06-01/ INTO TABLE ods.order_di PARTITION (dt2024-06-01)思路二改用cp而不是mv的加载策略。加载前先把源文件复制一份到临时目录LOAD DATA临时目录源目录保持不动。这样重跑时源文件还在直接删分区重加载即可。hdfs dfs -cp /data/landing/order/2024-06-01/ /tmp/load_staging/order/2024-06-01/ALTER TABLE ods.order_di DROP PARTITION (dt2024-06-01); LOAD DATA INPATH /tmp/load_staging/order/2024-06-01/ INTO TABLE ods.order_di PARTITION (dt2024-06-01);思路二多了一次cp的开销但换来了幂等性我认为这笔买卖划算。日批任务里可重跑比省那点IO重要得多。5.3 用脚本封装幂等逻辑手工敲命令容易漏步骤把幂等逻辑封装成脚本#!/bin/bash DT$1 SRC_DIR/data/landing/order/${DT} STAGING_DIR/tmp/load_staging/order/${DT} TABLEods.order_di # 1. 复制源文件到临时目录保留源文件 hdfs dfs -rm -r -f ${STAGING_DIR} hdfs dfs -cp ${SRC_DIR} ${STAGING_DIR} # 2. 删除目标分区幂等关键 hive -e ALTER TABLE ${TABLE} DROP PARTITION (dt${DT}) # 3. 加载 hive -e LOAD DATA INPATH ${STAGING_DIR} INTO TABLE ${TABLE} PARTITION (dt${DT}) # 4. 验证行数 CNT$(hive -e SELECT count(*) FROM ${TABLE} WHERE dt${DT} | tail -1) echo 分区 ${DT} 加载完成行数${CNT} # 5. 清理临时目录 hdfs dfs -rm -r -f ${STAGING_DIR}这个脚本跑多少遍结果都一样这就是幂等。注意第2步的DROP PARTITION如果分区不存在会报错可以加IF EXISTS但Hive的DROP PARTITION语法对IF EXISTS支持因版本而异稳妥起见用hive -e包一层错误忽略或者先判断分区是否存在。注意DROP PARTITION之后分区目录会被删除如果表是外部表数据目录也会被删因为分区目录在表目录下。所以外部表场景下重跑策略要更谨慎最好用ADD PARTITION指向独立目录的方式。6. 小文件、编码、权限那些加载后才暴露的问题LOAD DATA本身很少报错但加载后查询时问题就来了。这一节讲几个高频的。6.1 小文件问题如果上游每天推几百个小文件LOAD DATA会把它们原样挪进分区目录。Hive查询时每个小文件对应一个map任务几百个小文件就是几百个map调度开销远超实际计算。这就是热词里hive优化小文件的由来。解决思路有两个方向方向一加载前合并。在落地目录先做一次合并把多个小文件合成大文件再加载。# 用HDFS的getmerge合并后重新上传 hdfs dfs -getmerge /data/landing/order/2024-06-01/ /tmp/merged.csv hdfs dfs -put /tmp/merged.csv /data/landing/order/2024-06-01_merged/方向二加载后合并。用INSERT OVERWRITE重写分区触发合并。SET hive.merge.mapfilestrue; SET hive.merge.size.per.task256000000; INSERT OVERWRITE TABLE ods.order_di PARTITION (dt2024-06-01) SELECT * FROM ods.order_di WHERE dt2024-06-01;方向二会走计算引擎慢但能顺便做格式规整。方向一快但只是物理合并不解决格式问题。日常我倾向方向一简单直接。6.2 中文乱码热词里删除hive乱码分区和tecplot load data 错误 no mapping for unicode都指向编码问题。LOAD DATA不检查编码文件是GBK还是UTF-8它照单全收。查询时如果表定义是UTF-8而文件是GBK中文就乱码。排查方法直接看文件编码。hdfs dfs -cat /data/landing/order/2024-06-01/order_1.csv | file -如果显示ISO-8859或者Non-ISO extended-ASCII基本就是GBK。解决办法是在加载前转码iconv -f GBK -t UTF-8 order_1.csv order_1_utf8.csv或者在建表时指定SERDEPROPERTIES里的编码但Hive对编码的支持有限最稳的还是加载前转好。6.3 权限问题LOAD DATA执行时Hive会用当前用户的身份去操作HDFS。如果目标表目录的属主是hive而你用etl用户执行可能报权限拒绝。这时候要么用hive用户执行要么让管理员给表目录加写权限。hdfs dfs -chmod -R 775 /user/hive/warehouse/ods.db/order_di/但改权限要慎重别把敏感目录开放了。生产环境更规范的做法是通过Hive的代理用户机制或者Ranger/Sentry做细粒度授权而不是简单粗暴地chmod 777。7. 加载失败后的排查链路LOAD DATA失败的情况不多但一旦失败排查要讲方法。按这个顺序走第一步看报错信息。Hive的报错通常很直白比如Path does not exist就是源路径写错了Permission denied就是权限问题Partition not found可能是分区字段名写错了。第二步确认源路径。手动hdfs dfs -ls一下确认路径存在、文件非空、当前用户有读权限。第三步确认目标表。DESCRIBE FORMATTED ods.order_di看表的分区字段名、类型、存储位置。分区字段名写错是高频错误比如表里是dt你写成day。第四步确认分区值格式。分区字段是string值写2024-06-01没问题如果是int值写20240601写成2024-06-01就可能出问题。第五步看HiveServer2日志。前面几步都没问题就去/var/log/hive/hiveserver2.log翻日志里面会有更详细的堆栈。我遇到过一次很隐蔽的失败源目录里有个隐藏文件.SUCCESSLOAD DATA把整个目录加载时这个隐藏文件也被当成数据文件挪进去了查询时报格式错误。后来加载前加了一步过滤hdfs dfs -rm /data/landing/order/2024-06-01/.SUCCESS或者加载时只指定具体的数据文件不指定整个目录。这种坑文档里不会写只有踩过才知道。8. 和Flink Sink、Doris这些方案的边界热词里出现了flink sink hive表 数据不入表和hive与doris说明很多人在纠结实时链路和离线链路怎么配合。这里说清楚LOAD DATA的边界。LOAD DATA是离线批处理的工具适合T1的日批场景。它的优势是简单、快、无依赖。但如果你要的是分钟级延迟LOAD DATA就不合适了那是Flink Sink Hive或者直接写Doris的场景。Flink Sink Hive数据不入表常见原因是分区提交策略没配对或者Hive表的分区格式和Flink写入的格式不一致。这跟LOAD DATA是两套体系别混在一起排查。至于Hive和Doris的配合常见架构是Hive做ODS层存储Doris做ADS层查询加速中间用数据同步工具把Hive分区数据导入Doris。这时候LOAD DATA负责的是Hive内部的日批加载Doris那边是另一套导入流程各管各的。我的建议是别用实时链路的思路去改造离线加载。日批数据就用LOAD DATA老老实实加载简单可靠。实时需求单独建实时链路两套体系并行互不干扰。硬要把实时方案套到日批场景只会把简单问题复杂化。最后分享一个我用了很久的习惯每次LOAD DATA之后不管成功失败都往一张审计表里插一条记录记下分区、行数、加载时间、执行人。时间长了这张表就是最好的排查依据——哪天数据对不上一查审计表就知道是加载环节的问题还是下游处理的问题。这个习惯成本很低但省下的排查时间难以估量。
返回列表