ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HBase 电商用户行为数据存储实战:Rowkey 设计与预分区指南

HBase 电商用户行为数据存储实战:Rowkey 设计与预分区指南 不是所有数据库都天生适合“海量写入 海量查询”这种矛盾场景尤其在电商这种用户行为数据每时每刻都在产生的业务里。HBase 之所以能成为很多大厂用户行为存储的标配方案核心就是它把“写路径”简化到了极致同时通过 Rowkey 设计和预分区把“读路径”控制在了可预期的范围内。这篇内容不聊虚的直接拆解 HBase 在电商行为数据场景下的设计方案、Java 开发细节和落地时的真实坑点。1. 电商用户行为数据的存储困境与 HBase 的定位1.1 行为数据到底有多“大”为什么传统关系型数据库扛不住先看一组电商场景下的典型数据特征。用户行为包括浏览、点击、搜索、加购、下单、收藏、支付成功等一个中等规模的电商平台日活百万级别每天产生的行为日志量大约在 5 亿到 20 亿条之间。单条记录不大几十到几百字节但问题是总量大、增速猛、峰值写入集中。MySQL 这类关系型数据库在这个场景下有几道过不去的坎写入瓶颈单机写入 TPS 在几千到一万左右就到头了行为日志这种动不动每秒几万到几十万条写入的流量直接打爆主库。存储成本行为数据要保留很长时间做用户画像、推荐模型训练、运营分析动辄几十 TB 甚至 PB 级别MySQL 的存储成本和运维成本都扛不住。扩展性差分库分表能缓解一部分问题但行为数据的查询维度按用户、按时间范围和关系型建模天然不匹配分片键设计非常痛苦。Schema 固定行为数据的事件类型、字段会随着业务迭代频繁新增MySQL 改表结构成本高、风险大。这时候 HBase 的价值就很清晰了。它是一个稀疏的、分布式的、面向列族的 NoSQL 数据库建立在 HDFS 之上用 Java 写的天生就是为海量结构化或半结构化数据设计的。单表可以存百亿行、PB 级数据写入性能在合理设计下能轻松支撑每秒几十万次 Put而且扩容就是加机器的事不需要像 MySQL 那样做数据重分布。1.2 HBase 为什么适合“用户行为”这个具体场景用户行为数据有一个鲜明特点一次写入多次读取极少更新。用户的浏览记录一旦产生就是事实后续不会修改顶多标记无效这和 HBase 的存储模型非常契合。HBase 不支持完整的事务和复杂 Join但行为分析根本不需要这些它只需要一个高性能的 Key-Value 存储外加一个能按 Rowkey 范围扫描的模型。另外HBase 的列族设计天然支持“按需读取”。一条行为日志可能有几十个字段但分析时往往只需要其中几个。HBase 按列族、按列存储数据查询时只需指定需要的 Column Family 甚至 Qualifier读出来的数据量远小于一行全字段这在海量数据下能显著减少 IO。我常用一个类比来解释 HBase 和关系库的差异MySQL 像一本装订好的 Excel 账本每行格式一致查询靠索引HBase 像一个无限大的货架仓库每个货架Region上有无数个编号唯一的包裹Rowkey包裹里按格子列族分类存放物品。你要找某个用户的全部行为直接奔着那个编号Rowkey 前缀去取出来就是。没有 Join 的必要因为所有数据都已经按“用户”这个维度摆好了。2. Rowkey 设计决定表性能的生死线2.1 Rowkey 在 HBase 中的核心地位HBase 的所有查询都依赖 Rowkey它决定了数据落在哪个 Region、以什么顺序存储。设计 Rowkey 是 HBase 应用开发里最重要的一环没有之一。一个差的 Rowkey 能让集群写入热点化、查询全表扫描一个好的 Rowkey 则让读写均匀分布、查询精确命中。Rowkey 设计有几个基本原则长度控制Rowkey 越短越好尽量控制在 8~16 字节。每条 Rowkey 都会随数据冗余存储100 亿行数据Rowkey 多 10 字节就是 100GB 的额外存储开销。散列性取模、哈希或用可逆算法处理前缀避免顺序递增带来的写入热点。查询友好把查询场景中恒定不变的维度放在 Rowkey 前缀让 Scan 能按前缀快速定位。2.2 三大经典 Rowkey 模式倒序、加盐、哈希倒序模式处理的是“最新数据最常查”的场景。电商平台看用户行为百分之九十的情况是查最近几天的。如果 Rowkey 是userId timestamp那同一个用户的数据会顺序写入最新数据排在 Region 尾部老数据在前面。查询最新行为时要 Scan 大半个 Region效率很低。把时间戳倒过来变成userId (Long.MAX_VALUE - timestamp)最新的行为就排在最前面Scan limit N 就能直接拿到最新 N 条效率极高。加盐模式处理的是“写入热点”问题。如果用userId直接做 Rowkey 前缀头部活跃用户大 V、爆款商品的数据会集中砸向同一个 Region形成热点。做法是在前缀加一个散列值比如MD5(userId).subString(0,4) userId timestamp。数据按哈希值均匀分布写入压力被打散但查询时也能直接算出同一个用户的前缀哈希值Scan 不受影响。这是一个用一点额外字节换取系统稳定性的典型取舍。哈希模式适合那些前缀本身没有顺序特性的业务键。比如商品维度的行为统计表用商品 ID 做前缀时如果商品 ID 是自增整数写入会集中在尾部 Region。用reverse(商品ID)或前缀加固定长度哈希把数据打散到所有 Region。代价是查询时也要先算一遍哈希。2.3 电商用户行为表的 Rowkey 落地设计结合电商实际情况我会推荐“用户维度 行为时间倒序”的主表方案Rowkey MD5(userId).substring(0,4) userId (Long.MAX_VALUE - eventTime)列族设计成两个cf_event存储事件本身属性Qualifier 用eventType、pageUrl、productId、categoryId、duration、device等。每个 Qualifier 都是一个单元格独立存储查询时按需拉取。cf_meta存储辅助信息比如eventId全局唯一 ID、traceId链路追踪、ft前端埋点校验字段。查询“某用户最近 100 条行为”的语义变成Scan startRow MD5前缀 userIdstopRow MD5前缀 userId 0xFFlimit 100一次 Scan 精准命中。统计“某用户今天浏览了多少次商品”时直接用 Get 拿到该用户某个时间段的全部行为再做计数。这套 Rowkey 我在实际项目中用过200 亿行的表单次前缀 Scan 响应在 20ms 以内稳定得很。常见误区直接用user_md5 userId做前缀这等效于在开头又加了一次随机散列查询时还要额外存一份映射关系。正确做法是用固定的 4 字节哈希前缀保证分布再用原始 userId 保证可拼接、可反解。3. 存储方案设计从数据模型到 Compaction 调优3.1 列族规划与版本控制HBase 列族的数量建议控制在 1~3 个不要学习关系库建十几个列族的习惯。每个列族会对应到 HDFS 上的一个目录列族越多Region 打开的文件数越多Compaction 和 Flush 的联动也越复杂。行为数据这种场景一个列族就够用最多加一个 meta 列族做辅助字段。版本数VERSIONS默认保留 3 个版本这是为支持“同一 Rowkey 多次更新保留历史值”设计的。但行为数据是 append-only 的同一 Rowkey 不会重复写入所以把版本数设置为 1 是合理的能省掉不少读路径上的版本比较开销。TTL 的设置更关键。用户行为数据一般有明确的生命周期实时分析要最近 1 小时的离线画像要最近 90 天的运营分析可能要 180 天的。HBase 原生支持按列族设置 TTL过期数据会被自动删除不需要业务方写定时任务一遍遍 Scan 删除老数据。建议把主行为表的 TTL 设置为 180 天明细超过 180 天直接交给数据仓库归档。3.2 预分区不预分区就等于慢性自杀HBase 建表时默认只有一个 Region所有写入先涌向这个 Region直到它超过阈值触发分裂Split。分裂过程会伴随 Region 下线、重新分配在线业务在这段时间可能出现写入毛刺甚至超时。行为数据这种高写入场景必须在大流量来临前通过预分区把 Region 数量铺开。预分区数量的估算公式并不复杂。假设单 Region 的合理数据量是 10GB默认 10GB 触发分裂建议控制在 5~6GB预计 30 天产生 1.5TB 数据那就需要1500GB / 5GB ≈ 300个 Region。再开一个冗余系数比如 360~400 个 Region。每个 Region 一个 SplitKeyRowkey 前缀哈希值是 16 进制 0 到 f共有 65536 种组合按65536 / Region数划分区间就行。建表时用 HBase Shell 就能完成create user_event_behavior, {NAME cf_event, VERSIONS 1, TTL 15552000, BLOOMFILTER ROW}, {NAME cf_meta, VERSIONS 1, TTL 15552000}, {SPLITS [0000, 0016, 0032, ...]}实际项目里我会写一个脚本按 Region 数均匀生成 SplitKey 列表配合自动化平台在建表时一次传入。注意SPLITS 这里是数组传入的是每个分区的起始 RowkeyRegion 数量等于 SplitKey 数量加一别算错。3.3 BloomFilter 与 BlockCache 的关键调优BloomFilter 是 HBase 读路径上容易被忽视的加速器。它解决的是“判断一个数据是否存在”的问题存在误判但不会漏判。行为数据的查询模式是按用户 ID 精确 Get 或短前缀 Scan把 BloomFilter 类型设为 ROW会在读取时先过滤掉大量根本不包含目标数据的 HFile显著减少磁盘随机读。对行为数据场景BlockCache 大小调大一点收益明显。读行为多集中在用户最近行为上缓存命中率上去了Scan 的 P99 延迟能降一半。RegionServer 堆内存的 40%~50% 分配给 BlockCache 是常见配置hfile.block.cache.size0.45左右。如果同时有较重的大 Scan 任务可以开 BucketCache 结合堆外内存做分层缓存。# RegionServer 关键配置示例 hfile.block.cache.size0.45 hbase.regionserver.global.memstore.size0.35 hbase.hregion.memstore.flush.size536870912 hbase.hregion.memstore.block.multiplier4MemStore 内存占比要保证写缓冲足够同时避免 Flush 频繁触发。行为数据写入量大如果 MemStore 太小会频繁触发小 Flush产生大量小 HFile后续 Compaction 压力剧增。flush.size设成 512MB配合block.multiplier4能让 MemStore 在 2GB 时才触发阻塞写入给 Flush 留出缓冲时间。4. Java 开发实录从建表到批量写入、前缀级联 Scan4.1 基础客户端环境配置与连接池HBase 的 Java 客户端开发最核心的类是org.apache.hadoop.hbase.client.Connection。这个对象是重资源内部维护了 RPC 连接池、ZK 会话和元数据缓存整个应用应该只创建一个共享实例不要每次请求都创建。实际生产里见过不少同学在函数里 new Connection直接把 RegionServer 连接数打到上限RegionServer 报 Blocked 异常业务全部超时。用 HBase 2.x 的 API 创建连接import org.apache.hadoop.hbase.HBaseConfiguration; import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.client.*; import org.apache.hadoop.conf.Configuration; import java.io.IOException; public class HBaseConnectionManager { private static volatile Connection connection; public static Connection getConnection() throws IOException { if (connection null || connection.isClosed()) { synchronized (HBaseConnectionManager.class) { if (connection null || connection.isClosed()) { Configuration conf HBaseConfiguration.create(); conf.set(hbase.zookeeper.quorum, zk1:2181,zk2:2181,zk3:2181); conf.set(hbase.zookeeper.property.clientPort, 2181); // 开启 RPC 压缩行为数据多为文本 JSON压缩收益明显 conf.set(hbase.client.rpc.compress, true); connection ConnectionFactory.createConnection(conf); } } } return connection; } }HBase 客户端的核心端口是 2181ZooKeeper、16020RegionServer RPC、16010RegionServer Web UI、16030HMaster Web UI。如果是 HBase 1.xRegionServer 的 RPC 端口可能是 60020。排查端口问题时先telnet一下 2181 确认 ZK 正常再看 16020 是否通这两个端口是客户端连接有没有问题的分水岭。4.2 建表 API 与预分区写入用 Java 建表和 HBase Shell 效果一致API 更灵活适合集成到自动化平台里。下面是预分区建表的代码public static void createTable(Connection conn, String tableName, int regionCount) throws IOException { TableName tn TableName.valueOf(tableName); try (Admin admin conn.getAdmin()) { if (admin.tableExists(tn)) { return; } TableDescriptorBuilder builder TableDescriptorBuilder.newBuilder(tn); // 行为事件列族 ColumnFamilyDescriptor eventFamily ColumnFamilyDescriptorBuilder .newBuilder(Bytes.toBytes(cf_event)) .setMaxVersions(1) .setTimeToLive(15552000) // 180 天 .setBloomFilterType(BloomType.ROW) .setCompressionType(Compression.Algorithm.SNAPPY) .build(); // 元信息列族 ColumnFamilyDescriptor metaFamily ColumnFamilyDescriptorBuilder .newBuilder(Bytes.toBytes(cf_meta)) .setMaxVersions(1) .setTimeToLive(15552000) .build(); builder.setColumnFamily(eventFamily); builder.setColumnFamily(metaFamily); // 预分区按 65536 均匀切分 byte[][] splitKeys generateSplitKeys(regionCount); admin.createTable(builder.build(), splitKeys); } } private static byte[][] generateSplitKeys(int regionCount) { int interval 65536 / regionCount; byte[][] splits new byte[regionCount - 1][]; for (int i 0; i regionCount - 1; i) { int value interval * (i 1); splits[i] Bytes.toBytes(String.format(%04d, value)); } return splits; }注意setCompressionType(Compression.Algorithm.SNAPPY)。行为数据以文本和 JSON 为主Snappy 压缩率通常在 30%~50%而且压缩提升的写入速度往往抵消掉 CPU 开销。生产环境不开压缩等于把磁盘 IO 白白浪费在冗余数据上。4.3 批量写入BufferedMutator 的正确姿势写入行为数据最高效的方式不是 Table.put 单线程循环而是使用BufferedMutator做异步批量提交。public class EventWriter { private final BufferedMutator mutator; public EventWriter(Connection conn) throws IOException { this.mutator conn.getBufferedMutator(TableName.valueOf(user_event_behavior)); // 单次 RPC 打包 10MB 数据 this.mutator.getBufferedMutatorParams() .writeBufferSize(10 * 1024 * 1024); } public void write(Event event) throws IOException { String rowkey buildRowkey(event); Put put new Put(Bytes.toBytes(rowkey)); // 行为列族写入业务字段 put.addColumn(Bytes.toBytes(cf_event), Bytes.toBytes(eventType), Bytes.toBytes(event.getEventType())); put.addColumn(Bytes.toBytes(cf_event), Bytes.toBytes(productId), Bytes.toBytes(event.getProductId())); put.addColumn(Bytes.toBytes(cf_event), Bytes.toBytes(duration), Bytes.toBytes(event.getDuration())); // meta 列族写入辅助信息 put.addColumn(Bytes.toBytes(cf_meta), Bytes.toBytes(eventId), Bytes.toBytes(event.getEventId())); mutator.mutate(put); } private String buildRowkey(Event event) { String md5Prefix MD5Util.getMD5Prefix(event.getUserId()); long reverseTime Long.MAX_VALUE - event.getEventTime(); return md5Prefix event.getUserId() reverseTime; } }写入路径有个细节尽量把 Mutation 数量攒到writeBufferSize再 flush。BufferedMutator 内部会按 buffer 大小自动批量发送 RPC每批次几百到几千条网络效率比逐条 Put 高一个数量级。如果直接调mutate后不关心 flush应用退出前要记得mutator.flush()否则内存里积压的数据会丢。生产环境用 Kafka 消费行为日志到 HBase 写入这一步消费线程数控制在 RegionServer 处理能力的 60%~70%给 Flush/Compaction 留出余量。曾踩过大坑消费线程开太多写入洪峰直逼 RegionServer 的 MemStore 上限数据 Flush 到 HDFS 的速度跟不上Block 了写入消费端堆积最后是连环雪崩。4.4 支持“最近行为列表”的前缀级联 Scan读路径最核心的操作是“查某用户最近 N 条行为”。这里要写对 startRow 和 stopRow查错了就是全表扫描。public ListEvent getRecentEvents(String userId, int limit) throws IOException { String prefix MD5Util.getMD5Prefix(userId) userId; byte[] startRow Bytes.toBytes(prefix); // 前缀后加一个永远不会出现的高值字节形成范围上界 byte[] stopRow Bytes.toBytes(prefix \uFFFF); Scan scan new Scan(); scan.withStartRow(startRow); scan.withStopRow(stopRow); scan.setCaching(100); // 每次 RPC 拉取 100 条 scan.setLimit(limit); // 限制返回条数 scan.addFamily(Bytes.toBytes(cf_event)); Table table conn.getTable(TableName.valueOf(user_event_behavior)); try (ResultScanner scanner table.getScanner(scan)) { ListEvent events new ArrayList(); for (Result result : scanner) { events.add(parseResult(result)); if (events.size() limit) break; } return events; } }注意几个点withStartRow和withStopRow在 HBase 2.x 中是含头不含尾的。stopRow 设成前缀 \uFFFF能覆盖该用户下所有数据。setLimit是 HBase 2.0 后新增的接口语义是“每张表最多返回多少行”能有效减少 ResultScanner 空跑。ResultScanner 必须关闭最好放进 try-with-resources。忘了关闭会让 RegionServer 上的 Scan 资源一直占着久了把堆内存拖垮。setCaching(100)控制一次 RPC 从 RegionServer 拉几行到客户端。行为列表场景数据量不大100 是合理值。如果是全表大 Scancaching 反而要调小比如 500~1000 行太大的缓存会导致 RegionServer 一次给你返回海量数据客户端 GC 压力飙升。4.5 复杂分析场景的补充方案协处理器与二级索引行为数据不止按用户查运营分析还会按“事件类型时间段”“商品维度”查。HBase 原生不支持二级索引面对这类查询只能全表 Scan效率没法接受。两个补充方案Phoenix在 HBase 之上封装 SQL 层自动把 SQL 查询转换成 HBase Scan支持创建二级索引Covered Index。团队里有 SQL 背景同学时Phoenix 可以显著降低开发成本。但要注意 Phoenix 的索引表本身也依赖 HBase会有额外写入开销。自建索引表写行为数据时同时写一张“事件类型 日期”为 Rowkey 的统计表存储该维度下的用户 ID 集合。查询时先查索引表拿到 userId 列表再回到主表按前缀 Scan。这和 MySQL 建联合索引的思路一致但完全自主可控不引入额外组件生产实践最稳。5. 运维排坑实录写入热点、Region 分裂与 Scan 超时5.1 写入热点Rowkey 前缀设计不当的真实案例一次线上事故排查记录。某个活动页在晚高峰时写入 TPS 达到了 30 万但 HBase 集群一个 Region 的写入延迟飙升到 5 秒其他 Region 空闲。现象是典型的写入热点。定位方法有三个HBase Master Web UI 里看 Region 的requests计数如果某个 Region 的写请求数远超其他 Region 平均值基本实锤热点。RegionServer 日志里搜Blocking、MemstoreFlushSize相关字样热点 Region 的 MemStore 会频繁触发 blocking flush。客户端埋点看单 Region 分区耗时分布式 Trace 里能直接看到那个 Region 的长尾。排查结果开发同学把 Rowkey 写成了userId timestamp而当时平台对头部用户做了流量扶持头部用户的请求量占到总流量的 40%全部打在同一个 Region 上。修复方案就是前面说的加盐前缀MD5(userId).substring(0,4)。上线后热点消失Region 请求分布均匀。避坑提醒加盐前缀位数不宜超过 4 字节。前缀太长会降低同一用户的 Scan 效率因为每次查询都要拼接并扫描多余字节。4 字节提供 65536 种分布对绝大多数场景足够了。5.2 Region 分裂风暴与预分区之后的问题Region 分裂是 HBase 运维里最琐碎也最影响稳定性的事。分裂时 Region 会短暂下线RPC 重试会放大延迟。我遇到过一次比较极端的场景数据模型从全量快照改为增量追加时没有重新评估预分区导致 3 天内 Region 分裂了 1000 多次分裂日志刷屏RegionServer 频繁因文件句柄超限重启。应对策略新表上量前严格按数据增长量计算预分区。线上表如果实在没预分区用hbase shell的split命令手动触发分裂尽量选择凌晨低峰期。分裂导致 Region 数过多时合并小 Region用merge_region命令。Region 数量不是越多越好Region 管理本身有内存成本单 RegionServer 维护 1000 个 Region 会让堆内存压力很大。5.3 Scan 慢查询缓存命中率与 RPC 压缩的效果行为数据读取慢大多数情况不是 HBase 慢而是 Scan 的设计让 RegionServer 做了大范围扫描。排查思路和优化路径我整理成表现象可能原因优化手段单次 Scan 延迟超过 1sstartRow/stopRow 设置过宽扫描了用户全部历史缩小时间窗口配合 Rowkey 前缀限定范围集群整体吞吐 OK但 P99 高BlockCache 命中率低磁盘随机读多增大 BlockCache 比例开 ROW BloomFilter多个 Scan 并发后延迟恶化每个 Scan 的 caching 太大内存被客户端结果集占满调小 caching批量拉取网络传输量大GC 频繁RPC 未开压缩开启hbase.client.rpc.compress数据量增长后延迟劣化HFile 数量过多Compaction 跟不上调大 Flush 大小检查 Compaction 队列这里重点说一下 RPC 压缩。行为数据在 HBase 里以文本为主压缩率非常高。开启 RPC 压缩后客户端到 RegionServer 的网络传输量能下降 50% 以上。代价是两端各多一次 CPU 压缩/解压。如果你的集群 CPU 有余量强烈建议开启如果 CPU 已经跑满 80% 以上先加机器或关压缩。5.4 一把梭的 BulkLoad大批量初始化的推荐路径从数据仓库或离线日志往 HBase 导历史行为数据如果走 Put API 硬写几千万行要跑几个小时还挤占在线读写资源。正确做法是先写好 HFile再直接 BulkLoad 到 HBase跳过写 WAL 和 MemStore 的过程速度能提升数倍。// 用 MapReduce 或 Spark 生成 HFile Configuration conf HBaseConfiguration.create(); Job job Job.getInstance(conf, GenerateHFile); job.setMapOutputKeyClass(ImmutableBytesWritable.class); job.setMapOutputValueClass(Put.class); HFileOutputFormat2.configureIncrementalLoad(job, conn.getTable(TableName.valueOf(user_event_behavior)), conn.getRegionLocator(TableName.valueOf(user_event_behavior)));生成 HFile 后用LoadIncrementalHFiles工具批量导入hbase org.apache.hadoop.hbase.mapreduce.LoadIncrementalHFiles \ /tmp/hfile_output user_event_behaviorBulkLoad 有两点要提前准备好一是 HFile 的 Key 顺序必须和表的分区边界一致HFileOutputFormat2 会自动按照 SplitKey 生成对应 HFile二是导入前先做一次 Major Compaction不然表里会有大量延迟合并的小 HFile读性能短时间会很差。实测数据单机 BulkLoad 一小时能导 2000 万行是 Put 写入的 4 到 6 倍速非常适合做历史数据迁移、离线回填。5.5 面试会被问的重点HBase 数据模型与读写路径顺带整理几个行为数据场景里常被追问的 HBase 核心问题实际项目中理解这些能少走很多弯路HBase 和 HDFS 的关系HBase 是构建在 HDFS 上的一层分布式 KV 数据库。HDFS 负责最终存储HBase 在它上面实现了随机读写能力本质是靠内存缓冲MemStore 顺序写HLog 定期落盘HFile这套机制。Region 和 RegionServer 怎么对应表被水平切分成多个 RegionRegion 分布在不同 RegionServer 上。Region 是数据分布和负载均衡的最小单位RegionServer 决定了一个表能有多少并发读写在物理上同时发生。写入流程五步Client 找 ZK 获取元数据 → 定位目标 RegionServer → 写 WAL → 写 MemStore → 异步 Flush 成 HFile。这五步里 WAL 保证不丢数据MemStore 保证高吞吐。读取为什么要查 BloomFilterHFile 默认是顺序写的精确点查时不知道目标数据在哪个文件、哪个块。BloomFilter 是每个 HFile 自带的小索引先过滤掉无关文件再用 BlockIndex 定位数据块每一层都在减少磁盘 IO。说实话好多人在 HBase 上栽跟头不是 API 不会写而是对“数据怎么分布在 Region 里、查询怎么命中 Region”心里没底。把上面这几个问题真正想透写起来基本不会跑偏太多。6. 存储方案的整体效果与后续演进的思考空间回到开头那个命题电商海量用户行为数据HBase 究竟解决了什么问题我经历过一次从 MySQL 分库分表迁移到 HBase 的过程效果非常直观。同样 50 亿行的用户行为数据MySQL 那套方案晚高峰写入 P99 已经到 3 秒磁盘接近写满扩容要动一堆分片。迁移到 HBase 后写入 P99 稳定在 10ms 内存储空间因为 Snappy 压缩直接缩了 60%扩存储就是加节点一个下午的事。但这个方案不是银弹。HBase 在行为数据上的劣势也很明确不支持事务性更新不能做复杂聚合。如果业务场景既要“存行为明细”又要“行为发生后实时修改状态”HBase 就不如 MySQL 或 Redis 好使。所以我的习惯是行为数据全部进 HBase业务状态数据留 MySQL两边靠 Kafka 异步同步各取所长。个人踩过几次坑之后最深的体会是HBase 项目成功与否七分靠设计两分靠运维一分靠代码。Rowkey 和预分区设计在开发阶段多花一天想清楚能少写一个月的问题排查文档。行为数据这个场景本身足够简单纯粹把写入路径打散、读路径固定、生命周期管好这套方案能扛住绝大多数电商平台体量。后续如果业务上来了还可以把 HBase 和 Elasticsearch、ClickHouse 做分层HBase 留最热的明细ES 做检索CK 做分析但那是另一个话题了。
返回列表