资讯动态

HBase数据模型与读写原理:RowKey、列族和LSM树实战解析

发布时间:2026/10/27 13:20:00 来源:尧图企业网站定制
1. 为什么单独把 HBase 拎出来聊第一次接触 HBase 是在一个日志检索类项目里当时底层还挂着单机 MySQL单表刚过八千万行带索引的查询开始从毫秒级退化到秒级加机器、加缓存都只是暂时按压。后来把这份明细数据整体搬到 HBase 上同样一条按时间范围加维度前缀的查询从秒级回到了几十毫秒。这件事给我的冲击不在于换个数据库快了多少而在于我意识到大数据场景下的存储选型本质上不是比谁更快而是比谁的数据结构更贴合访问模式。HBase 是什么一句话概括它是一个建立在 HDFS 之上的、分布式的、面向列的、支持海量稀疏数据随机读写的 NoSQL 数据库。它脱胎于 Google 那篇讲 BigTable 的论文最早是为了解决单表几十亿行、几百万列、还要能随机查这个关系型数据库天生接不住的问题。它能做三件传统库很吃力的事按 RowKey 做毫秒级随机读写、按列族做物理隔离存储、数据量从 GB 平滑长到 PB 而不改代码。它接不住的事也很明确复杂多表 JOIN、跨行事务、灵活的二级索引、即席聚合分析这些都不是它的菜。这篇内容适合谁看如果你是刚入行做数据平台、第一次被安排去对接一下 HBase的后端同学这篇能帮你把概念地图搭起来如果你已经在用 HBase但对为什么写入快、读取慢为什么列族不能建太多为什么 RowKey 设计决定生死这些问题一直停留在背诵结论的层面这篇会把背后的机制掰开给你看。我不打算写成官方文档的中文翻译而是按一个实际用过它、也被它坑过的人的视角把概念、原理、操作和踩坑一步步串起来。1.1 从一张千万级明细表说起关系型数据库到底卡在哪先别急着背 HBase 的术语我们从一个具体场景倒推。假设你要存一份用户行为明细每天新增五千万条要保留一年那就是一百八十亿行。每条记录大概二十个字段但不同行为类型用到的字段完全不同——点击行为有页面 ID 和停留时长下单行为有订单号和金额分享行为有渠道标识。如果用 MySQL 建表你面临三个绕不过去的坎。第一是稀疏字段的存储浪费。关系型表是行式存储一整行连续放在一起那些为 NULL 的列照样占位至少占位标记。一百八十亿行里如果平均有六成字段是空的浪费的存储和 IO 是惊人的。第二是写入吞吐。MySQL 的写入要维护 B 树索引随机写多的时候页分裂频繁单表写到一定规模后写入延迟会明显抖动。第三是水平扩展的代价。分库分表要改应用代码要处理跨分片查询扩容时数据迁移是个大工程业务方得配合停写。HBase 的设计恰好是从这三个痛点反推出来的。它按列族存储同一个列族的数据物理上放在一起空字段根本不占空间它把随机写转成顺序追加靠 LSM 树把写性能拉平它的数据天然按 RowKey 范围切成 Region 分散到不同机器上扩容只需要加 RegionServer数据会自动迁移。这三条不是孤立的功能而是同一套设计哲学的三个侧面——用写放大和读放大换取写的吞吐和扩展的线性度。理解了这个取舍后面所有概念都会顺起来。你会明白为什么 HBase 的写入吞吐能到单机每秒几万甚至十几万条也会明白为什么一次不带 RowKey 前缀的 Scan 会让你等到怀疑人生。1.2 HBase 的定位不是替代 MySQL而是接住它接不住的那部分经常有人问我上了 HBase 是不是就能把 MySQL 换掉。我的答案从来都是不能也不该。这两个库的定位差异太大了拿它们互相替代最后一定两头不讨好。我把它们的适用边界整理成一张表实际做选型的时候直接对着看维度关系型数据库HBase数据规模单表千万到亿级较舒适十亿到百亿级起步才划算访问模式任意条件组合查询、JOIN必须走 RowKey 或 RowKey 前缀事务能力完整的 ACID单行原子跨行事务基本没有二级索引原生支持没有需要自己建索引表聚合分析支持但大数据量慢不适合交给计算引擎写入特征随机写量中等高吞吐顺序写量大扩展方式垂直加配置为主水平加机器近线性这张表背后有一条很实用的判断标准如果你的查询条件在写入时就能确定、并且能编码进主键那 HBase 是好选择如果查询条件千变万化、需要用户自由组合那 HBase 会让你生不如死。举个具体例子做订单明细存储查询永远是某个用户在某段时间的订单这个模式天然适合把用户 ID 和时间拼进 RowKey但如果你要做一个后台运营系统运营同学随便勾选十几个条件做筛选那还是老实用关系型库加查询引擎。注意不要把 HBase 当成更快的 MySQL来用。我见过有团队把所有业务表一股脑迁到 HBase结果每个列表页都要全表扫最后又灰溜溜迁回去中间浪费了两个月。2. HBase 的数据模型把表这个概念重新拆一遍关系型数据库里的表是一个很直觉的东西行是记录列是字段行列交叉处是值。HBase 也叫表但它的表和前者几乎只有名字相同。刚上手的人最容易犯的错就是拿关系型的思维去理解 HBase 的列然后在一个列族里塞几十个字段最后发现读写性能莫名其妙变差。要真正理解 HBase 的数据模型你得先接受一个前提HBase 的物理存储单元不是行而是列族。这个前提一换整个模型的逻辑就自洽了。下面我按从大到小、从粗到细的顺序把这个模型拆开讲。2.1 表、行、列族最小存储单元其实是列族HBase 的数据组织层级是 命名空间Namespace→ 表Table→ 行Row→ 列族Column Family→ 列限定符Column Qualifier→ 单元格Cell。前三层好理解命名空间相当于库表就是表行就是一条记录。真正的分水岭在第四层。列族是建表时必须显式声明的而且一旦建好修改的代价非常大。为什么因为一个列族在物理上对应一个 Store而一个 Store 对应一组 HFile 文件。同一个列族下的所有数据不管列限定符是什么都会被写进同一批 HFile 里。换句话说列族是 HBase 里数据放在一起的最小单位。你查一行数据时如果只涉及一个列族HBase 只需要打开这个列族的文件如果涉及三个列族就要打开三组文件。这就是为什么列族数量直接决定读性能。那列族该按什么维度划分核心原则是按访问模式分不按业务含义分。举个实际例子存一份设备上报数据字段有设备 ID、上报时间、温度、湿度、电量、固件版本、信号强度。如果你按基本信息和运行指标分成两个列族查询时经常要同时取那分成两个列族就没意义反而多一次文件打开。但如果你发现固件版本、信号强度这类字段只在排查问题时偶尔查一次而温度湿度是每次都要拿的那把这两类分到两个列族就很划算——日常查询只碰一个列族IO 量能砍掉一大截。我的经验值是列族数量控制在 1 到 3 个超过 3 个就要有非常明确的理由。官方文档也提过类似建议因为列族多了会带来 flush 风暴、compaction 放大、内存占用上升等一系列连锁问题。有个项目我接手时表里有 7 个列族每次 minor compaction 都抖动合并到 2 个之后写入延迟曲线肉眼可见地平了下来。2.2 列限定符与稀疏性空值不占空间这件事有多重要列限定符Column Qualifier是列族下面的具体字段比如info:name、info:age里的name和age。它和关系型的列有一个本质区别不需要在建表时定义写入时直接给用多少有多少。这个特性带来的直接好处是 schema 极度灵活同一张表里不同行可以有完全不同的字段集合。但真正值钱的不是灵活而是稀疏性带来的存储效率。HBase 底层存的是 KeyValue 结构每一条记录都是行键 列族 列限定符 时间戳 值这样一个完整的键值对只存实际写了的没写的字段压根不存在。这就好比你家衣柜不是先买一百个格子再把空着的格子留着落灰而是有多少件衣服挂多少个钩子。拿前面那个设备上报场景算一笔账假设每个设备有 50 个可能字段但实际每次上报平均只填 8 个。用关系型表你要为 50 个字段预留位置利用率 16%用 HBase只存那 8 个利用率 100%。数据量上到百亿级别这个差距就是能不能扛住的分水岭。不过这里有个反直觉的坑列限定符本身也占存储。因为每一条 KeyValue 都要把列限定符的字节完整带上如果你的列名起得又长又啰嗦比如device_temperature_celsius_value在百亿行的量级下光列名就能吃掉几十 GB。所以我的习惯是列名尽量短temp、hum、vol这种缩写能看懂就行别搞自解释长名。这个细节你在任何教程里都很少看到但它是真金白银的存储成本。2.3 时间戳与多版本一行数据为什么可以有多个值HBase 的每个单元格不是单值而是多版本。你往info:temp写一个值HBase 会自动带上一个时间戳默认是 RegionServer 的当前毫秒时间同一个 RowKey、同一个列限定符下可以有多个不同时间戳的值共存。读取的时候默认返回最新版本也可以指定读几个版本或者读某个时间范围。这个设计最初是为了解决什么解决覆盖即丢失的问题。关系型数据库 UPDATE 一条记录旧值就没了HBase 的写入本质上是追加一条新版本旧版本还在只是默认不返回。这在很多场景下很有用比如做配置变更审计你想知道某个参数历史上被改过几次、每次是什么值比如做数据修复误写了一个错误值可以按时间戳回查上一个正确版本。版本数量由列族级别的VERSIONS参数控制默认是 1。这里有个很多新手会踩的坑默认 1 意味着你看不到历史版本但历史版本并不会立刻消失。它要等到 compaction 的时候才会被清理掉。所以如果某个列族的写入非常频繁而你又没调整VERSIONS和 TTL那些看不见的旧版本会一直占着存储直到 compaction 才发现然后引发一次大规模的 IO 抖动。我的建议是如果确定不需要多版本把VERSIONS显式设成 1 并配一个合理的TTL让过期数据尽早被标记删除减少 compaction 的压力。设置方式很直接建表时指定或者后期改# 建表时指定版本数和 TTL单位秒 create device_metrics, {NAME info, VERSIONS 1, TTL 7776000} # 后期修改列族属性 alter device_metrics, {NAME info, VERSIONS 3}TTL 7776000是 90 天。这个参数我一般会强制要求业务方给出明确的数据保留期因为先留着以后可能有用这种想法在 HBase 里代价极高。2.4 RowKey唯一索引也是性能命门RowKey 是 HBase 里唯一的主键行按 RowKey 的字典序排列这个排序不是可选项是物理存储顺序。所有关于 HBase 性能的讨论最终都会收敛到 RowKey 设计上因为它是你唯一能控制的、决定数据落在哪台机器哪个位置的抓手。RowKey 设计的核心目标有两个查询要能命中前缀、写入要能打散热点。这两个目标经常打架需要根据实际负载做权衡。下面几种模式是我在实际项目里用得最多的设计模式做法适用场景代价时间倒序拼接userId (Long.MAX - timestamp)查询某用户最近 N 条单用户内有序跨用户仍可能热点哈希前缀hash(userId) % 16 userId ts写入量大、热点明显范围扫描能力下降盐值前缀(0-9随机) 原始key时序数据写入打散读取要扫 10 个前缀反转字段把手机号倒序写入尾号相同的查询只适合特定等值查询我具体说说哈希前缀这个模式为什么实用。假设你有一批高活跃设备每秒上报一次如果 RowKey 直接用设备 ID 开头那活跃设备的写入会全部压到同一台 RegionServer 上其他机器闲得发慌这就是典型的写入热点。加一个两位的哈希前缀后同一个设备的写入会被分散到多个 Region压力就摊平了。代价是查单个设备的历史数据时要扫多个前缀如果每次查的数据量不大这个代价可以接受。提示RowKey 长度建议控制在 10 到 100 字节之间。太短了容易冲突太长了存储开销大——别忘了 RowKey 在每条 KeyValue 里都会重复出现它和列族名、列名一样是重复成本最高的部分。还有一个容易被忽略的点RowKey 里不要放会变化的东西。HBase 没有 UPDATE 主键这回事你改了 RowKey 就等于删一条加一条而且还得自己保证两条都处理干净。所以设计时就要想清楚这条数据的天然主键到底是什么。3. 架构拆解一个请求在集群里到底走了哪些机器理解了数据模型下一步要知道这些数据被放在哪些机器上、由谁管。HBase 的架构图看着挺复杂但其实角色就那么几个而且分工非常干脆一个负责调度和元数据一堆负责真正干活一个外部组件负责介绍人,一个底层负责存文件。我把每个角色的职责和它在实际运维中的存在感列一下组件核心职责出问题时表现HMaster建表改表、Region 分配、负载均衡、故障转移元数据操作失败但读写可能照常RegionServer承载 Region处理读写请求管理 WAL 和 MemStore部分数据不可读Region 被重新分配Region按 RowKey 范围切分的数据分片单个 Region 过大导致热点和分裂ZooKeeper维护集群状态、meta 表位置、Master 选举整个集群无法定位数据等于瘫痪HDFS持久化存储 WAL 和 HFile写入失败数据面临风险3.1 HMaster 与 RegionServer 的分工HMaster 是个很容易被误解的角色。很多人以为它和 MySQL 的主节点一样所有请求都要经过它。实际上 HMaster 完全不参与数据读写它的工作更像物业公司负责登记、分配、协调、处理报修但不管住户家里的日常起居。具体来说HMaster 干这些事建表、删表、改表结构把新表切出来的 Region 分配到各个 RegionServer监控 RegionServer 的存活状态某个 RegionServer 挂了之后把它上面的 Region 重新分配给其他机器定期做负载均衡把 Region 从忙的机器挪到闲的机器以及处理 Region 分裂和合并的协调工作。RegionServer 才是干活的。一台 RegionServer 上会挂几十到几百个 Region每个 Region 负责一段 RowKey 范围。RegionServer 内部维护几个关键结构一个 WAL预写日志保证数据不丢一个 BlockCache读缓存以及每个 Region 对应的一个 MemStore写缓存。读写请求打到 RegionServer它在这些结构之间穿梭最终把结果返回。这个分工带来的一个重要好处是HMaster 的短暂不可用不会影响在线读写。我做运维的时候有过一次 HMaster 因为 GC 卡住两分钟的情况期间所有读写请求完全正常业务方毫无感知。但如果 ZooKeeper 出问题那就是另一个故事了。3.2 Region 与分区数据是怎么被切开的Region 是 HBase 水平扩展的基本单位。一张表在逻辑上是一整张但物理上被按 RowKey 范围切成很多段每段就是一个 Region。比如一张表有 10 亿行RowKey 从a000到z999切成 16 个 Region第一个负责a000到某个中间值第二个接着往下最后把所有机器的存储和计算压力摊开。Region 的大小有一个阈值历史版本默认 10GB较新的版本调整到了更小的值。当一个 Region 长到超过阈值它会分裂成两个。分裂这个动作本身不涉及数据拷贝只是元数据上标记一下父 Region 下线两个子 Region 上线各自读取原来文件的一部分。这里有个实操层面的关键点建表时最好做预分区Pre-splitting。如果建表时不切默认只有一个 Region所有写入全压在一台机器上直到这个 Region 长到阈值才分裂。而在数据量快速增长的前期一次分裂往往要等很久这段时间的写入瓶颈是实打实的。预分区的做法是在建表时直接指定切分点# 按十六进制前缀预分区成 16 个 Region create device_metrics, info, {SPLITS [1,2,3,4,5,6,7,8,9,a,b,c,d,e,f]}配合前面说的哈希前缀 RowKey预分区才能真正发挥作用。如果 RowKey 是纯粹的设备 ID 递增预分区反而没用因为数据还是会往最后一个 Region 挤。3.3 ZooKeeper 在中间扮演的角色ZooKeeper 在 HBase 里的作用可以类比成通讯录加仲裁庭。客户端要读一条数据它不知道数据在哪台机器上得先查通讯录集群里 HMaster 挂了要选新的得靠仲裁庭投票。具体职责有这么几条维护hbase:meta表的位置这张表记录了所有用户表的 Region 分布是 HBase 的总目录维护集群中所有 RegionServer 的在线列表做 HMaster 的选举和故障切换以及存放一些分布式锁和状态标记。为什么非要用 ZooKeeper因为 HBase 需要的是一个强一致的、能感知节点存活的协调服务。RegionServer 要定期往 ZooKeeper 写心跳超过一定时间没写ZooKeeper 就认为它挂了通知 HMaster 去做故障转移。这套机制如果自己实现工作量巨大且容易出 bug直接用一个成熟的协调组件是更务实的选择。注意ZooKeeper 是 HBase 的单点依赖。生产环境至少要部署三个节点组成 quorum并且放在独立的机器上不要和 RegionServer 混部。我见过把 ZooKeeper 和 RegionServer 部署在同一台机器上的配置结果 RegionServer 一次 Full GC 把 ZooKeeper 的心跳也卡住了集群误判节点死亡触发一轮不必要的 Region 迁移雪崩就这么来的。3.4 HDFS 作为底座为什么数据最终落到 HDFS 上HBase 自己不负责数据的持久化存储它把所有文件都扔给 HDFS 管。这个设计很像我专心做数据库逻辑硬盘的事交给专业的人。WAL 写在 HDFS 上HFile 也写在 HDFS 上HBase 只负责决定什么时候写、写到哪个路径、什么时候合并。好处有两层。第一层是副本机制白捡。HDFS 默认三副本HBase 天然获得了数据冗余某台机器硬盘坏了不影响数据可用性。第二层是计算存储分离的雏形。RegionServer 可以部署在和数据节点同一批机器上追求本地性也可以分离部署追求弹性数据本身跟计算节点的生命周期解耦。代价是写入路径变长了。一次写要先过 HBase 的 WAL、MemStoreflush 的时候再走 HDFS 的写管线三个副本依次确认。所以 HBase 的写入延迟下限比本地盘数据库要高通常在几毫秒到十几毫秒这个区间。如果你的业务要求亚毫秒写入HBase 不是合适的选项。理解这层关系还有一个实际用途排查问题时要看 HDFS 的脸色。如果 HDFS 集群在做 balancer、某个 DataNode 慢、或者磁盘写满HBase 的写入延迟会跟着抖。我排查过好几次HBase 变慢的问题最后定位到的都是底层 HDFS 的某个节点磁盘 IO 打满。4. 读写流程与 LSM 树写入为什么快读取为什么慢这一节是我认为 HBase 最值得花时间理解的部分。很多人能背出先写 WAL 再写 MemStoreflush 成 HFile这个流程但问到为什么这么设计、为什么写入快读取慢就答不上来了。理解了这个取舍你在做容量规划和性能调优时会有完全不同的判断力。核心就一句话HBase 用 LSM 树Log-Structured Merge Tree把随机写转化成了顺序写代价是读取时要合并多个来源。4.1 写路径WAL → MemStore → HFile一次 Put 请求进来RegionServer 的处理顺序是这样的第一步把这条记录追加写到 WAL 里。WAL 是顺序追加的文件写在 HDFS 上开启WAL的目的是万一 RegionServer 在数据落盘前挂掉重启后可以重放日志恢复。这里用的是追加而不是随机修改所以速度很快。第二步把记录写进对应 Region 的 MemStore。MemStore 是内存里的有序结构跳表数据写进去会自动按 RowKey 排序。这一步纯粹是内存操作。第三步返回客户端写入成功。注意这时候数据还在内存里没落到 HFile。第四步异步发生当 MemStore 的大小超过阈值默认 128MB可以调会触发一次 flush把内存里这批有序数据一次性写成 HFile。因为是整块顺序写速度远高于一条条随机写。这个流程解释了 HBase 写入快的原因每一次写入对磁盘来说都是一次顺序追加没有 B 树的页分裂没有随机寻道。实测下来单台配置中等的机器HBase 的写入吞吐能到每秒几万条比同等硬件上的关系型库高一个数量级。代价是什么内存风险和数据延迟可见性。MemStore 里的数据还没落盘如果 RegionServer 此刻宕机虽然 WAL 能恢复但恢复过程要重放日志时间可能不短。而且刚写进去的数据在内存里读取时要额外查一次 MemStore这条链路就比直接从磁盘读复杂。!-- hbase-site.xml 中几个与写路径相关的关键参数 -- property namehbase.regionserver.global.memstore.size/name value0.4/value /property property namehbase.hregion.memstore.flush.size/name value134217728/value /property property namehbase.hregion.memstore.block.multiplier/name value4/value /propertyglobal.memstore.size是 0.4 意味着整个 RegionServer 堆内存的 40% 给所有 MemStore 用。这个值不是越大越好因为堆内存还要留给 BlockCache 和其他运行时结构。调太大容易在 flush 的时候触发长时间停顿调太小会导致频繁 flush产生大量小文件。我一般建议读写均衡的场景按 0.4 对 0.4 分配读多写少的场景把 BlockCache 调大一些。4.2 Flush 与 Compaction把随机写攒成顺序写MemStore flush 出来的 HFile 是不可变的。这点很关键HBase 从来不修改已经写好的 HFile新的写入永远产生新文件。这就带来一个问题——数据越写越多文件越来越多读一次数据可能要翻几十个文件。解决办法就是 Compaction合并。Compaction 分两种。Minor Compaction 挑几个小的、相邻的 HFile 合并成一个稍大的清理掉已经标记删除的数据和过期版本。Major Compaction 把某个 Region 下某个列族的所有HFile 合并成一个彻底清理垃圾数据。Major 的开销极大因为它要重写整个列族的数据期间磁盘 IO 和网络带宽都会被吃满。默认策略是 Minor 频繁跑、Major 定期跑历史上是 7 天但很多生产环境会关闭自动 Major改到低峰期手动触发。这里有个非常实用的经验Compaction 是 HBase 运维里最主要的不确定性来源。写入放大、读取抖动、超时告警追根溯源经常能追到一次意料之外的 compaction。控制它的手段有这么几个手段具体做法解决的问题控制 flush 频率适当调大 memstore flush size减少小文件数量限制 compaction 并发调整hbase.regionserver.thread.compaction.large避免 compaction 抢占 IO关闭自动 Major设hbase.hregion.majorcompaction为 0改用脚本定时触发避免高峰期突然大合并控制单 Region 大小及时 split避免单个 Region 过大减少单次 compaction 的数据量合理设 TTL让过期数据尽早被清理减少无效数据的合并开销我踩过最疼的一个坑是某张表写入量突然翻了三倍MemStore 频繁 flush 产生大量小 HFileMinor Compaction 跟不上文件数量一路涨到上千读取延迟从 20ms 飙到 2 秒。当时的处置是先把写入限流然后手动触发了几轮 Compaction 把文件数压下去。事后复盘根本原因是这张表没设 TTL历史数据无限累积白白增加了 compaction 的负担。4.3 读路径BlockCache、MemStore、HFile 三层查找读一条数据HBase 要找三个地方BlockCache读缓存、MemStore写缓存、HFile磁盘文件。这三层任何一层有数据都要参与合并最终返回时间戳最新的版本。这就是读放大的来源——一次 Get 可能要看三个位置的数据。定位到具体 Region 之后读取的顺序是这样的先查 BlockCache这是 RegionServer 堆内存里的一块区域缓存最近读过的数据块。命中就直接返回速度最快。这块内存的大小由hfile.block.cache.size控制读多写少的场景可以给到 0.4 甚至更高。再查 MemStore也就是还没 flush 到磁盘的那批数据。这是为什么刚写进去的数据能立刻读到的原因。最后查 HFile。HFile 内部有三级结构帮助快速定位布隆过滤器先判断这个 RowKey 是不是可能在这个文件里不存在就直接跳过省一次 IO索引块定位到具体的数据块位置然后读数据块。这里布隆过滤器的价值巨大它能过滤掉大量根本不包含目标 RowKey 的文件把翻 50 个文件降到翻 2 个文件。# 查看某张表各列族的布隆过滤器配置 describe device_metrics # 建表时显式开启行级布隆过滤器适合 Get 多的场景 create device_metrics, {NAME info, BLOOMFILTER ROW}布隆过滤器有三个取值NONE、ROW、ROWCOL。ROW是按行键过滤适合按 RowKey 做 Get 或小范围 ScanROWCOL是按行键加列限定符过滤适合宽表场景下只查少数几列。默认是ROW多数场景够用但如果你的表列特别多、每次只查其中几列改成ROWCOL能进一步减少读取的数据块。提示顺序读和随机读在 HBase 里的表现差异极大。如果你要做全表扫描做离线分析不要用 Scan 一个劲读那样会打爆 RegionServer 的 IO。正确做法是用计算引擎比如 Spark配合快照读或者直接读 HFile 底层的文件。5. 动手跑一遍从环境到常用命令概念讲再多不动手跑一遍都是空的。这一节我按能跑起来、能查数据、能看懂结果的顺序走一遍顺便把几个关键配置和命令的用法说清楚。我假设你手上有一台能装软件的机器单机模式足够验证概念不涉及多机部署的细节。5.1 环境准备与配置要点跑单机版 HBase 需要三样东西JDK、Hadoop或者直接用本地文件系统、HBase 自身。验证概念的话用本地文件系统就够了配置hbase.rootdir指向本地目录即可省掉 HDFS 的一堆配置。!-- conf/hbase-site.xml 单机模式最小配置 -- configuration property namehbase.rootdir/name valuefile:///data/hbase/value /property property namehbase.zookeeper.property.dataDir/name value/data/zookeeper/value /property property namehbase.cluster.distributed/name valuefalse/value /property property namehbase.unsafe.stream.capability.enforce/name valuefalse/value /property /configuration倒数第二个参数cluster.distributed设成 false表示单机模式HBase 会自己起一个内嵌的 ZooKeeper。最后一个参数在新版本里经常需要显式设成 false否则在某些文件系统上启动会报错。这两个是新手最容易卡住的地方网上大量启动失败的提问都跟它们有关。启动命令很简单bin/start-hbase.sh然后用bin/hbase shell进入交互界面。如果 shell 能正常打开并显示版本号说明服务起来了。启动日志在logs/目录下如果起不来第一件事就是去看hbase-*-master-*.log里的 ERROR。注意单机模式下所有数据都存在本地磁盘机器一换数据就没了别拿它当生产环境用。真正部署的时候hbase.rootdir要指向 HDFS 路径cluster.distributed要设为 true还要配好hbase.zookeeper.quorum指向独立的 ZooKeeper 集群。5.2 建表、写入、查询、扫描的完整操作进到 shell 之后最常用的命令就那么几个。我按一次完整的操作流程串一遍每条命令都说明它在干什么。# 建表表名 device_metrics两个列族 info 和 metric hbase(main):001:0 create device_metrics, info, metric # 看一下表结构重点看 VERSIONS、TTL、BLOOMFILTER 这几项 hbase(main):002:0 describe device_metrics # 插入一条数据四个参数分别是表名、RowKey、列、值 hbase(main):003:0 put device_metrics, dev001_20240115080000, info:model, A100 hbase(main):004:0 put device_metrics, dev001_20240115080000, metric:temp, 36.5 hbase(main):005:0 put device_metrics, dev001_20240115080000, metric:hum, 62 # 取一行能看到所有列族下的所有列 hbase(main):006:0 get device_metrics, dev001_20240115080000 # 只取指定列族减少 IO hbase(main):007:0 get device_metrics, dev001_20240115080000, info # 只取指定列 hbase(main):008:0 get device_metrics, dev001_20240115080000, {COLUMN metric:temp} # 范围扫描STARTROW 和 STOPROW 是左闭右开 hbase(main):009:0 scan device_metrics, {STARTROW dev001_20240115, STOPROW dev001_20240116} # 限制返回条数调试时一定要加否则可能卡死 hbase(main):010:0 scan device_metrics, {LIMIT 10}这里有几个实操细节值得单独说。第一scan不加LIMIT是新手最容易犯的错。一张上亿行的表你敲个scan table回车shell 会一直往下吐轻则卡住终端重则把 RegionServer 的读缓存冲垮影响线上。我现在的习惯是任何 scan 命令都先带LIMIT和STARTROW、STOPROW。第二RowKey 的字典序决定了范围扫描的边界。上面例子里的 RowKey 是设备ID_时间戳拼接同一个设备的数据会连续排在一起所以按dev001_20240115到dev001_20240116扫就能拿到这一天的所有数据。这就是为什么前面反复强调 RowKey 设计——你的查询模式必须能翻译成 RowKey 的前缀否则 scan 就只能全表扫。第三get和scan的定位方式不同。get是精确找一行走的是点查路径scan是范围扫会顺序读多个 Region。前者适合按 ID 查详情后者适合按时间范围拉列表别混用。还有一些管理类的命令日常也会用到# 统计行数大数据量下慎用会触发全表扫 hbase(main):011:0 count device_metrics # 清空表数据但保留表结构 hbase(main):012:0 truncate device_metrics # 下线表才能删除 hbase(main):013:0 disable device_metrics hbase(main):014:0 drop device_metrics # 查看所有表 hbase(main):015:0 listcount这个命令我要特别提醒一句它在大表上非常慢因为它本质是一次全表扫描。要快速估算行数可以用hbase org.apache.hadoop.hbase.mapreduce.RowCounter走 MapReduce或者直接看 Region 的元数据统计。我在生产环境上见过有人为了确认数据量敲了个 count结果跑了一个小时还没结束最后只能 kill 掉。5.3 表结构设计的几个原则命令会了之后真正决定成败的是设计。我把这些年总结的原则列出来每条都对应具体的后果。RowKey 必须服务于查询。这是第一原则没有例外。设计之前先问清楚业务有哪几种查询每种查询的条件能不能拼成前缀如果有个查询条件永远是某城市某类型的最新 N 条那 RowKey 就应该是城市_类型_倒序时间如果某个查询是某用户的全部记录那就是用户ID_时间。一个表服务多种查询模式时通常需要建多张表或者建索引表别指望一张表通吃。列族按访问频率分不按业务含义分。前面说过了这里再强调一次。判断标准很简单这两组字段是不是总是一起读是就合并不是就分开。控制单行的大小。一行数据如果超过几十 MB会带来一系列问题写入时 MemStore 里的单条记录过大flush 时的粒度变粗读取时一次要加载大量数据块缓存效率下降。我一般建议单行控制在 10MB 以内如果业务上确实需要存大字段比如图片、长文本把大字段单独放一个列族或者干脆存到对象存储里HBase 只存引用。提前规划 Region 数量和增长节奏。建表时预分区配合哈希前缀的 RowKey让写入从一开始就摊开。同时要估算数据增长速率如果三个月内数据量会翻十倍那预分区的数量要按那个量级来设计别等到热点出现了再改改 RowKey 意味着数据重写。6. 常见问题与排查实录HBase 这玩意跑起来容易跑稳不容易。下面这些问题都是我在实际项目里真真切切遇到过的按现象、原因、处置三列整理成速查表遇到类似情况可以直接对着找。6.1 常见问题速查表现象可能原因排查与处置写入延迟突然飙升MemStore 满了触发阻塞或 HDFS 写入慢看 RegionServer 日志有无Blocking updates检查 HDFS 磁盘 IO读取延迟高Get 要几百毫秒HFile 数量过多布隆过滤器未生效看 Region 的 storefile 数量必要时手动 compact单个 RegionServer 负载明显偏高RowKey 热点数据倾斜检查 Region 分布考虑加盐前缀或预分区Region 频繁分裂单 Region 数据增长过快提前预分区或调整分裂策略客户端报RegionTooBusyException单 Region 写入过快MemStore 打满降低写入并发或扩大预分区报ClockOutOfSyncException集群节点时间不同步配置时间同步服务检查各节点时间差RegionServer 频繁掉线内存不足触发长 GC或 ZooKeeper 会话超时看 GC 日志调整堆大小和 ZK 超时时间表数据查不到但 HDFS 有文件meta 表信息不一致出现孤儿 Region用管理工具做一致性检查谨慎修复写入成功但读不到读的版本数配置为 1读到的是旧版本检查 VERSIONS 设置和时间戳这张表里我想单独展开两个最典型的场景因为它们的排查路径比较绕光看表不一定能对上号。6.2 两个踩过的坑热点与 compaction 风暴第一个坑是写入热点。有张表存的是某类事件流水RowKey 直接用的事件发生时间戳倒序。上线第一天没问题第二天开始监控上出现一个很奇怪的现象集群有 8 台 RegionServer7 台 CPU 在 10% 以下1 台常年 90% 以上写入延迟从 5ms 涨到 200ms。当时的直觉是数据倾斜但看 Region 数量是均匀的每台机器上的 Region 个数差不多。真正的原因藏在 RowKey 的分布里。因为 RowKey 以时间戳开头而所有写入都是当前时刻的数据所以新数据永远落在 RowKey 最大的那个 Region 上其他的 Region 都是历史数据几乎不写入。Region 数量均匀但写入压力完全集中在一个点上。解决办法是给 RowKey 加哈希前缀。具体做法是取业务 ID 的哈希值模 16转成两位十六进制作为前缀再把原来的时间戳拼在后面。改完之后写入被打散到 16 个前缀区间配合建表时的预分区压力立刻摊平。代价是查询某一条具体记录时需要先算出前缀才能定位如果要做全量扫描也得扫 16 个区间。这个代价在当时的场景下完全可以接受因为查询都是按 ID 点查前缀是确定的。第二个坑是 compaction 风暴。现象是每天凌晨两点到四点集群整体负载飙升读写延迟全线飘红白天又恢复正常。查监控发现这几段时间的磁盘 IO 和网络流量是白天的三倍日志里全是 compaction 相关的记录。根因是自动 Major Compaction 的触发时机不受控。默认策略下HBase 会在某个时间窗口内随机触发 Major而这张表数据量大、列族多一次 Major 要重写几百 GB 数据。更糟的是多个 Region 的 Major 可能撞在一起形成叠加效应。处置方案分三步先把自动 Major 关掉hbase.hregion.majorcompaction设为 0改成用脚本在业务低峰期分批触发然后调整 Minor Compaction 的线程数限制它的并发最后给这张表加上了 TTL把半年前的历史数据清掉压缩了需要合并的数据总量。三板斧下去凌晨的负载尖峰基本消失了。提示Compaction 相关的调优要非常谨慎因为它直接关系到读性能和磁盘占用。改参数之前先在预发环境压测一轮观察 HFile 数量、读延迟、磁盘使用率三个指标的变化别在生产环境上直接试。6.3 几个只有踩过才知道的细节说几个文档里不太提、但实际会咬人的点。列族名和列名不要太长。前面提过一次这里再算笔账。假设一张表有一百亿条 KeyValue列族名平均 10 字节、列名平均 12 字节光这两项就是 220GB 的额外存储。改成 2 字节和 4 字节能省下 160GB。这在存储成本上是实打实的钱。不要在 shell 里做批量操作。用 shell 循环执行几千条 put速度慢到令人绝望因为每条命令都有一次网络往返。批量导入要用hbase org.apache.hadoop.hbase.mapreduce.ImportTsv或者写程序用 BufferedMutator 批量提交后者能把几千条攒成一次 RPC。RegionServer 的堆内存不要超过 32GB。这是 JVM 的一个经验界限超过之后对象指针压缩失效内存占用会明显膨胀。而且堆越大Full GC 的停顿越长RegionServer 一旦停顿超过 ZooKeeper 的会话超时就会被误判为死亡触发 Region 迁移。宁可多部署几台小内存的机器也别搞一台超大内存的。写数据前先确认列族存在。往一个不存在的列族里写数据HBase 会直接抛异常而不是像关系型数据库那样自动加列。这个错误在开发阶段很常见尤其是改表结构忘了同步到所有环境的时候。TTL 的单位是秒不是天。这个坑我踩过不止一次。设TTL 90本意是保留 90 天结果 90 秒后数据就没了。正确的写法是90 * 24 * 3600或者直接写7776000。HBase 的删除是标记删除。你执行delete或deleteall数据不会立刻消失只是打上一个删除标记等 compaction 的时候才真正清理。这意味着两件事一是删除操作本身很快二是删除之后磁盘空间不会立刻释放。如果你的目的是快速释放空间要给表设 TTL 并等待 compaction或者用truncate整表清空。客户端要配重试和超时。默认的重试次数和超时时间在跨机房或者网络抖动的环境下往往不够用。我一般会把 RPC 超时设到 30 秒以上重试次数设到 5 次以上同时开启客户端侧的区域缓存减少每次请求都要查 meta 表的开销。这几个参数配好之后偶发的网络抖动基本不会传导成业务报错。我在实际使用中最大的体会是HBase 的难不在概念而在它把很多取舍暴露给了使用者。关系型数据库帮你把大部分决策做完了你只需要写 SQLHBase 则要求你自己想清楚数据怎么分布、按什么模式访问、增长节奏如何想不清楚它就会用性能问题提醒你。所以每次设计一张新表我都会在纸上把 RowKey 结构、列族划分、查询模式、数据增长预估这四项写清楚再动手建表。这个习惯帮我避免的返工比我学过的任何调优技巧都值钱。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑