物流数仓项目复盘:Hive结果表Sqoop同步MySQL全流程
最近在复盘一个智慧物流项目里面编号7.1.3的数据模块让我印象很深。这个模块的定位很直白把业务库里的快递单和运单数据同步到离线数仓清洗后按主题算指标最终用Sqoop把统计结果落到MySQL给BI看板和运营系统直接查询。整条链路覆盖指标统计、业务数据处理、快递单主题、运单主题以及ETL调度做完之后基本形成了一套可复用的物流数仓出口方案。如果你在做物流或者订单类业务的数据仓库或者正被“Hive结果表怎么安全同步给MySQL”这类问题卡住这篇内容可以参考一下少走点弯路。后面我会按项目拆解、建模、ETL、Sqoop导出、问题排查这几个部分展开把当时怎么定口径、怎么写SQL、怎么处理脏数据都讲清楚。1. 项目背景与需求拆解1.1 智慧物流场景里的“快递单”和“运单”到底指什么先对齐一下业务概念。物流公司每天会产生两类核心单据一类是快递单以“件”为单位记录从客户下单、揽收、派送、签收的全流程另一类是运单以“车”或“批次”为单位记录干线运输、中转、到达的运输计划。两者不是一对一一个运单可以装很多个快递单。这个关系决定了数据建模时快递单主题和运单主题要分开建但又需要在运单指标里关联快递单数量。在项目里我们同步进来的业务数据主要来自三张表快递单主表、运单主表、运单状态变更日志表。快递单主表字段相对简单单号、运单号、客户单号、创建时间、揽收时间、签收时间、状态、网点ID、金额、重量等。运单主表更偏运输侧运单号、线路ID、起始城市、目的城市、车型、额定载重、实际重量、计划发车时间、实际发车时间、到达时间、状态。第三张日志表则记录每个运单在路上的状态变化比如已建、待发、在途、到达、异常。这两类主题如果不区分清楚很容易出现指标乱串。比如“单量”到底是快递单量还是运单量必须有明确口径。我们当时的规定是凡是面向C端收派件统计的用快递单主题凡是面向运输环节统计的用运单主题。这样运营看板和运输调度看板才不会打架。1.2 指标统计的难点不在SQL在口径很多人在物流数仓里写SQL写得风生水起最后死在了口径上。比如“今日签收量”这一句话可以有好几种解释按签收时间归属到自然日还是按订单创建时间归属到那天下单的订单如果某个订单是昨天创建、今天签收两种口径就会差很多。我们最后统一采用“业务发生时间”归属也就是签收量按签收时间计入当天不按下单日期兜底。这样运营侧每天对账更直接。常见指标我们先拉了一张口径表做任何开发都按这张表来主题核心指标口径说明快递单揽收量当天产生揽收动作的快递单数快递单签收量当天产生签收动作的快递单数快递单在途量统计截止时刻仍在途未签收的快递单数快递单滞留件超过48小时未完成派送的快递单数快递单平均揽收时效从下单到揽收的平均小时数快递单平均签收时效从揽收到签收的平均小时数运单运单量统计周期内发动的运单数运单装载率实际计费重量与额定载重的比值运单平均运输时长从发车到到达的平均小时数运单平均中转次数每个运单经历中转节点的平均次数这些指标在后端离线计算MySQL里只存结果不要在MySQL里再算一遍。数据平台的意义就是把指标算好、算对业务系统只要按约定取数就行。1.3 技术选型为什么最终选择了Sqoop导出导出方案我们对比过几种。写程序直连MySQL拿查询结果对大数据量不现实而且每次指标变更都要改代码。Spark直接写MySQL又重了一点虽然性能可以但多一套Spark作业维护成本。最后用的还是集群原本就带的标准同步组件按天把Hive结果文件导出到MySQL。理由是离线数据量不算特别大每天统计结果几十万到几百万行MapReduce并发写MySQL完全能扛住而且Hive表在HDFS上的文件格式是确定的导出组件直接读文件就行不需要额外开发。这里多说一句如果你的下游是实时的在线接口那Sqoop这种离线导出显然不适合应该走Flink或者同步变更事件。我们这个项目当时的需求是BI看板和日报表小时级更新完全够用所以离线方案是最稳的。这套做法也是这类离线统计场景下的通用套路细节可以根据现场调度方式调整。2. 数据模型与表结构设计2.1 数仓分层从ODS到ADS的设计思路这个项目当时是标准四层结构。ODS层直接镜像业务MySQL按天分区DWD层做清洗和明细加工把快递单、运单的状态和关键时间字段整理好DWS层按主题做汇总快递单主题和运单主题各出各的日表ADS层就是最终导出到MySQL的那些结果表。分层的好处是业务方说某个数对不上可以直接从DWD明细开始排查不用把脏数据问题压到最后一步。ODS层的同步也不复杂因为是离线数仓不会用实时同步方案。每天凌晨通过原有同步任务把业务库前一天的新增和变化数据拉下来。这里要注意快递单是有状态的一天内可能从“已揽收”变成“已签收”所以ODS层不能只拉创建当天的新单还要把当天状态发生过变化的单子一起拉下来否则DWD层的状态永远是第一次同步的快照后续状态根本进不来。DWD层我倾向保留业务原始字段同时增加几个辅助字段比如状态编号、业务日期。这样后面算指标不用每次重新解析字符串。DWS层再按照城市、网点、线路等维度聚合成日表。ADS层的目的很纯粹就是和下游消费方对齐字段结构。2.2 快递单主题的表结构快递单DWD表我设计成了按dt分区的明细表主键是express_no同时冗余waybill_no方便和运单表关联。关键字段包括快递单号、运单号、客户订单号、创建时间、揽收时间、签收时间、取消时间、当前状态、城市ID、网点ID、支付金额、重量、是否删除标志。表结构大概是这样CREATE TABLE IF NOT EXISTS dwd.dwd_express_order_detail ( express_no string COMMENT 快递单号, waybill_no string COMMENT 运单号, customer_order_no string COMMENT 客户订单号, create_time string COMMENT 下单时间, collect_time string COMMENT 揽收时间, sign_time string COMMENT 签收时间, cancel_time string COMMENT 取消时间, status string COMMENT 当前状态: PENDING/COLLECTED/IN_TRANSIT/SIGNED/CANCELED, city_id bigint COMMENT 末端城市ID, outlet_id bigint COMMENT 网点ID, order_amount decimal(10,2) COMMENT 支付金额, item_weight decimal(10,3) COMMENT 重量kg, is_deleted int COMMENT 业务删除标志 ) COMMENT 快递单明细表 PARTITIONED BY (dt string COMMENT 业务日期分区) STORED AS ORC;之所以把时间字段都设成string而不是timestamp是为了避免时区转换和Hive timestamp格式带来的坑。反正业务侧最终展示也是字符串离线处理时用from_unixtime/unix_timestamp转来转去反而容易出问题。ORC格式压缩率高而且后面Sqoop导出时说句实话也不是直接读ORC而是从HDFS导出文件路径字段分隔符需要控制好。这一点在后面导出部分会细说。DWS层的快递单日汇总表则按dt分区加上stat_date普通字段、city_id维度、outlet_id维度以及一组指标字段。注意我这里把stat_date做成普通列而不是直接用分区字段。原因很简单导出到MySQL时Hive分区字段不会出现在数据文件里只有普通列才会写进去。如果你想用分区字段当统计日期必须在查询时把它作为一个普通列输出。2.3 运单主题和指标统计结果表运单DWD表则偏向运输侧表结构里包含运单号、线路ID、起始城市、目的城市、车型、额定载重、实际重量、体积重、计划发车时间、实际发车时间、到达时间、状态、创建时间、更新时间和分区字段。运单状态变化比较多所以我单独保留了一张状态日志表在ETL里用窗口函数取每个运单的最新状态。这个建模思路和快递单有一点区别快递单可以直接覆盖更新因为末端流程相对简单运单中间节点多状态日志表先append再在DWD层做成拉链式当前快照能避免漏掉中途状态。结果表方面MySQL里需要一张快递单日统计表和一个运单日统计表。快递单日统计表主键用stat_date dim_type dim_value组合dim_type可以是城市、网点、线路等维度类型dim_value是具体维度ID。这样一张表能承载多维度多指标业务端查询时按dim_type过滤就行。运单日统计表类似。另外如果业务系统需要查询快递单明细还需要一张明细结果表按stat_dateexpress_no唯一。不过明细表数据量大通常只保留近30天历史版本归档到独立库。3. ETL处理流程与细节3.1 调度与数据处理链路ETL链路从凌晨开始跑。当天凌晨先同步ODS然后跑DWD清洗DWS汇总最后Sqoop导出到MySQL。调度平台按任务依赖配置任何一个环节失败都会阻断下游任务。这里我特别强调依赖关系要写清楚DWS任务依赖DWD任务完成Sqoop导出依赖DWS完成。千万不能做成每天固定时间点触发否则一次大数据量延迟就会导致后续任务拿着昨天的半成品数据去跑。DWD清洗这一步重点是做三件事去重、补字段、格式化时间。快递单和运单都以主键去重保留最新一条。时间字段统一转成yyyy-MM-dd HH:mm:ss格式避免有的库存的是yyyy/MM/dd HH:mm:ss。还有一个常见坑业务库里会有测试单、删除单DWD层要把这些过滤掉避免污染指标。测试单一般用单号前缀或商户ID特征识别删除单用is_deleted字段过滤。3.2 快递单指标SQL每一天一张分区别把逻辑堆在MySQL快递单的日汇总SQL我直接写了一个比较典型的版本。假设dwd_express_order_detail表已经按dt分区存放每一天的全量快照那么计算某一天的指标时直接从这个分区取数按城市维度聚合。SQL大致如下INSERT OVERWRITE TABLE dws.dws_express_daily PARTITION (dt ${dt}) SELECT ${dt} AS stat_date, city_id, count(DISTINCT express_no) AS total_cnt, count(IF(status IN (COLLECTED, IN_TRANSIT, SIGNED), express_no, NULL)) AS collected_cnt, count(IF(status SIGNED, express_no, NULL)) AS signed_cnt, count(IF(unix_timestamp(sign_time) - unix_timestamp(collect_time) 48 * 3600, express_no, NULL)) AS delayed_cnt, round(avg(IF(collect_time IS NOT NULL, unix_timestamp(collect_time) - unix_timestamp(create_time), NULL)) / 3600, 2) AS avg_collect_hours, round(avg(IF(sign_time IS NOT NULL AND collect_time IS NOT NULL, unix_timestamp(sign_time) - unix_timestamp(collect_time), NULL)) / 3600, 2) AS avg_sign_hours FROM dwd.dwd_express_order_detail WHERE dt ${dt} AND is_deleted 0 GROUP BY city_id;这里有几个细节。count(IF(..., expr, NULL))的写法和sum(CASE WHEN ... THEN 1 ELSE 0 END)效果一样但语义上更直观尤其在指标字段比较多的时候。平均时效计算里必须先判断时间字段非空否则会把NULL值当成0参与平均导致时效被严重拉低。还有unix_timestamp函数对格式很敏感如果你在DWD层没有统一好时间格式这里可能直接返回NULL整行变成NULL。所以我习惯在DWD层先做一次时间格式清洗。这个汇总SQL的结果全量覆盖当天的分区意味着无论重跑多少次只要dt分区是同一个Hive分区内容都会被覆盖成最新结果。这就是幂等性的基础。Sqoop导出同样要遵循这个思路见后面。3.3 运单状态流转与装载率计算运单的ETL比快递单多一步状态归一。因为业务系统会把状态变更写到日志表光看运单主表的status字段最多只能知道当前状态无法判断中间是否发生过滞留或异常。我在DWD层直接用窗口函数处理状态日志表取每个运单最新一条记录生成当前运单快照。这段逻辑很经典INSERT OVERWRITE TABLE dwd.dwd_waybill_current PARTITION (dt ${dt}) SELECT t.waybill_no, t.route_id, t.status, t.update_time, t.vehicle_type, t.rated_weight, t.actual_weight, t.volumetric_weight FROM ( SELECT waybill_no, route_id, status, update_time, vehicle_type, rated_weight, actual_weight, volumetric_weight, row_number() OVER (PARTITION BY waybill_no ORDER BY update_time DESC) AS rn FROM ods.ods_waybill_status_log WHERE dt ${dt} ) t WHERE t.rn 1;注意这里WHERE dt ${dt}是为了确保把历史所有状态日志都纳入计算而不只是当天日志。如果只取当天的重跑历史分区时就会丢失之前的状态。代价是每天全量重刷会导致数据量变大但只要日志表不是无限膨胀这个代价可以接受。如果日志量太大通常会在DWD层再做一张拉链表按运单生命周期闭合状态这里就不展开了。装载率指标稍微有点讲究不是简单拿actual_weight / rated_weight就完事。快递运输里还有体积重的问题体积大但重量轻的货物如果只按实际重量统计车辆装不满但看起来装载率很高完全没有参考价值。我们当时统一按“计费重量”来算也就是实际重量和体积重取大值。体积重的折算标准是长宽高除以6000单位是厘米和千克。这样算出来的装载率才对运输调度有指导意义。SELECT round(avg(IF(actual_weight volumetric_weight, actual_weight, volumetric_weight) / rated_weight * 100), 2) AS load_rate FROM dwd.dwd_waybill_current WHERE dt ${dt} AND status CANCELED;这种口径一定要先在文档里写清楚否则运输侧会质疑你为什么装载率经常超过100%就是因为有的车型额定载重设计偏保守实际能装更多。我没法告诉你标准值只能提醒你这个指标的口径比SQL本身更重要。3.4 ETL里的幂等和重试要点ETL重跑是数仓日常工作里最频繁的事数据晚到、源表字段变更、Hive任务失败都会导致要重跑。所以我要求每个ETL节点必须支持同一分区重复执行且结果一致。具体做法有三点一是所有INSERT都写成INSERT OVERWRITE不要用INSERT INTO。这样每次重跑前会把旧分区自动覆盖。二是先创建临时表或者先在Hive里跑一个小查询确认源数据没有明显异常再启动正式任务。三是重跑时不能把下游任务也一起重跑造成重复导出调度平台要能指定从某个节点开始补偿。还有一个我踩过的坑是Hive动态分区要小心。最开始图方便用动态分区按城市写入结果当天小城市太多生成了几千个分区整个任务跑了两个小时。后来改成按天静态分区再把城市作为普通维度字段聚合速度一下子降下来了。数据40分钟跑完。这个时间我印象很深因为那时候才知道分区设计不是越细越好要和查询和导出方式结合着看。4. Sqoop导出到MySQL实操4.1 目标库建表与连接串配置Sqoop导出的前置条件是MySQL目标表要先建好。统计结果表我一般建议用组合业务主键而不是自增ID因为Hive端每天导出的数据天然按stat_date分组用自增ID反而容易因为重复导出产生垃圾行。快递单日统计表的MySQL建表语句大概长这样CREATE TABLE ads_express_daily_stats ( stat_date varchar(10) NOT NULL COMMENT 业务日期, dim_type varchar(20) NOT NULL COMMENT 维度类型, dim_value varchar(50) NOT NULL COMMENT 维度值, total_cnt bigint DEFAULT 0 COMMENT 快递单量, collected_cnt bigint DEFAULT 0 COMMENT 揽收量, signed_cnt bigint DEFAULT 0 COMMENT 签收量, delayed_cnt bigint DEFAULT 0 COMMENT 滞留件, avg_collect_hours decimal(10,2) DEFAULT NULL COMMENT 平均揽收时效, avg_sign_hours decimal(10,2) DEFAULT NULL COMMENT 平均签收时效, update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (stat_date, dim_type, dim_value) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT快递单主题日统计结果表;这里把update_time设置成自动更新时间每次Sqoop导入数据时会自动更新业务端可以用来判断数据新鲜度。MySQL连接串也很关键我当时经常碰到编码和时间问题后来固定成下面这种写法--connect jdbc:mysql://10.1.2.3:3306/ads_warehouse?characterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai注意连接串里写的是characterEncodingutf8MySQL驱动里不认utf8mb4这种写法表结构用utf8mb4连接字符串用utf8就够了。serverTimezone一定要指定否则老版本驱动默认时区和服务器时区不一致导出datetime字段会差8小时。useSSLfalse是为了避免连接认证开销内网环境不需要加密。4.2 每日统计表的全量导出命令每日统计表的数据量一般不大但要求结果可靠我习惯先清空目标表再全量导出。清空操作可以用一个独立脚本比如mysql -h 10.1.2.3 -u user -pxxx ads_warehouse -e TRUNCATE TABLE ads_express_daily_stats;然后再执行Sqoop导出。导出命令里要明确指定--columns不要依赖文件里的字段顺序因为Hive查询SQL改过之后文件里的列顺序可能和MySQL期望的不一致。命令模板sqoop export \ -D mapreduce.job.queuenamedefault \ --connect jdbc:mysql://10.1.2.3:3306/ads_warehouse?characterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai \ --username data_export \ --password-file /user/sqoop/password.file \ --table ads_express_daily_stats \ --columns stat_date,dim_type,dim_value,total_cnt,collected_cnt,signed_cnt,delayed_cnt,avg_collect_hours,avg_sign_hours,update_time \ --export-dir /user/hive/warehouse/dws.db/dws_express_daily/dt${dt} \ --input-fields-terminated-by \001 \ --input-lines-terminated-by \n \ --input-null-string \\N \ --input-null-non-string \\N \ --num-mappers 4 \ --batch--input-fields-terminated-by \001是Hive默认的字段分隔符如果不写Sqoop默认按逗号分隔读出来全乱。--input-null-string和--input-null-non-string两个参数必须要否则Hive里的NULL会变成字符串“NULL”写进MySQL这个坑后面专门说。--batch参数会把多条INSERT批量提交减少MySQL交互次数能明显提升导入速度。我和同事实测过几十万行数据不加--batch时导入要几分钟加了之后基本一分钟以内。--num-mappers也不是越大越好。我一般按数据量估算每天统计结果100万行以内开4个Mapper数据量更大每增加一个Mapper都会额外占用MySQL连接和索引更新开销。如果目标表有频繁查询建议并发不要超过8不然会把MySQL写挂。配合--batch4个Mapper在绝大多数场景下已经够用。4.3 增量明细表的Sqoop导出命令如果是快递单明细结果表每天新增的数据都要追加到MySQL同时业务端还要支持按单号查询。这种场景不能简单truncate全表因为历史数据要保留。可以使用增量导出模式。Sqoop的增量导出有两种模式--update-mode updateonly只在主键冲突时更新--update-mode allowinsert是主键冲突时更新不存在时插入。对于明细表我一般用allowinsertsqoop export \ --connect jdbc:mysql://10.1.2.3:3306/ads_warehouse?... \ --username data_export \ --password-file /user/sqoop/password.file \ --table ads_express_order_detail \ --columns express_no,waybill_no,status,create_time,collect_time,sign_time,stat_date,... \ --export-dir /user/hive/warehouse/ads.db/ads_express_order_detail/dt${dt} \ --input-fields-terminated-by \001 \ --input-null-string \\N \ --input-null-non-string \\N \ --update-key express_no \ --update-mode allowinsert \ --num-mappers 4这里有一个限制--update-mode allowinsert在部分版本里不能和--batch同时使用因为批处理的INSERT在冲突处理上走了不同的JDBC路径。我碰到过明明添加了allowinsert结果日志里还是报主键冲突最后排查发现就是同时加了--batch。去掉批量参数就好了。如果业务端对更新一致性要求不高也可以先按stat_date删除当天数据再普通增量导入这样脚本更简单也不依赖Sqoop的update机制。4.4 导出一致性校验与性能调优Sqoop导出成功并不代表数据一定对。我每次导出完成后都会做一次行数和关键指标校验。最简单的办法是在Hive端和MySQL端各查一次汇总值做对比hive -S -e SELECT count(*), sum(total_cnt), sum(signed_cnt) FROM dws.dws_express_daily WHERE dt${dt} /tmp/hive_check.txt mysql -h 10.1.2.3 -u user -pxxx ads_warehouse -e SELECT count(*), sum(total_cnt), sum(signed_cnt) FROM ads_express_daily_stats WHERE stat_date${dt} /tmp/mysql_check.txt diff /tmp/hive_check.txt /tmp/mysql_check.txt echo check ok注意Hive端查的是dws表MySQL端查的是结果表两边的sum(total_cnt)如果都是空或者NULL也会造成diff异常所以脚本里最好加个非空判断。这个校验脚本后来被集成到调度平台里一旦diff异常会直接挂掉任务避免脏数据给业务侧造成影响。性能调优方面除了前面说的--batch和--num-mappers还有一个容易被忽略的点MySQL目标表如果索引太多Sqoop导入时每插一行要维护索引速度会指数级下降。我当时做过一次测试统计表只有一个主键索引导入500万行大约10分钟如果额外加了三四个查询索引时间翻倍都不止。所以我会在导入前把非业务核心索引先删掉导入完成后再重建。这个操作对每天全量统计表非常有效。5. 常见问题与排查记录5.1 Hive里的NULL被写成字符串NULL这是Sqoop导出里最常见也最容易被忽视的问题。Hive表在HDFS上存储时NULL值默认会用\N两个字符表示。如果Sqoop导出时不加对应的参数它会把\N当成普通字符串写进MySQL结果MySQL里出现一堆文本“NULL”业务端在用Java查询时根本判断不了空值。我们当时就因为这个问题BI显示某天签收率变成了“NULL”排查了半天。解决办法就是导出命令里加上两个参数--input-null-string \\N和--input-null-non-string \\N。第一个参数针对字符串字段第二个针对数值字段。注意转义命令行里写成\\N最终传给MapReduce的是\N。如果用的是shell脚本还要小心引号被吃掉。这个方法对Hive默认的TextFile格式和大部分ORC转出的文本文件都有效。5.2 导出后MySQL时间对不上差了8小时有一次运营反馈前一天签收量的结果比业务系统统计少了将近半天但快递单量是正常的。后来发现不是SQL口径问题而是Sqoop导出时Hive里存的时间字段是字符串MySQL目标表也定义成了datetimeJDBC驱动在写入时把字符串解析成了时间并且做了时区转换结果全都变成了UTC时间比北京时间早8小时。我们的解决办法是统一口径Hive里所有时间字段都保持字符串MySQL目标表对应字段也直接用varchar或datetime如果你非要用datetime就在JDBC连接串里强制指定serverTimezoneAsia/Shanghai并且保证MySQL服务器本身的时区是08:00。但从那次之后我所有统计表里的时间都改成了varchar因为统计表的时间字段根本不需要参与SQL运算只是给业务看。字符串不会有任何时区问题也省掉后续一堆莫名其妙的转换。5.3 主键冲突和重跑导致的重复数据统计任务重跑在数仓里太正常了。问题是如果Sqoop导出脚本不是幂等的重跑一次就可能把同一批数据再插一遍。我们在快递单明细表上遇到过主键冲突原因就是任务失败后重跑第一次插入的数据还留在MySQL里第二次跑到同一批快递单号就撞了主键。排查思路很直接先看Sqoop日志里报的是Duplicate entry还是直接把错误吞掉了。如果是允许insert的更新模式要看Sqoop版本和配置文件是否真的开启了allowinsert。更保险的做法是在调度脚本里每次导出前先执行一次删除操作DELETE FROM ads_express_order_detail WHERE stat_date ${dt};然后再执行导出。这样不管任务重跑多少次最终表里只有一份当天数据。虽然多了一步删除但比追着主键冲突排查省心得多。千万别图省事不删这个坑我踩过两次。5.4 导出缓慢与MySQL负载居高不下Sqoop导出慢多数不是因为数据量太大而是因为目标表索引维护和并发控制没做对。一次全量导出几十万行结果MySQL的CPU被打满普通查询全部超时。我当时第一个怀疑的就是--num-mappers开太多结果从8降到4之后CPU掉下来一些但导入时间反而还快了一点。另外还要排查是不是--batch没加。如果没有批量提交每行单独走一次JDBC execute网络和SQL解析开销都很高MySQL端会显示大量小写入。加上--batch之后会明显缓解。还有一种情况是MySQL表上有大量二级索引导入过程中MySQL要为每一行更新索引CPU消耗自然上去了。这种场景可以在导入前先禁用或者删除非关键索引导完后重建。对每日全量结果表非常有用。5.5 字符集、截断和字段顺序问题字符集问题通常表现为中文乱码或者MySQL报Incorrect string value。解决办法是Hive落HDFS的文件用UTF-8MySQL表用utf8mb4JDBC连接串用characterEncodingutf8三层对齐。需要注意MySQL的utf8mb4在JDBC驱动端不支持直接写characterEncodingutf8mb4这是驱动参数和历史兼容性决定的你只要按utf8来写实际上也是按UTF-8处理。字段截断问题大多数是Hive侧字段长度比MySQL目标varchar长度长。比如业务单号用了奇怪的前缀长度超过32位MySQL写入时报Data truncation。我会在Sqoop导出之前在HiveSQL里统一做一次截断和清洗不要指望Sqoop帮你处理。字段顺序问题在前面已经强调过Sqoop导出时--columns必须和Hive查询输出列顺序完全一致否则数据会错位。这个错位很隐蔽行数是对的校验总和大概率也对不齐。最后再说一句个人习惯。这一套快递单和运单主题的离线链路做完给我最大的经验就是每次Sqoop导出任务跑完不要只看日志里的“SUCCESS”我习惯用业务系统里最常用的那几条SQL去查一遍导出的表看看到底能不能查出他们要的数。因为有时候行数和总和都对但某个维度的口径已经被中间层悄悄改了。另外如果哪天你们也遇到“源表加了字段但Sqoop导出报错”的情况先别急着改SQL回到--columns这个参数上找原因多半是字段列表没跟上表结构变化。希望这些经验能帮各位少折腾几个晚上。