1. Hive 在 Flink 里到底能干嘛
核心就两件事:
1)读 Hive:既能一次性读(bounded),也能像流一样追新增(unbounded / streaming read)
2)写 Hive:批写支持 append/overwrite;流写支持持续写入并按策略提交分区(让下游逐步可见)
真正的价值在于:你可以做"实时数仓"的经典链路
Kafka 实时明细流 → Flink 清洗聚合 → 写 Hive 分区表 → 下游 Presto/Trino/Hive/Spark 直接消费最新分区
2. 读 Hive:Batch 读快照,Streaming 追增量
2.1 默认是 bounded:查询时刻的快照
Flink 默认把 Hive 表当 bounded source:扫一遍就结束。适合离线、回灌、批 ETL。
2.2 Streaming 读:持续监控新分区/新文件
打开 streaming-source.enable 后:
- 分区表:监控"新分区出现",增量读新分区
- 非分区表:监控目录中新文件出现,增量读新文件
关键参数你至少要记住这几个:
streaming-source.enable:是否启用流式读取(默认 false)streaming-source.monitor-interval:多久扫一次元数据/目录(默认实现里常见 1min/60min 的策略差异)streaming-source.partition.include:读all还是lateststreaming-source.partition-order:latest 的判定规则(partition-name / partition-time / create-time)streaming-source.consume-start-offset:从哪个 offset 开始追(随 order 类型不同,写法不同)
2.3 不改表结构也能临时启用:SQL Hints OPTIONS
线上经常遇到"表是公共资产,DDL 不敢改",这时候用 hint 最舒服:
sql
SELECT *
FROM hive_table
/*+ OPTIONS(
'streaming-source.enable'='true',
'streaming-source.monitor-interval'='1 min',
'streaming-source.consume-start-offset'='2020-05-20'
) */;
这个技巧特别适合做临时回放、临时追分区、临时验证链路。
2.4 读 Hive 的几个硬性注意事项(避坑必看)
1)原子性要求
- 非分区表:新文件必须"原子写入"到目录(写一半被扫描到会读到不完整数据)
- 分区表:新分区在 Hive Metastore 视角也要"原子可见"(否则你往老分区追加文件会被当成新数据重复消费)
2)分区多会慢
Streaming 的监控策略本质是扫目录/分区列表,分区爆炸会明显拖慢,甚至压垮 metastore。
3)Streaming 读 Hive 表不能在 DDL 里定义 watermark
也就是说:你不能直接把这种 streaming hive source 当事件时间流去开窗口算子(至少在这块能力上要接受限制),需要你在上游自己构造时间语义,或者改用其他承载(比如 Kafka)。
2.5 读 Hive View 的小限制
- 必须先
USE CATALOG到 HiveCatalog - View 里的 SQL 语法要兼容 Flink(Hive SQL 和 Flink SQL 关键字/字面量可能不同)
2.6 读性能优化:向量化、并行度推断、Split 调优
你做大表查询时,性能通常卡在三个点:文件格式、split 划分、并行度策略。
1)向量化读取(ORC/Parquet)
满足条件(ORC/Parquet + 不含复杂类型)会自动启用;如需关闭可用配置 table.exec.hive.fallback-mapred-reader=true。
2)Source 并行度推断
table.exec.hive.infer-source-parallelism.mode 支持 static/dynamic/none。生产里更常用 dynamic(执行期推断更准),但也要给个上限:table.exec.hive.infer-source-parallelism.max。
3)Split 划分调优(尤其 ORC)
table.exec.hive.split-max-size:单个 split 最大字节数(默认 128MB)table.exec.hive.file-open-cost:打开文件的"估算成本"(默认 4MB)
小文件很多时,把 open-cost 估高一点,Flink 更倾向于合并成更少 splits,减少调度/打开文件开销。
4)分区太多时:多线程加速元数据加载
table.exec.hive.calculate-partition-size.thread-numtable.exec.hive.load-partition-splits.thread-numtable.exec.hive.read-statistics.thread-num
3. Temporal Join:用 Hive 当维表,最常见的实时数仓玩法
3.1 Processing-time temporal join:Flink 目前主力支持
典型用法:Kafka 明细流(带 proctime)去关联 Hive 维表,拿到"处理时刻看到的最新维度"。
sql
SELECT *
FROM orders_table AS o
JOIN dimension_table FOR SYSTEM_TIME AS OF o.proctime AS dim
ON o.product_id = dim.product_id;
事件时间(event-time)temporal join Hive 目前还不在这套路径里,别一上来就死磕 event-time。
3.2 两种"最新"的语义:最新分区 vs 最新整表
场景 A:维表每天生成一个"全量快照分区"
这类最经典,强烈建议用 "latest partition as temporal table"。
Hive 侧建表(Hive dialect)时,直接配置:
streaming-source.enable=truestreaming-source.partition.include=lateststreaming-source.monitor-interval=12 h(别太频繁,Metastore 会顶不住)streaming-source.partition-order选 partition-name / create-time / partition-time
场景 B:维表是整表 overwrite(非分区或你就想扫全表)
这时是 "latest table as temporal table",Flink 会把表加载进每个 join subtask 的内存缓存里,用 TTL 控制多久刷新一次:
lookup.join.cache.ttl(默认 60min)
重要提醒:每个并行子任务都有一份缓存,维表必须能放进 slot 内存,否则直接 OOM。
4. 写 Hive:Batch 写可 overwrite,Streaming 写靠分区提交让下游可见
4.1 Batch 写:INSERT INTO 追加,INSERT OVERWRITE 覆盖
批模式写 Hive 时,数据一般在 Job 结束后一次性可见。
sql
INSERT INTO mytable SELECT 'Tom', 25;
INSERT OVERWRITE mytable SELECT 'Tom', 25;
分区写也支持静态/动态混合:
sql
-- 全静态分区
INSERT OVERWRITE myparttable PARTITION (my_type='type_1', my_date='2019-08-08')
SELECT 'Tom', 25;
-- 全动态分区
INSERT OVERWRITE myparttable
SELECT 'Tom', 25, 'type_1', '2019-08-08';
-- 静态 + 动态
INSERT OVERWRITE myparttable PARTITION (my_type='type_1')
SELECT 'Tom', 25, '2019-08-08';
4.2 Streaming 写:持续写入 + 分区提交策略
流写不支持 INSERT OVERWRITE,只能持续 append,并通过 partition commit 让下游逐步看到完整分区。
典型配置长这样(Hive dialect 建表):
sql
SET table.sql-dialect=hive;
CREATE TABLE hive_table (
user_id STRING,
order_amount DOUBLE
) PARTITIONED BY (dt STRING, hr STRING)
STORED AS parquet
TBLPROPERTIES (
'partition.time-extractor.timestamp-pattern'='$dt $hr:00:00',
'sink.partition-commit.trigger'='partition-time',
'sink.partition-commit.delay'='1 h',
'sink.partition-commit.policy.kind'='metastore,success-file'
);
上游 Kafka 表(default dialect)带 watermark,写入时把时间拆成 dt/hr:
sql
SET table.sql-dialect=default;
INSERT INTO TABLE hive_table
SELECT
user_id,
order_amount,
DATE_FORMAT(log_ts, 'yyyy-MM-dd'),
DATE_FORMAT(log_ts, 'HH')
FROM kafka_table;
如果 watermark 用的是 TIMESTAMP_LTZ,记得把 sink.partition-commit.watermark-time-zone 设成会话时区,否则你会看到"分区提交莫名其妙晚几个小时"的现象。
4.3 写到 S3 想要 Exactly-once:关键开关
默认 streaming write 走"rename committer",S3 不适合做 exactly-once。想要 exactly-once,需要把:
table.exec.hive.fallback-mapred-writer=false
这样会用 Flink native writer(目前只对 parquet/orc 路线成立),更适合对象存储的语义。
5. 动态分区写入与小文件:性能和 OOM 的经典战场
5.1 动态分区写默认会按分区字段排序
目的:让 sink 一次写完一个分区,减少同时打开的 partition writer 数量,提高吞吐并避免 OOM。
批模式下可以用:
table.exec.hive.sink.sort-by-dynamic-partition.enable(默认 true)
如果你关掉它,分区很多且数据交织,特别容易出现 "太多 writer 同时打开 → OOM"。
5.2 分区多但数据不倾斜:用 DISTRIBUTED BY 把同分区聚到一起
在 Hive dialect 的 batch 场景里,你可以:
DISTRIBUTED BY <partition_field>SORTED BY <partition_field>
把同分区的数据尽量落到同一个节点,减少写端 writer 数。
6. 写端自动统计与 Compaction:让数仓更"好用"
1)Auto Gather Statistics(批模式)
默认会自动收集统计并写回 metastore,但文件多时会慢,可以关闭:
table.exec.hive.sink.statistic-auto-gather.enable=false
如果表是 Parquet/ORC,还能通过读取 footer 快速算 numRows/rawDataSize,但文件量大时仍建议调大线程:
table.exec.hive.sink.statistic-auto-gather.thread-num
2)File Compaction
- 流模式:行为和 FileSystem sink 类似(checkpoint 后合并临时文件)
- 批模式:按分区统计平均文件大小,小于阈值就触发合并
常用参数:
auto-compaction=truecompaction.small-files.avg-size(默认 16MB)compaction.file-size(目标文件大小)compaction.parallelism(自适应 batch 下尤其建议手动调大)
7. 生产落地建议:把"正确性验证"和"性能压测"做成一键切换
你前面让写的那套闭环,其实和 Hive 场景是绝配:
- 开发/联调:Sink 用 Print,把 join 后的关键字段打出来,看 RowKind、看分区字段、看维表是否按预期刷新
- 压测/定位瓶颈:Sink 换 BlackHole,吞吐跑满,观察反压、checkpoint、state size,判断瓶颈在 join/agg 还是在写 Hive
等你把要压测的那段 SQL(尤其是 join/agg/topn/UDF)贴出来,我可以直接给你改成两份脚本:
xxx_print_verify.sql:最小输出、最强断言,专门验对不验快xxx_blackhole_benchmark.sql:去掉外部 IO 干扰,专门压吞吐定位瓶颈