简介:这是一份关于Flink与HBase在阿里巴巴电商业务中应用的PDF文档,适合大数据实时计算方向的技术人员、数据研发工程师及架构师阅读。文档完整整理了阿里技术专家秋奇(李剑)的实践分享,从业务背景、典型场景到技术架构层层展开,重点剖析报表监控、商品库管理、用户足迹分析、供应链预警、全链路debug平台等真实案例,并给出基于Datahub接入、Flink流计算、HBase存储的实时数仓设计方法。实现部分包含groupBy聚合、输出到HBase的TableUtil.writeToHbaseSink、HBase表DDL定义、维表JOIN等关键代码片段,同时介绍异构数据源支持、2000+机器部署、单机QPS 20W及亿级数据量处理等生产级优化策略,方便读者直接借鉴落地。压缩包内是1个PDF文件,大小约2.9MB,内容紧凑、图文与代码并重,已有167人学习,适合希望快速掌握实时计算与大数据存储结合方案的开发者。
1. 一份文档,一条链路:Flink 与 HBase 在电商实时业务里到底怎么配
互联网上经常看到类似「2-4 Flink HBase 阿里巴巴电商业务中的应用.pdf」这种命名方式,它不像一本系统的书,更像一份内部技术分享的存档。抛开具体 PDF 的页数不谈,这类标题指向的问题非常集中:电商业务里的实时数据,为什么偏偏要选 Flink 做计算、选 HBase 做存储?用户在双 11 下单后的那一秒,订单数据经过几跳才能出现在「我的订单」里?这篇文章要讲的就是这条链路的完整落地路径——从架构选型、环境部署、代码实现,到 Rowkey 设计、WAL 异常排查和生产验证。适合正在做实时数仓、实时风控或用户画像的工程师,也适合手里只有一个 Flink 作业模板、却不知道 HBase 那一端该怎么配合的人。
2. 电商实时链路的架构分工:为什么是 Flink 与 HBase,而不是别的组合
2.1 先画一张架构草图:消息、计算、存储三层各干什么
阿里电商业务的实时链路,最经典的分层是三层:消息层、计算层、存储层。消息层通常是 Kafka,负责把订单创建、支付成功、物流状态变更、用户点击事件收集起来;计算层是 Flink,负责消费这些消息,做实时 ETL、窗口聚合、维度关联、风控规则判断;存储层才是 HBase 登场的时机——处理完的结果要落下去,供在线业务实时查询。
这里有一个经常被误解的点:Flink 不是「数据库」,它是一个流式计算引擎,状态和 Checkpoint 虽然在内存和文件系统里,但长时间保存结果、提供点查服务并不是它的职责。Flink 算完一批「近 5 分钟支付金额」或「用户最近 100 条行为序列」,必须写到某个能扛高并发随机读的存储里。HBase 就是为这种场景设计的:海量写入、稀疏列、按 Rowkey 毫秒级点查,且不要求预先定义严格的 schema。
所以在电商场景里,分工是:Kafka 负责缓冲流量洪峰,Flink 负责把无序事件变成有序业务结果,HBase 负责把结果沉淀成可被前端、推荐、风控系统随时查询的服务。三者之间最典型的连接方式就是 Kafka 作为 Flink 的 Source,HBase 作为 Flink 的 Sink 或维表。数据流是单向的,但查询是随机的、并发的、低延迟的。
有经验的同学会在这里追问:为什么不把结果写回 MySQL?这就要看下一节的边界分析。
2.2 存储选型对比:HBase 与 MySQL、Redis 的边界在哪
电商 team 里最常出现的争论是「HBase 能干的,MySQL 加索引也能干,Redis 更快,为什么还要 HBase」。我用一张表格把边界讲清楚,这张表也是给评审看的最直接论据。
| 对比维度 | MySQL | Redis | HBase |
|---|---|---|---|
| 数据量 | 单表千万级需要分库分表 | 取决于内存,大 key 昂贵 | 百亿行起步,靠 HDFS 横向扩展 |
| 写入模型 | 随机写,受磁盘 IOPS 限制 | 内存写,落盘成本高 | 顺序写 HDFS + WAL,批量吞吐高 |
| 读模式 | 二级索引、复杂 SQL | KV 点查,无复杂条件 | 仅 Rowkey / 范围 Scan,无索引 |
| Schema | 固定列 | 无 | 稀疏列,列族内可动态加列 |
| 一致性 | 强一致 | 视配置而定 | 强一致(单 Row 级别) |
| 成本 | 分库分表后运维很重 | 内存昂贵 | 普通服务器就能跑,成本较低 |
这套对比落到电商场景里的结论是:订单流水、用户行为序列、实时风控命中记录,每张表都是亿级起步,且写入是业务主动产生的持续高压,HBase 的批量写能力更匹配。Redis 适合放热 Key 和计数器,比如库存扣减、排行榜;MySQL 适合放订单主档这种需要事务、需要按多个字段查的数据。但订单主档最终也要归档到 HBase 做历史查询,因为 MySQL 扛不住全量历史在线读。
注意,HBase 不是万能的。它没有二级索引,你要查「今天支付金额大于 100 的所有订单」,直接用它做是灾难;这种场景应该让 Flink 先聚合好,再把聚合结果按订单号或用户 ID 写进 HBase,查询永远走 Rowkey。
2.3 电商场景最常见的三张 HBase 表:订单、用户行为与实时库存的列族设计
理解了边界后,我们看典型的表设计。我自己在项目里最常用到的是下面三张表,它们几乎覆盖了 90% 的电商实时需求。
第一张是订单实时表,表名叫order_realtime。Rowkey 用「支付时间取反 + 订单号散列」,列族用cf,里面放订单金额、商品 ID、买家 ID、卖家 ID、订单状态。这张表服务的是「我的订单」列表、客服查单、风控抽查。列族只有一个就够,不需要把下单金额和支付状态拆到多个列族,因为 HBase 一个列族下文件数量太多时,Region 的 MemStore 刷新压力会变大,多个列族反而增加管理复杂度。
第二张是用户行为序列表,user_behavior。Rowkey 用「用户 ID 反转 + 时间戳」,列event的多个版本保存点击、加购、收藏行为。这张表的数据来源是 Flink 消费用户行为日志后按用户聚合,写到 HBase 时每一列对应一类行为,比如cf:click_1h、cf:cart_1h。这样推荐系统拿到一个用户 ID 后,Scan 最近几十个列就能快速构造特征,不用再去 Hive 里跑离线任务。
第三张是实时库存表,stock_realtime。Rowkey 用「SKU ID 前缀 + 仓库 ID」, 列cf:available存可用库存,cf:frozen存冻结库存。这张表的特点是高频更新,Flink 从订单消息中实时扣减库存,写入量极大,所以一定要做预分区,且列族里关闭块缓存(BLOCKCACHE=>'false'),因为库存是强读值,不需要缓存旧数据。
三张表的设计背后其实是一套原则:Rowkey 必须能支撑查询路由;列族尽量少;需要实时更新的列用独立列存值,不要塞进一个 JSON 字符串里,否则后续无法用 HBase 的增量操作。这些原则在后面的 Rowkey 章节会再深入。
3. 从零跑通 Flink + HBase:安装配置、版本匹配与端口连通性
3.1 安装踩坑最少的方式:HBase 伪分布式 + Flink Standalone
很多人在看完架构图后直接去生产环境搭集群,结果被 kerberos、云厂商外网隔离、防火墙端口限制折磨得怀疑人生。我一般建议先在本地跑一个「最小可复现环境」:HBase 用伪分布式,Flink 用 Standalone 单机模式。这个组合足够验证连接器和代码逻辑。
先装 HBase 伪分布式。下载对应 Hadoop 版本的二进制包后,编辑conf/hbase-site.xml,关键配置如下:
<configuration> <property> <name>hbase.rootdir</name> <value>hdfs://localhost:9000/hbase</value> </property> <property> <name>hbase.cluster.distributed</name> <value>true</value> </property> <property> <name>hbase.zookeeper.quorum</name> <value>localhost:2181</value> </property> <property> <name>hbase.unsafe.stream.crash.tolerance</name> <value>allow</value> </property> <property> <name>hbase.wal.provider</name> <value>filesystem</value> </property> </configuration>这里说明一下:hbase.cluster.distributed设为true表示分布式模式,但只有一个 RegionServer,所以叫伪分布式;hbase.rootdir指向 HDFS 路径,如果没搭 HDFS,也可以临时用本地文件系统(但生产别这么干);hbase.zookeeper.quorum是客户端要连接的 ZooKeeper 地址,HBase 自身的协调依赖它。
Flink Standalone 更简单,下载二进制包后直接执行bin/start-cluster.sh,默认会启动一个 JobManager 和一个 TaskManager,Web UI 在 8081 端口。HBase 启动顺序要注意:先确认 HDFS 起来了,再执行bin/start-hbase.sh。如果 HDFS 没起,HBase 的 Master 会卡在master initialing,后面避坑章节会专门说。
3.2 版本匹配表:Flink 与 HBase 客户端兼容的几种常见组合
版本匹配是新手第一个翻车点。Flink 的 HBase 连接器是分版本维护的,不同 Flink 版本依赖的 HBase 客户端版本不同,而 HBase 客户端和 HBase 服务端之间也存在兼容红线。我在多个项目里验证过比较稳的组合,列成一张表:
| Flink 版本 | 推荐的 HBase 连接器 | HBase 服务端版本 | 说明 |
|---|---|---|---|
| Flink 1.12 | flink-connector-hbase-1.4 | HBase 1.x | 老项目常见,依赖.4 |
| Flink 1.13 - 1.15 | flink-connector-hbase-2.2 | HBase 2.x | 最常用,支持 2.0 以上 |
| Flink 1.16+ | flink-connector-hbase-2.2(继承) | HBase 2.4+ | 新版 Flink 仍复用 2.2 连接器 |
| Flink CDH 发行版 | 自带 hbase-shaded 客户端 | CDH HBase 1.2 | 注意 shading 冲突 |
注意表格里有个坑:连接器包名里的版本号对应的是 HBase 客户端主版本,不是 Flink 版本。比如flink-connector-hbase-2.2.jar里的 2.2 指的是 HBase 2.2 的客户端。如果你的 HBase 服务端是 1.4,却引入 2.2 连接器,大概率出现NoSuchMethodError,因为客户端调用了服务端不存在的方法。
解决版本冲突的常见做法是用 shaded 包。Flink 官方提供的 HBase 连接器有两种:hbase和hbase-shaded,后者把 HBase 依赖重新打包,避免与作业中的其他 HBase 版本冲突。作业提交时把 shaded 连接器放入FLINK_HOME/lib,或者在 Maven 依赖里用provided作用域排除冲突。我在 1.14 上碰到过作业里同时存在 HBase 2.1 和 2.2 客户端导致 Region 扫描异常,最终统一用 2.2 shaded 解决。
HBase 服务端版本一旦定了,尽量别频繁升级。HBase 2.x 与 1.x 的 RPC 协议不兼容,跨大版本客户端访问会直接抛DoNotRetryIOException。生产环境升级前,一定要先在测试环境跑通。
3.3 用 Flink SQL 验证连通性:创建 HBase 表并写入一条数据
会写代码前先验证连通性,最有效的方式是用 Flink SQL。Flink 1.15 以上版本支持直接通过CREATE TABLE关联 HBase,不需要写 Java 类。下面这段 SQL 可以当作连通性测试的「Hello World」。
CREATE TABLE hbase_order ( rowkey STRING, cf ROW<order_amount DOUBLE, order_status STRING> ) WITH ( 'connector' = 'hbase-2.2', 'table-name' = 'order_realtime', 'zookeeper.quorum' = 'localhost:2181', 'sink.buffer-flush.max-rows' = '100', 'sink.buffer-flush.interval' = '2s' ); INSERT INTO hbase_order SELECT 'order_001', ROW(99.90, 'PAID');这段 SQL 的逻辑是:在 Flink 侧声明一个名为hbase_order的映射表,rowkey是唯一主键,cf列族下的两个列通过ROW类型映射。执行前要确保 HBase 里已经存在order_realtime这张物理表,且列族名是cf,否则 Flink 写数据时报TableNotFoundException。
WITH参数里最关键的是zookeeper.quorum,它决定了 Flink 作业如何找到 HBase 集群。很多刚上手的人只改了table-name,忽略了 zooKeeper 地址,本地作业连远程集群时一直超时。另一个参数sink.buffer-flush.max-rows控制缓冲区大小,100 行刷新一次对测试场景合适,生产建议调到 1000 以上。
如果 INSERT 执行没有报错,用 HBase shell 执行get 'order_realtime', 'order_001'能看到数据,说明 Flink 与 HBase 的网络、认证、协议三层全部通了。这一步过了,再进入正式的业务代码开发。
4. 把订单流写进 HBase:DataStream 自定义 Sink 与 Flink SQL 两种落地姿势
4.1 方式一:DataStream 自定义 DataSink,用 BufferedMutator 做批量写
Flink SQL 适合简单场景,但电商业务里经常要拿到流里的原始字段做过滤、纠偏、补维度,这时候 DataStream API 更灵活。自定义 Sink 的标准做法是继承RichSinkFunction,在open()方法里创建 HBase 连接,在invoke()方法里逐条写。但逐条写性能太差,所以一定要用BufferedMutator做批量提交。
public class OrderHBaseSink extends RichSinkFunction<OrderEvent> { private Connection conn; private BufferedMutator mutator; @Override public void open(Configuration parameters) throws Exception { org.apache.hadoop.conf.Configuration conf = new org.apache.hadoop.conf.Configuration(); conf.set("hbase.zookeeper.quorum", "zk1:2181,zk2:2181"); conf.set("zookeeper.znode.parent", "/hbase"); conn = ConnectionFactory.createConnection(conf); BufferedMutatorParams params = new BufferedMutatorParams(TableName.valueOf("order_realtime")); params.writeBufferSize(4 * 1024 * 1024); // 4MB buffer params.setWriteBufferPeriodicFlushTimeoutMs(2000); mutator = conn.getBufferedMutator(params); } @Override public void invoke(OrderEvent event, Context context) throws Exception { Put put = new Put(Bytes.toBytes(event.getRowkey())); put.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("order_amount"), Bytes.toBytes(String.valueOf(event.getAmount()))); put.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("order_status"), Bytes.toBytes(event.getStatus())); mutator.mutate(put); } @Override public void close() throws Exception { if (mutator != null) mutator.close(); if (conn != null) conn.close(); } }这段代码的逻辑是:open()里只创建一次连接和批量写入器,避免每条数据都新建连接;invoke()里构造Put对象并交给BufferedMutator,它内部积累到 buffer 大小或超时后自动提交。这里有个容易被忽略的参数:writeBufferSize设为 4MB,默认值是 2MB,调大后单次 RPC 携带的写入量更大,吞吐更高,但内存占用也上升。setWriteBufferPeriodicFlushTimeoutMs(2000)是为了兜底,防止低流量场景下数据积压太久。
注意 HBase 连接不是线程安全的,一个Connection只能给一个 Sink 用。如果并行度是 10,每个并行实例都会调用open(),各自创建连接,这是合理的。但不要共享一个静态连接,否则并发 mutate 会出现NullPointerException或数据错乱。
4.2 方式二:Flink SQL 维表 JOIN,用 HBase 关联订单明细
实时计算里另一大类需求不是写结果,而是用 HBase 作为维表,给订单流补上商品信息或卖家等级。Flink SQL 的TEMPORARY TABLE特性很方便,但要区分主表和维表的声明方式。下面用订单流和商品维表做示例。
CREATE TABLE order_stream ( order_id STRING, sku_id STRING, amount DOUBLE, event_time TIMESTAMP(3), WATERMARK FOR event_time AS event_time - INTERVAL '5' SECOND ) WITH ( 'connector' = 'kafka', 'topic' = 'order_topic', 'properties.bootstrap.servers' = 'kafka:9092', 'scan.startup.mode' = 'latest-offset', 'format' = 'json' ); CREATE TABLE sku_dim ( sku_id STRING, sku_name STRING, category_id STRING, PRIMARY KEY (sku_id) NOT ENFORCED ) WITH ( 'connector' = 'hbase-2.2', 'table-name' = 'sku_dim', 'zookeeper.quorum' = 'zk1:2181' ); CREATE TABLE enriched_order ( order_id STRING, sku_id STRING, amount DOUBLE, sku_name STRING ) WITH ( 'connector' = 'print' ); INSERT INTO enriched_order SELECT o.order_id, o.sku_id, o.amount, d.sku_name FROM order_stream o LEFT JOIN sku_dim FOR SYSTEM_TIME AS OF o.event_time AS d ON o.sku_id = d.sku_id;逻辑说明:order_stream是 Kafka 里的订单事件,带事件时间和水位线;sku_dim声明为 HBase 维表,主键是sku_id;FOR SYSTEM_TIME AS OF是时态表连接的固定语法,表示在事件时间发生时去关联那个时刻的维表快照,这是实时维表关联的标准写法,能避免结果漂移。LEFT JOIN可以保留关联不上的订单,在电商里 SKU 已下架时非常有用。
这里要特别提醒:HBase 作为维表时,Flink 默认是每一行都同步查询一次 HBase,延迟可能达到几十毫秒,吞吐上不去。所以生产上一定要给维表开启缓存,比如 Flink 1.15 之后在 HBase 连接器里支持'lookup.cache.max-rows'和'lookup.cache.ttl'参数。很多人没开缓存,以为维表 JOIN 慢是网络问题,其实是 HBase 每行都打 RegionServer 导致的。
4.3 必须调整的写入参数:批次大小、重试与 buffer 刷新
无论是 DataStream 还是 SQL,HBase 连接器有一组参数是每次上线前都要确认的。我整理成一份自己的检查清单:
| 参数 | 推荐值 | 作用 |
|---|---|---|
hbase.client.write.buffer | 4MB - 8MB | 客户端 RPC 缓冲区大小,越大约吞吐越高 |
hbase.client.retries.number | 10(生产) | 单个 RegionServer 异常时的重试次数,太大会拖慢失败恢复 |
hbase.client.pause | 500ms | 重试间隔,默认 1000,可缩短 |
sink.buffer-flush.max-rows | 1000 - 5000 | Flink sink 侧批量提交的行数 |
sink.buffer-flush.interval | 1s - 3s | 时间兜底,防止低流量场景积压 |
rejected.executor | true | 写入被拒绝时,后台线程异步重试,防止主线程阻塞 |
参数里最容易踩坑的是rejected.executor。HBase 客户端在 RegionServer 繁忙时,队列积压会触发RejectedExecutionException,默认抛出异常导致 Flink 作业失败。打开异步重试可以缓解,但代价是数据写入延迟变高,而且重试日志会比较难看。我遇到过线上一个作业因为这个参数没开,每五分钟失败一次,重启后恢复,循环了一星期才定位到。
批次大小也不是越大越好。如果一次 flush 的行数超过 5000,而且每行有十几个列,单次 RPC 请求体可能超过 RegionServer 的hbase.server.rpc.maxsize(默认 128MB)导致请求被拒。通常的做法是先固定sink.buffer-flush.max-rows = 2000,观察 RegionServer 的 WAL 写入耗时,再逐步调大。
5. HBase 侧的设计与避坑:Rowkey 散列、预分区、WAL 异常和端口排查
5.1 Rowkey 设计:订单号加盐还是反转,取决于查询模式
HBase 性能上限很大程度由 Rowkey 决定,这是所有踩坑文章的第一条。电商场景里订单号的天然形态是「时间前缀 + 业务流水号」,比如20241119235912345。如果直接用这个字符串做 Rowkey,同一秒内产生的订单会落在同一个 Region Server 上形成写热点,时间一长数据倾斜很严重。
常见解法有两种。第一种是加盐:在 Rowkey 前面拼一个随机前缀,比如把订单号的 hash 值取后两位。这样写入会均匀分布到 100 个预分区里,但代价是查询时无法确定盐值,必须扫描所有分区。适合「写入为主、读取按时间范围扫」的场景,比如行为日志落库。
第二种是反转:把订单号或用户 ID 的前后部分颠倒。因为订单号尾号在自然生成时是随机的,反转后原本集中在前面的时间信息扩散到 Rowkey 尾部,随机性变强。这个方案的好处是只要知道完整 Rowkey 就能直接点查,查询效率高,适合「按订单号查状态」的服务。
我通常建议按查询模式来选。如果线上查询入口只有「按订单号查」,就做反转;如果要做流式分析,常常根据时间范围批量扫描,就做加盐,并把盐值字段额外保存在列里,方便后续按天扫描。不要同时加了盐又反转,那样会把可读性搞没,排查问题时会非常痛苦。
5.2 预分区与热点:写倾斜时先看 RegionServer 的 IO
HBase 新建表默认只有一个 Region,所有写入都压在同一个 RegionServer 上。所以建表时必须手工预分区,最土但有效的做法是确定 Rowkey 哈希范围后,把 16 个分区边界写进建表语句。
create 'user_behavior', 'cf', {SPLITS => ['1','2','3','4','5','6','7','8','9','a','b','c','d','e','f']}这个建表命令的意思是:对 Rowkey 的第一个字符做切分,切成 16 个区间。这里有个前提——Rowkey 的散列值字符必须均匀且排序合理。如果你用 MD5 或 SHA1 生成散列,第一个字符就是 0-f,正好对应。如果 Rowkey 是数字或者 ASCII 字符串,切分点要根据实际抽样值来定,不能拍脑袋。
热点问题排查时,先去 HBase UI 页面的 RegionServer 列表看每个 Region 的requests计数。如果某一个 Region 的请求量是其他的 10 倍以上,再用hbase hbck检查该 Region 的起止 Rowkey,大概率会发现写入的 Rowkey 前缀集中在某个区间。这时候需要检查上游 Flink 作业是否在拼 Rowkey 时用了相同的盐值,或者原始字段为空导致 hash 结果全为同一值。
还有一个很容易被忽略的热点来源:Flink 的并行度高于 HBase Region 数,但每个并行子任务写自己的 HBase 连接,连接池里的随机路由算法会让并发请求自然分散,所以并行度不是热点主因。真正的坑是 Flink 作业里把 Rowkey 写死成order_id原值,没做任何散列,导致大促高峰期某个订单前缀下的写入集中到一个 Region。
5.3 避坑清单:Master initialing、WAL 预写日志异常、端口连不上
下面这几条是我在不同项目里真实遇到过、且网上反复被搜的高频问题,按「现象 → 原因 → 解决」三个步骤拆开。
现象一:HBase Master 一直处于master initialing状态,页面能开但迟迟不进入 Active。原因:Master 启动时要初始化 HDFS 上的元数据目录,如果 HDFS 不可写或者 ZooKeeper 里残留旧集群的临时节点,初始化会卡住。最常见的是hbase.rootdir指向的 HDFS 路径没有写权限,或 HBase 进程是 root 跑的而 HDFS 默认目录属于某个用户。 解决:先用hdfs dfs -mkdir -p /hbase并确认可写,再在 ZooKeeper 里清理/hbase节点(先停掉 HBase 进程,再删除,最后重启)。如果/hbase元数据本身损坏,用hbase hbck -fix修复;如果元数据里库表结构不一致,直接删掉重建比修复快,前提是允许丢数据。
现象二:日志里持续刷WAL hlog was corrupted或hbase.wals路径异常。原因:WAL 预写日志是 HBase 写数据时先落盘的日志,RegionServer 异常宕机时 WAL 可能损坏。另一个常见原因是配置了hbase.wal.provider=asyncfs,在本地文件系统上不支持,或者 WAL 目录权限不对。 解决:检查hbase-site.xml里hbase.wal.provider的值。生产用asyncfs需要 HDFS 支持;本地伪分布式或测试环境用filesystem最稳。WAL 文件损坏但 RegionServer 还能启动时,把损坏的 WAL 文件从/hbase/WALs/目录移走备份,先恢复服务再决定是否用 HLog 恢复工具重建数据。千万别在生产环境直接删除 WAL 目录,那等于丢数据。
现象三:Flink 作业连 HBase 超时,报Can't get connection to ZooKeeper,但 ZK 明明活着。原因:HBase 2.x 的客户端端口和服务端端口不是一回事。客户端连的是 ZooKeeper 的客户端口 2181,而 HBase Master 的 RPC 端口是 16000,RegionServer 的 RPC 端口是 16020。很多人在本地调试时,只在本机防火墙放了 2181,没放 16020,Flink 作业在读写数据时抛异常。 解决:本地开发时,把三组端口全放通。
| 服务 | 默认端口 | 用途 |
|---|---|---|
| ZooKeeper | 2181 | HBase 客户端定位 Meta 表和 RegionServer |
| HBase Master | 16000 / 16010 | RPC 通信 / Web UI |
| HBase RegionServer | 16020 / 16030 | 数据读写 / Web UI |
| HDFS NameNode | 9870 / 8020 | 存储底层数据 |
现象四:Flink SQL 写 HBase 数据不报错,但读不到任何值。原因:常见于坑点里的「HBase 表不存在但 Flink 自动创建了空表」。连接器在表不存在时,默认不会自动建表,但也不会立刻报错,而是把写入缓存在 buffer 里,直到 flush 时才发现表不存在,抛出TableNotFoundException。此时看起来就像「写成功了但没数据」。 解决:执行 SQL 之前,先用 HBase shell 手动建表,并确认列族名与 Flink DDL 里的ROW类型一致。如果已经手动建表了,就把 Flink 作业重启,让新 DDL 生效。
现象五:Flink 作业反压高,但 HBase 吞吐上不去。原因:不一定是 HBase 慢,很有可能是 HBase 的 RegionServer 垃圾回收停顿或者磁盘写入等待。我见过一个案例,RegionServer 的 JVM 堆只有 8GB,但 Region 数量开到了 300 个,每个 Region 的 MemStore 都要占用内存,导致频繁 GC。 解决:先看 RegionServer 的 GC 日志,调整HBase RegionServer 的内存配置。先数一下 Region:每个 Region 至少预留 128MB 内存给 MemStore,100 个 Region 需要 12.8GB。然后看 HBase 的 flush 线程是否堆积,把hbase.hstore.flush.retries.number` 从默认 3 调大到 10,避免 Flush 失败后阻塞后续写入。
6. 生产前的三点验证:数据一致性、反压监控与血缘管理
6.1 验证一:从 HBase 反查最近 10 条订单,确认链路不漏数
写完作业后,第一步不是看 Flink UI 的numRecordsOut,而是直接去 HBase 里验证结果。用 HBase shell 的scan命令,按时间范围扫最近数据。
scan 'order_realtime', {STARTROW => '20241119', ENDROW => '20241120', LIMIT => 10}这个命令的逻辑是:扫描 Rowkey 在 20241119 到 20241120 之间的所有行,最多返回 10 条。前提是你的 Rowkey 设计保留了时间前缀。如果之前加了盐,Rowkey 第一位不是时间,这个 scan 就扫不出想要的数据——这也反过来检验了你 Rowkey 设计是否合理。
观察返回结果时,重点关注两个指标:一是订单数量跟 Flink 侧统计的numRecordsOut是否接近;二是每条数据的order_status是否都是最新状态。如果 status 还有 PENDING 老值,说明 Flink 作业在写 HBase 时没有做状态去重,导致旧事件覆盖了新事件。电商订单状态流转用「最后写入获胜」策略会出错,正确做法是 Flink 里按订单号分组,保留最新一条事件后再写 HBase。
6.2 验证二:用 Flink UI 火焰图与反压判断瓶颈
作业跑起来后,在 Flink Web UI 的 Job 页面里能看到每个 Task 的反压状态。反压是三个颜色:绿色正常、黄色接近饱和、红色已满。如果某个 Source 或 Operator 长时间红色,说明下游处理不过来或下游存储写入慢。
火焰图是更直接的性能定位工具。Flink 1.15 之后可以在 TaskManager 日志的堆栈采样里生成 Flame Graph,但操作路径比较隐蔽。我一般更常看 HBase 侧的表现:打开 HBase RegionServer 的/rs-status页面,看写请求延迟和 WAL 写入耗时。如果延迟持续超过 50ms,就在 Flink 作业里加独立线程打印 Sink 的排队长度,方法是在自定义 Sink 里周期性输出mutator.getWriteBufferSize(),观察缓冲区是否始终接近上限。
如果确认瓶颈在 HBase 侧,优先排查是不是写热点,然后看sink.buffer-flush参数是否过小。我调过的项目中,最常见的无效操作是盲目增大 Flink 并行度,而 HBase 侧只有两个 RegionServer,并行度再高,瓶颈还是两个 RegionServer 的处理能力。
6.3 验证三:用 OpenMetadata 获取 Flink 血缘关系,避免元数据黑洞
线上链路一多,最怕的就是没人说得清某张 HBase 表的数据是从哪个 Kafka topic 来的、中间经过了哪些 Flink SQL 处理。OpenMetadata 这类元数据平台可以通过扫描 Flink 作业的 DDL 和 DML 语句,自动解析出Source → Transform → Target血缘。
具体做法是把 Flink SQL 提交到 OpenMetadata 的 ingestion 流程里,它会把CREATE TABLE、INSERT INTO之间的引用关系提取出来,画成一张血缘图。这样当 HBase 的一张表数据异常时,你能顺藤摸瓜找到上游的 Flink 作业,而不是翻遍每个人的笔记。
这个验证还有一个实际作用:给评审和运维留底。我们在做数据治理时,经常被问「这张表挂了影响哪些下游」,有血缘关系图之后,五分钟能给出准确列表。建议从项目第一天就把血缘关系拉起来,攒到十几条链路之后再补,成本和风险都会成倍上升。
最后说一个我自己的教训:有一次上线前偷懒,没有做 6.1 的反查验证,结果 HBase 里只有新数据、没有历史数据,排查了三天才发现是 Flink 作业里把 Rowkey 的散列函数写错,导致新旧事件被路由到相同 Rowkey 互相覆盖。从那以后,每次上线必做「HBase 反查最近 N 条 + Flink 计数比对」两步,十分钟就能发现大多数链路问题。这个习惯帮我挡住了很多次大促前夜翻车,希望也能帮到你。
本文还有配套的精品资源,点击获取