尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Hive与HBase整合实战:外部表映射、读优化与HFile批量写入

Hive与HBase整合实战:外部表映射、读优化与HFile批量写入 上周有个做风控的朋友在群里问我他们线上把用户行为明细全量写在 HBase 里现在业务方要一张按天、按渠道的漏斗报表是不是只能先把数据导出到 HDFS再灌进 Hive 跑离线任务我的答复是不用绕这一圈Hive 可以直接把 HBase 的表当成一张外部表来查这就是 Hive 和 HBase 的整合。说白了它让在线写、离线读这件事不用再做一次数据搬运HBase 负责扛住端上的高频随机读写Hive 负责用 SQL 做批量聚合两边共用同一份底层数据。这篇文章我打算把这条链路从头到尾捋一遍环境依赖怎么配、外部表的列映射怎么写、读的时候为什么慢、写的时候为什么更慢、批量装载怎么绕开逐行 Put 的开销以及我在真实集群里踩过的那几个一眼看不出原因的坑。不管你是刚接触 Hive 数仓的新人还是已经在维护 HBase 集群的老手都能从里面找到可以直接抄走的东西。1. 先搞清楚这两个系统的分工边界再谈整合1.1 一个是列式扫描的分析引擎一个是行键点查的 KV 库很多人一上手就想把 Hive 和 HBase 整合但没想明白整合的价值到底在哪。Hive 的底层数据躺在 HDFS 上表是目录、列是文件适合一次扫描几千万行做聚合代价是启动慢、延迟高单次查询动辄几十秒到几分钟HBase 的数据按 rowkey 排序存在 Region 里适合按主键做毫秒级的单行读写代价是它几乎没有按任意列做聚合的能力你要统计就只能全表 Scan。这两个特性几乎是互补的。整合的实质是让 Hive 拿到一份能按 SQL 语义读写的 HBase 视图从而把两件事串起来一是 HBase 里的明细数据不落地直接做离线统计二是 Hive 里算好的宽表结果直接写回 HBase 供在线接口查询。前者省掉了每天一趟的全量导出后者省掉了一套结果同步服务。1.2 哪些场景值得整合哪些场景纯属自找麻烦我的判断标准很简单看数据的访问形态是不是两边都要。值线上服务按用户 ID 高频读写明细同时又要按天做全量统计口径的口径核对这种一份数据两种用法的场景整合收益最大。值Hive 侧算出来的宽表比如用户标签、风控评分需要按主体 ID 做毫秒级查询用 HBase 承接结果表比落 MySQL 再分库分表省事得多。不值数据本身只在离线用从来没人在线点查过。这种直接留在 Hive 里就行多一层 HBase 只是多一层运维成本。不值数据只在线上用离线只是偶尔导一次做报表。那用 HBase 自带的导出能力定期 dump 到 HDFS 更简单没必要让两个引擎长期耦合。一句话整合是为了消除同一份数据在两个系统之间来回搬的成本如果本来就没有来回搬的需求整合只会给你增加一个故障点。1.3 三条常见的技术路线以及我为什么默认选第一条路线实现方式适合的读写方向我的评价HBaseStorageHandler 外部表Hive 建 EXTERNAL TABLESTORED BY HBaseStorageHandler读为主小批量写首选纯 SQL 就能跑通快照导出后建 Hive 外部表HBase snapshot → ExportSnapshot 到 HDFS → Hive 读 HFile只读、超大批量不打扰线上适合T1大报表Phoenix 作为桥接层Phoenix 建视图Hive 走 JDBC 或 PhoenixStorageHandler读为主依赖组件多链路长第一条路线的核心优势是零额外组件。Hive 自带 HBaseStorageHandler只要把 HBase 的几个 client jar 塞进 Hive 的类路径一条CREATE EXTERNAL TABLE就能读到 HBase 的数据不需要起任何中间服务。第二条路线我在数据量特别大、又不想让离线任务影响线上 RegionServer 的时候会用代价是要额外维护快照的生成和清理。第三条路线除非团队本来就在用 Phoenix 做二级索引否则我不建议引入多一层就多一个版本兼容问题。2. 环境准备jar 包、版本矩阵和那些绕不开的类加载报错2.1 版本匹配没对齐后面全是玄学Hive 和 HBase 的整合是典型的跨组件依赖地狱。HBaseStorageHandler 这个类编译的时候是跟着某个 HBase 主版本编的运行的时候如果 classpath 里出现另一个主版本的 HBase jar表现就是各种莫名其妙的 NoClassDefFoundError 和 NoSuchMethodError。我的经验是遵循下面这个对应关系能省掉一大半排查时间Hive 版本建议搭配的 HBase 版本说明Hive 1.xHBase 1.x老集群jar 依赖相对少Hive 2.xHBase 1.x / 2.x2.x 需要对一下 handler 编译版本Hive 3.xHBase 2.x官方主线组合推荐注意判断能不能跑通的唯一标准不是版本号表面上是否匹配而是运行时 classpath 里加载的 hbase-client、hbase-common、hbase-server、hbase-protocol、hbase-mapreduce 这几组 jar 是不是同一个版本。版本号混装是九成类加载报错的根因。2.2 jar 到底该放哪三种方式的取舍这个问题几乎每个新手都会问。Hive 加载 HBase 依赖一般有三种做法我用得最多的是第一种放进 Hive 的 auxlib 目录。把 hbase 相关 jar 拷到$HIVE_HOME/auxlib/下Hive 启动时会把该目录下所有 jar 加入 classpath。好处是一次配置全局生效连 HiveServer2 的会话也能用。用会话级ADD JAR。适合临时验证缺点是每个会话都要加一遍而且只在当前会话有效。配hive.aux.jars.path。指向一个自定义目录灵活但要注意这个目录必须对 HiveServer2 进程可读。用第一种方式时我会先确认目标 jar 清单ls $HBASE_HOME/lib/ | grep -E ^hbase-(client|common|server|protocol|mapreduce|http|shaded)拷过去之后用一个最简单的动作验证是否生效hive -e ADD JAR /opt/hive/auxlib/hive-hbase-handler.jar; CREATE EXTERNAL TABLE probe_tbl(k string) STORED BY org.apache.hadoop.hive.hbase.HBaseStorageHandler WITH SERDEPROPERTIES(hbase.columns.mapping:key) TBLPROPERTIES(hbase.table.nameprobe_tbl);如果这条 DDL 能跑完没报类找不到说明依赖基本齐了。跑不通的话别急着怀疑 HBase 服务先在客户端侧找原因。2.3 连通性自检把端口清单贴在工位上类加载过了下一关是网络。离线集群和在线集群经常不在同一个网段防火墙策略是最容易被忽略的一环。HBase 常见端口我整理成了下表做联调的时候逐个telnet一遍比什么都快。端口用途2181ZooKeeper 客户端接入16000HMaster RPC16010HMaster Web UI16020RegionServer RPC16030RegionServer Web UI16060RegionServer 相关辅助服务8080 / 9090REST / Thrift 服务默认端口自检顺序我一般是这样走的先确认 ZooKeeper 能连让 HBase client 能读到hbase:meta的位置再确认 Hive 所在节点能通到所有 RegionServer 的 16020最后才是跑 SQL。这三步里任何一步卡住后面都会表现为任务卡在 map 阶段不动但实际上根因在网络上。2.4 NoClassDefFoundError 这类报错的完整排查链路java.lang.NoClassDefFoundError是这条链路上最高频的报错包括但不限于org/apache/hadoop/crypto这类看着跟 HBase 毫无关系的类。我的排查步骤固定是四步基本能定位到九成问题。第一步看报错的完整栈。这句话很关键——NoClassDefFoundError 打印的往往是找不到的那个类但真正的问题可能在它上面一层某个类的静态初始化块崩了导致后续引用它的时候报 Not Found。所以一定要往上翻几行找第一个Caused by。第二步确认 jar 是否真的在 classpath 里。不是看你有没有拷文件而是在 Hive 会话里执行list jars或者在提交任务时打印mapreduce.job.classpath。我遇到过好几次是文件拷进去了、权限不对Hive 进程读不到。第三步确认同一个类有没有被两个版本同时提供。用jar tf挨个查看 hbase-common 和 hbase-server 是不是一个版本。混装版本时先加载到谁就用谁行为完全不可预测。第四步检查是否被 Hadoop 自带的 jar 抢占。Hive 依赖的 hadoop-common 里可能带了旧版本的 protobuf 或 guava跟 HBase 需要的版本冲突这种冲突不报类找不到报的是 NoSuchMethodError。看到 NoSuchMethodError 就往这个方向想。2.5 换成 Tez 引擎之后事情会变得更微妙很多人验证 Hive-HBase 整合是在 MapReduce 引擎下跑的跑通了之后顺手把hive.execution.engine改成 tez结果任务直接起不来。原因在于 Tez 的容器是一个独立的 JVM它不会自动继承 Hive 客户端的 auxlib 类路径需要额外把 HBase 的 jar 分发到 Tez 的本地化资源里。我的做法是两件事一起做一是把 HBase jar 加入 Tez 的资源清单二是把hive.aux.jars.path指向的目录同步给 Tez 使用。改完之后别只验证 SELECT一定要跑一次 INSERT 写入因为读和写的代码路径不同读通了不代表写也通。set hive.execution.enginetez; set tez.task.resource.memory.mb2048; set hive.tez.container.size2048;Tez 带来的好处是显而易见的Hive on HBase 的查询大多是小规模的多次扫描加上聚合Tez 的 DAG 执行能省掉中间落盘我在同一份数据上实测过多阶段聚合的查询从两分半降到四十秒左右收益非常明显。3. 建表与列映射HBaseStorageHandler 的实战细节3.1 HBase 侧的 rowkey 设计决定了后面所有查询的生死整合的第一刀应该切在 HBase 侧而不是 Hive 侧。因为 Hive 读 HBase 时的并行度和过滤能力完全取决于你的 rowkey 长什么样。rowkey 的设计我只坚持三条一是长度尽量短HBase 的行键是逐字节比较的键越长Region 索引和 Bloom Filter 的开销越大二是把最常用的查询维度放在最左边比如做用户维度分析就user_id 时间戳做时间维度分析就时间戳 user_id顺序反了就没有前缀匹配能力三是必须做预分区不要用默认的自动切分。预分区的方式有很多我常用的是给一个连续的十六进制区间create user_behavior, {NAME info, VERSIONS 1, BLOOMFILTER ROW}, {SPLITS [10,20,30,40,50,60,70,80,90]}VERSIONS 1是我强烈建议的改动。HBase 默认保留三个版本对在线服务来说多版本意义不大但对离线全表扫描来说多版本意味着读放大。把版本数压到 1配合BLOOMFILTER ROW在读路径上的收益非常直接。3.2 Hive 外部表的 DDL逐项拆开讲有了 HBase 表Hive 侧就可以建映射了。一份典型的 DDL 长这样CREATE EXTERNAL TABLE ods_user_behavior ( row_key string, user_id string, item_id string, action_type string, event_ts bigint, ext_info string ) STORED BY org.apache.hadoop.hive.hbase.HBaseStorageHandler WITH SERDEPROPERTIES ( hbase.columns.mapping :key,info:user_id,info:item_id,info:action_type,info:event_ts,info:ext_info ) TBLPROPERTIES ( hbase.table.name user_behavior, hbase.table.default.storage.type string );这段 DDL 里有几个点必须解释清楚。第一EXTERNAL不是可选项而是必选项。用普通表的话你执行DROP TABLE的时候有可能把 HBase 侧的表一起干掉这个风险太大了。用 EXTERNAL 之后删 Hive 表只是删元数据HBase 数据不动。第二hbase.columns.mapping的数量和顺序必须跟 Hive 列严格一一对应多一个少一个都会直接报错。这个字段是我见过最容易写错的地方尤其是列多了以后靠肉眼对齐非常容易错位。我的习惯是先数 Hive 列的数量再数映射串里逗号分隔的段数两个数对上再往下走。第三hbase.table.default.storage.type建议显式写成 string。不写的话走默认值不同版本默认值不一样一旦和 Java 侧写入的方式不一致读出来就是乱码。提示列映射是写在表属性里的改映射等于重建表。但因为是 EXTERNAL你可以直接 DROP 再 CREATEHBase 数据完全不受影响。做映射调整的时候用这种方式迭代比我见过的任何在线改映射的偏方都安全。3.3:key、#b、#s、:timestamp这几个特殊字段的用法映射串里除了普通的cf:col还有几个以冒号开头的特殊写法理解它们能避免不少误用。:key代表 HBase 的 rowkey 本身它必须以第一个位置出现。如果你希望 Hive 里看到的是完整行键就给它一个 string 类型的列。复合行键我一般不用多个列去拼而是在写入侧就拼好一个字符串列读取侧需要拆分的时候再用substr或split处理这样做的好处是行为完全可预期。:timestamp映射的是单元格的时间戳类型只能是 bigint。它的典型用途是做按写入时间覆盖的幂等写入——写入时显式指定时间戳重复任务跑两次不会产生多版本数据。#b和#s是后缀修饰符加在列名后面表示二进制或字符串序列化。比如info:event_ts#b表示这个列按二进制读。这两个后缀在跨系统对接时特别重要因为它们直接决定了字节到类型怎么转换。写法含义我的使用建议:key行键只能放第一位:timestamp单元格时间戳做幂等覆盖时使用cf:col普通列最常用按默认类型解析cf:col#b二进制列Java 侧用 Bytes.toBytes 写入时用cf:col#s字符串列跨语言对接时最稳3.4 最阴的一类问题Java 侧写入和 Hive 侧读出来对不上这个坑我踩过不止一次值得单独说。HBase 里存的永远是字节数组没有类型概念。Java 开发用Bytes.toBytes(10086)写入一个 intHive 侧如果用 string 映射去读你会看到一串乱码反过来Hive 侧用一个 int 列写进去Java 侧用Bytes.toString去读同样拿不到你期望的数字。我的统一约定是所有跟外部系统交互的列一律按字符串存。Java 侧用Bytes.toBytes(String.valueOf(value))写Hive 侧映射成 string两边读出来的值完全一致。数字类型的转换留给计算层去做反正 SQL 里cast一下的成本很低而一旦出现类型对不上排查成本高得离谱。还有一个更隐蔽的版本问题如果同一个列的历史数据里混着两种写入方式早期 Java 裸写、后期走 Hive 写入那你的数据就是两种编码混存怎么读都有对不上的行。发现这种情况先按写入时间拉一份抽样做比对确认问题范围再决定是清洗重写还是新建列族。3.5 改表名、加列、换映射这些动作的安全边界日常运维里免不了要动表结构。我的原则是分清元数据动作和数据动作。改 Hive 侧表名用ALTER TABLE old_name RENAME TO new_name这个动作只改元数据不会碰 HBase 的表名映射除非你在 TBLPROPERTIES 里同步改了hbase.table.name。改完记得验证一次查询确认映射没被带偏。加列的话HBase 侧不需要做任何 DDL——HBase 是 schema-less 的新列族才需要显式创建新列不需要。所以加列只是在 Hive 侧重建外部表把新的cf:col加到映射串末尾。注意要加在末尾中间插入会导致所有后续列错位。至于换映射类型我建议一律走DROP CREATE的路线并且提前记录好旧的映射串方便回滚。在 EXTERNAL 表的场景下这个操作对底层数据零影响是所有方案里最干净的。4. 读路径优化为什么你的 Hive on HBase 查询慢得离谱4.1 Split 从 Region 来数据倾斜的根也在那里先讲一个很多人不知道的事实Hive 读 HBase 的时候输入切分不是像读 HDFS 文件那样按块大小切而是直接按 HBase 的 Region 来切一个 Region 对应一个 map 任务。这就意味着 map 任务的数量天然等于 Region 数量而 Region 的负载是否均衡取决于你当初预分区做得好不好。后果就是典型的倾斜表现大部分 map 任务几秒钟跑完个别任务跑了十几分钟整个 Stage 被这两个慢任务拖死。你在 Hive 侧怎么调hive.exec.reducers.bytes.per.reducer都没用因为瓶颈在 split 层面。解决方式只有两个方向一是在 HBase 侧做 Region 的均衡和合并让每个 Region 的数据量大致相当二是给 rowkey 加盐把热点打散。我一般倾向于第一种因为改 rowkey 设计对在线业务影响太大而 Region 层面的运维动作可以选在低峰期做。# 查看 Region 分布确认是否均衡 hbase hbck -details user_behavior如果发现某些 Region 明显偏大用split命令手工切一刀或者用merge_region合并小 Region。做完之后再跑一次 Hive 查询你会看到 map 阶段的完成时间变得整齐很多。4.2 下推的边界哪些条件下得去哪些必须在 Hive 侧兜着谓词下推是读路径优化的核心话题。HBase 能高效处理的只有 rowkey 上的范围条件因为它是按键有序存储的列上的过滤条件HBase 也有 Filter 机制但效果远不如 rowkey。我的实操判断是这样当 SQL 里的过滤条件作用在:key对应的那个列上并且是等值或者范围、、between时比较大的概率能被下推成 Scan 的起止行触发的是按区间扫描而不是全表扫描性能差距可能是几十倍。当条件作用在其他列上即使部分被下推成 Filter也不减少扫描的 Region 数量本质还是全扫。验证方式不要靠猜用EXPLAIN看执行计划EXPLAIN SELECT action_type, count(1) FROM ods_user_behavior WHERE row_key 20240101000000 AND row_key 20240102000000 GROUP BY action_type;计划里如果出现了类似HBaseScanRange或者带起止行的描述说明下推生效了。如果看到的是全表扫描加filterExpr那就说明这条路走不了下推得换个思路——要么改用 rowkey 条件要么干脆走快照导出。我还想提醒一点不要指望 Hive 帮你自动把substr(row_key,1,8)20240101这种函数形式的条件下推下去。函数包住 rowkey 之后优化器识别不出这是范围条件结果就是老老实实全表扫。4.3 并行度、内存与 Tez 参数调什么最有效读路径的参数调整我的优先级排序是这样的先把 split 层面的倾斜解决掉这一步的收益最大。再调容器内存。全表扫描时每个 map 任务需要缓存扫描结果内存不够会导致频繁 spill我的经验值是一个 map 至少给 2GB。最后才调并行度和聚合相关参数。set hive.execution.enginetez; set hive.tez.container.size3072; set hive.tez.java.opts-Xmx2458m; set tez.grouping.max-size268435456; set hive.auto.convert.jointrue;hive.auto.convert.join这个开关在 HBase 场景里特别值得打开。因为 HBase 侧的表往往是明细大表跟维度表关联时如果走 Map Join可以避免一次昂贵的 Shuffle我在实际任务里打开之后端到端时间普遍有百分之二三十的下降。4.4 一个实测对比让你感受下差距有多大下面这组数据来自我自己维护的一个集群表规模约 4 亿行、11 个 Region目标是统计某一天的 action 分布执行方式map 数量耗时备注全表 Scan 列过滤116分12秒Region 大小不均最慢的 map 占了一半时间rowkey 范围条件1148秒实际只扫了 2 个 Region 的数据rowkey 条件 均衡后的 Region1621秒预分区重做后效果最好快照导出后读 HFile1615秒完全不占用 RegionServer 资源可以看到从六分钟到二十秒中间没有任何黑科技靠的全是把 rowkey 用好、把 Region 切匀。这也解释了为什么我前面花了那么大篇幅讲 rowkey 设计——整合的功夫八成在 HBase 侧两成在 Hive 侧。顺带说一个宽表处理的小技巧。HBase 侧经常是一行多列的结构映射到 Hive 之后如果列数很多分析起来会很别扭。这时候会用到列转行的思路用explode把多列拆成多行或者反过来用collect_list加concat_ws把多行拼回一行。这两种转换在报表场景里几乎天天要写值得练熟。-- 多列拆成多行方便做统一口径的统计 SELECT user_id, kv.key, kv.value FROM ( SELECT user_id, map(click, click_cnt, view, view_cnt) AS kv FROM tmp_user_stat ) t LATERAL VIEW explode(kv) tmp AS key, value;5. 写路径从 Hive 回写 HBase 的两种姿势5.1 直接 INSERT 的代价藏在每一次 Put 和 WAL 里Hive 往 HBase 外部表写数据走的是一条很直接的路径每个 map 任务拿到一批行逐行构造成 HBase 的 Put 对象通过 HBase 客户端发到对应的 RegionServerRegionServer 先写 WAL 再写 MemStore。这条路径的问题是它把 Hive 的批量吞吐硬生生压成了逐个 Put的形态。数据量小的时候感觉不出来一旦上到千万行级别问题就会集中爆发RegionServer 的 WAL 写入量飙升Compaction 频繁触发线上查询延迟跟着抖。我见过很典型的一次一个 3000 万行的回写任务把在线集群的 P99 延迟拉高了三倍最后只能紧急停任务。如果你只是想写几十万行做一个小结果表直接 INSERT 完全没问题简单直接。但如果是大批量回写请务必走下面这条路。写入前还有两个参数值得关注。hive.hbase.wal.enabled控制写入时是否写 WAL默认是打开的关掉能提速但会牺牲可靠性——我的建议是别关宁可换方案也不要拿数据安全去换速度。5.2 generatehfiles先生成 HFile再批量装载这是大批量回写时我唯一推荐的方案。思路是让 Hive 不通过 HBase 的写入接口而是直接把数据按 HBase 的内部文件格式生成 HFile然后用 HBase 自带的装载工具把这些文件搬进表里。因为绕过了 WAL 和 MemStore装载过程对在线集群的影响极小速度也快得多。第一步打开生成 HFile 的开关并指定一个临时目录set hive.hbase.generatehfilestrue; set hbase.fs.tmp.dir/tmp/hbase-staging;第二步执行写入。注意目标表必须已经存在而且列族要跟你要写的数据对得上INSERT OVERWRITE TABLE dws_user_tag SELECT concat(user_id, _, dt) AS row_key, user_id, tag_value, dt FROM ods_user_tag_calc;第三步用 HBase 的装载工具把生成的 HFile 导入hbase org.apache.hadoop.hbase.mapreduce.LoadIncrementalHFiles \ /tmp/hbase-staging/dws_user_tag dws_user_tag这里有几个必须注意的点。临时目录的权限要对Hive 和 HBase 两个进程都需要能读写装载目标表的列族必须已经存在工具不会帮你建装载完成后记得清理临时目录否则下次任务可能读到脏文件。5.3 一条完整的数据链路示例从 MySQL 到 Hive 再到 HBase回到我开头提到的那个场景——用户标签需要从业务库算出来然后提供给在线接口查询。这条链路的典型形态是这样的第一步把 MySQL 的业务数据同步到 Hive 的 ODS 层用 Sqoop 或数据集成工具都行落到按天分区的表里。这一步的关键是分区字段要打上否则后面全量扫描的成本会失控。第二步在 Hive 里做标签计算输出到一张中间表。第三步把结果按 rowkey 规则拼好通过 HFile 的方式装载到 HBase。rowkey 我一般用主体 ID 标签类型的组合这样在线接口拿主体 ID 做前缀扫描就能一次取到所有标签不用做二次聚合。第四步验证。装载完不要只看任务成功与否一定要抽样比对从 Hive 中间表随机抽 100 行去 HBase 里按 rowkey 查回来逐字段核对。我在这步抓到过不止一次部分 Region 装载失败但任务返回成功的情况。5.4 幂等设计重复跑一次任务不应该产生重复数据离线任务的一个基本要求是可重跑。HBase 本身就天然支持这一点——同一个 rowkey、同一个列、用同一个时间戳写入结果就是覆盖不会产生多行。所以设计的重点是把 rowkey 和时间戳安排好。rowkey 里应该包含所有能唯一标识一条记录的维度比如主体 ID 业务日期 类型。这样重跑同一个日期分区时rowkey 完全一致写入就是覆盖。如果 rowkey 里漏了某个维度重跑就会产生两条不同的记录线上查询结果直接翻倍这种事故排查起来极其痛苦因为数据看起来都对只是多了一份。时间戳这一块我的习惯是让写入方显式指定而不是依赖服务端时间。因为服务端时间在重跑场景下会变一旦某次任务重跑单元格会保留新旧两个版本读取时需要额外的版本处理逻辑。用固定的业务时间戳行为完全可预期。6. 几个真实踩过的坑以及我的巡检清单6.1 一次全表扫描把在线集群拖垮的复盘这个事故值得完整讲一遍。某天下午一个数据分析同学在 Hive 里执行了一条针对 HBase 外部表的 SQL条件是WHERE dt 2024-01-01而dt只是一个普通列不是 rowkey 的一部分。结果是全表扫描11 个 map 任务同时从 HBase 拉全量数据。当时的排查过程是这样的先是监控告警RegionServer 的读请求队列积压到几千然后看 RegionServer 日志发现大量来自同一个 Hive 任务的 Scan 请求接着在 Hive 侧用list命令找正在跑的查询定位到具体 SQL最后是 kill 掉任务等队列消化。事后复盘得出的结论有三条第一dt这种时间维度如果经常被用来过滤就应该进 rowkey而不是当普通列存第二对外部表的访问应该加一层控制长查询要有超时和资源队列第三重要表应该配置快照让分析需求走快照读不碰线上 Region。从那之后我给所有对外暴露的 HBase 外部表都加了一条规矩只允许走 rowkey 条件的查询非 rowkey 条件的一律走快照或导出表。6.2 权限与认证环境下额外要确认的东西如果集群开了认证体系Hive 访问 HBase 需要确保 Hive 服务本身具备对应的访问凭据并且凭据的有效期覆盖任务运行周期。我遇到过任务跑到一半失败报的却是连不上 ZooKeeper最后查出来是凭据过期导致的会话失效。这类问题的排查思路是先看认证组件日志再看 HBase 日志不要被表象的报错带偏。权限这块还有一个容易被忽略的点Hive 建外部表这个动作本身会去读取 HBase 表的元数据。如果账号只有数据的读权限、没有元数据读权限DDL 阶段就会失败报错信息还比较含糊。遇到建表失败但数据明明能读的情况往这个方向查。6.3 我每次上线前必看的巡检清单养成了固定检查的习惯之后这类问题少了很多。下面这份清单是我在每次新增整合表之前都会过一遍的检查项检查方式通过标准jar 版本一致性逐个 jar 包对比版本号所有 hbase 相关 jar 同一版本rowkey 是否覆盖主要查询维度对照业务查询清单高频条件都在 rowkey 前缀里Region 是否均衡hbck 查看分布最大最小 Region 差距在两倍以内新列是否加在映射串末尾人工核对原有列位置未变动是否使用 EXTERNAL查看 DDL全部为外部表是否有快照保护查看表属性核心表有快照或导出副本抽样比对结果随机抽 100 行核对字段值完全一致这张表看起来琐碎但每一条都对应着我或者同事真实踩过的一个坑。6.4 什么时候应该果断放弃整合最后说个反方向的判断。整合不是银弹有些情况下硬上反而更糟。如果数据量已经大到单表几十亿行、Region 上千个Hive 每次查询都要起上千个 map调度开销比计算开销还大的时候就该考虑把数据按天导出成 HDFS 上的列存格式用 Hive 直接读文件只在需要明细追溯时才回查 HBase。这种离线副本 在线源库的双轨模式在很多大规模场景下比直接整合更稳。如果业务对 HBase 侧的延迟极其敏感连一次全表扫描的资源波动都承受不起那也应该走快照读路线把离线分析彻底隔离到独立集群。判断标准其实就一句话整合的价值在于省掉数据搬运如果为了整合反而要付出更大的稳定性代价那这笔账就不划算了。我个人在这条链路上折腾了几年的体会是Hive 和 HBase 的整合技术上的难点其实很少真正的难点在约定——约定 rowkey 怎么设计、约定类型怎么存、约定谁能查、约定什么时候走快照。把这些约定提前定死写进文档落到检查清单里后面九成的问题都不会发生。至于那几个 jar 包和类加载报错无非是多查几次日志的事谈不上什么门槛。真正让人头疼的永远是那些数据看起来对、但是不对的情况而它们几乎都源于早期没有约定好存储格式。如果你正准备动手我的建议是先用一张小表把整条链路走通建 HBase 表、建 Hive 外部表、读一次、写一次、用 HFile 装载一次。这五个动作跑完剩下的就只是规模问题而不是原理问题了。
返回列表