
大数据圈子混得久了你会发现 HBase 是一个绕不开的存储组件。不管你是做实时数仓、用户画像、订单明细还是 IoT 设备数据采集只要数据量上了亿级、写入并发压力大了MySQL 撑不住的时候大家第一个想到的往往就是 HBase。但 HBase 这玩意儿有个特点入门看着简单安装好、跑个 put 就能写数据可真要把它用好、用稳坑其实不少。我从第一次搭 HBase 到现在维护过好几套线上集群中间踩过的坑、调过的参、排查过的 GC 问题攒了不少经验。这篇文章就按“先说清楚原理和场景再走一遍完整安装配置然后动手搞 Shell 和 Java API 读写接着讲 Web 界面怎么看、面试高频考点怎么答最后重点聊聊 GC 延迟太高怎么排查和优化”这条主线来展开。不管你是零基础想入门还是工作一两年的开发想系统查漏补缺这篇应该都能给你省下不少时间。1. 先搞清楚它是什么HBase 的数据模型和适用场景1.1 为什么不用 MySQL 硬撑很多刚接触 HBase 的人第一反应是我 MySQL 用得挺好的为什么要换这个问题的答案得从数据量级和写入模式说起。在线业务系统里MySQL 单表数据量过了千万到亿级别查询性能会明显下滑通常要靠分库分表来解决。但分库分表本身有代价比如跨分片查询、全局自增主键、数据迁移这些都要自己去处理。而且电商大促、日志采集这类场景写入峰值很高MySQL 的主从同步和单机写入瓶颈很容易成为瓶颈。HBase 设计之初就是面向海量数据的分布式存储底层依赖 HDFS天然支持水平扩展单表百亿行、千万级 QPS 也扛得住。它牺牲了关系型数据库的强一致性和复杂查询能力换来了极致的写入性能和横向扩展能力。所以核心判断标准很简单如果你的数据量在千万级以内、查询模式又复杂多变比如各种 join、模糊查询、动态条件组合那别折腾MySQL、PostgreSQL 这类关系型数据库更适合。如果数据量是亿级起步、写入非常密集、查询模式主要靠主键那 HBase 是合适的选项。1.2 数据模型拆解RowKey、列族、单元格和时间戳HBase 不是关系型数据库它没有“表结构”这个概念。你不需要提前定义表的列只需要定义“列族”。数据存进去时列族下可以动态添加任意列这给业务开发带来了很大灵活性。RowKey行键每一行的唯一标识相当于 MySQL 的主键。HBase 中的数据是按 RowKey 的字典序排列存储的所以 RowKey 怎么设计直接决定了你的数据怎么分布、怎么查询效率高这是 HBase 开发里最核心的一环。Column Family列族列的集合。一张表可以定义多个列族但实际应用中建议尽量少一般一两个就够。因为不同列族的数据在物理上是分开存储的如果列族太多读写时要跨多个文件操作性能会下降。Cell单元格由 RowKey 列族 列 时间戳唯一确定的一个值。HBase 支持多版本同一个单元格可以存多个历史版本按时间戳倒序排列。读取的时候默认取最新版。Timestamp时间戳每个单元格写入时自动生成也可手动指定。它用来区分同一条数据的多个版本也是 HBase 实现一些“时间范围查询”的基础。用生活化的例子来理解把 HBase 想象成一个超级大的“多版本字典”RowKey 是页码列族是章节分类列是具体词条时间戳是词条修订日期。查询时给定 RowKey就能定位到那一页然后从指定的列族里取出词条并且天然带着日期版本。1.3 HBase 能做什么不能做什么说直白点HBase 擅长的是“大并发写、主键点查、范围扫描”。常见应用场景包括用户行为日志、埋点数据存储订单数据、交易记录的明细归档物联网设备上报的数据存储实时计算Flink、Spark Streaming的结果落地用户画像、标签数据的读写但它也有明显的短板。首先HBase 不适合复杂的关联查询多表 join 基本做不了。其次二级索引不是原生功能需要自己维护比如用 Phoenix 或者自己建索引表。再次事务支持很弱只有行级原子性没有跨行跨表事务。最后虽然 HBase 支持 Scan 扫描但全表扫描性能很差不适合做分析型查询数据分析还是交给 Hive、Spark SQL、ClickHouse 这类组件。理解这些边界很重要。很多人 HBase 没用好往往不是技术问题而是场景选错了。2. 环境准备与安装配置从零跑起一个可用的 HBase2.1 版本选型别乱选跟着 Hadoop 版本走HBase 版本选型是很多人入门的第一个坑。HBase 的版本和 Hadoop 的版本有兼容性要求不能随便搭。目前社区主流版本是 1.x 和 2.x。HBase 1.x老牌稳定使用广泛很多公司的老集群还在跑。如果你只是学习或者 Hadoop 是 2.x 老版本选 1.4.x 系列比较稳。HBase 2.x引入了许多新特性比如更高效的 Region 分配机制、Procedure V2 等。如果你是新项目Hadoop 是 3.x直接用 2.3.x 之后的版本兼容性和稳定性都更好。还有一个“配套版本”的问题HBase 依赖 ZooKeeper也依赖 HDFS。完整的一个 HBase 系统其实有三个组件HDFS 负责存储、ZooKeeper 负责协调、HBase 本身负责数据读写逻辑。如果是自己部署这三个组件要版本匹配。如果是用集成环境比如 CDH、HDP不过 HDP 已经停止维护了集成环境会帮你处理版本对应关系。学习阶段建议直接去 Apache 官网下载与 Hadoop 版本匹配的二进制包避免自己从源码编译省掉很多麻烦。2.2 前置环境JDK、Hadoop、ZooKeeper 一个都不能少HBase 跑起来最少需要三样东西JDKHBase 1.x 需要 JDK 8HBase 2.x 建议 JDK 8部分新版本支持 JDK 11。这一步比较简单配好 JAVA_HOME 就行。HadoopHDFSHBase 数据最终存在 HDFS 上。学习时可以用伪分布式模式也就是一个节点上同时跑 NameNode、DataNode、HBase。生产环境至少需要 3 台以上的服务器组成集群。Hadoop 需要配置好 SSH 免密登录保证节点之间能互相访问。ZooKeeperHBase 用 ZooKeeper 来协调 HMaster 的选举、RegionServer 的注册、元数据的管理。学习环境也可以让 HBase 使用自带的 ZooKeeper它内置了一个单机版的但生产环境建议独立部署 ZooKeeper 集群至少 3 台。提示学习阶段最容易出现的错误就是 Hadoop 进入安全模式Safe Mode导致 HBase 无法创建根目录。遇到这个问题不要慌检查 HDFS 空间是否足够然后执行hdfs dfsadmin -safemode leave手动退出安全模式即可。安装顺序建议先装 JDK再装 Hadoop 并确保 HDFS 能正常启动再装 ZooKeeper最后装 HBase。每一步都先验证好再进下一步不要一口气全装完再来排查那样出了问题很难定位。2.3 安装步骤配置文件详解我用的是 Apache 官方二进制包伪分布式的模式来演示。下载后解压需要修改的核心配置文件有三个第一个是 hbase-env.sh。这里主要配置 JDK 路径和 JVM 参数。# 指定 Java 环境 export JAVA_HOME/usr/local/jdk1.8 # 是否使用 HBase 自带的 ZK学习时可以先设为 true生产设为 false export HBASE_MANAGES_ZKtrue # 给 HMaster 和 RegionServer 分配的内存建议设置至少 2G别用默认的 1G export HBASE_HEAPSIZE2G第二个是 hbase-site.xml。这里面配置的核心项如下。configuration !-- 伪分布式模式数据存本地 HDFS -- property namehbase.rootdir/name valuehdfs://localhost:9000/hbase/value /property !-- 开启分布式模式单节点也叫伪分布式 -- property namehbase.cluster.distributed/name valuetrue/value /property !-- ZooKeeper 地址这里是本机 -- property namehbase.zookeeper.quorum/name valuelocalhost/value /property !-- ZooKeeper 端口默认 2181 -- property namehbase.zookeeper.property.clientPort/name value2181/value /property /configurationhbase.rootdir 里的 9000 端口要和你 Hadoop 的 fs.defaultFS 配置一致否则 HBase 找不到集群会一直报连接异常。第三个是 regionservers 文件旧版本叫 regionservers2.x 还支持 conf/regionservers。里面写 RegionServer 所在的主机名学习环境就写 localhost 即可。配置完成后先启动 Hadoop再执行start-hbase.sh。启动完用jps查看进程能看到HMaster和HRegionServer进程说明启动成功。注意HBase 非常“娇气”如果服务器时钟不一致、hostname 解析不对、防火墙没关都可能启动失败或运行不稳定。学习前把这三件事提前处理好能省很多排查时间。2.4 端口清单常用的端口与服务对应关系很多人在学习时搞不清哪个端口是干什么的。我整理了一份常用端口清单按“必知”和“进阶”分类。端口服务说明2181ZooKeeper客户端连接 ZooKeeper 的默认端口HBase 依赖它做协调16000HBase MasterHMaster 的 RPC 服务端口客户端向 Master 请求元数据16010HBase Master Web UIHMaster 的 Web 管理界面端口16020RegionServerRegionServer 的 RPC 服务端口读写数据的核心端口16030RegionServer Web UIRegionServer 的 Web 管理界面端口9000HDFS NameNodeHadoop NameNode 的 RPC 端口默认可配置9870HDFS NameNode Web UIHadoop 管理界面Hadoop 3.x 默认端口8090Thrift ServerThrift API 服务的默认端口需单独启动8080HBase REST ServerREST API 服务的默认端口需单独启动这里面2181、16000、16020 三个端口是日常调试的“命脉”。客户端连接 HBase 第一步就是通过 ZooKeeper2181拿到元数据所在位置再去 RegionServer16020读写数据。如果你用 Java API 连不上集群第一件事就是 telnet 一下这几个端口是否通。3. Web 界面与 Shell 操作上手路径要这么走3.1 Web 界面看什么Master UI 与 RegionServer UIHBase 装好之后浏览器访问http://localhost:16010就能看到 Master 的 Web 界面。这个界面很多人忽略了其实它信息量很大比命令行直观得多。界面上有几块关键信息Tables列出所有表点击任意表可以查看表结构、Region 数量、每个 Region 所在哪台 RegionServer。Region Servers展示每个 RegionServer 的状态包括 Heap 内存使用情况、请求并发、StoreFile 数量、读写请求量等。这是排查 GC 问题和数据倾斜的第一现场。Backup Masters查看备用 Master 节点生产环境会配置多个 Master 做高可用。Tasks显示正在执行的任务比如 Region 分裂、负载均衡等。RegionServer 的 UI 在http://localhost:16030可以看到更细粒度的数据比如单个 Region 的请求量、BlockCache 命中率、MemStore 大小等。实际运维中我会先打开 Master UI 看集群整体负载分布再点进某个负载异常的 RegionServer 看它的内存和请求曲线。这套排查路径新手最好从一开始就养成习惯。3.2 常用 Shell 命令建表、写入、查询、删除一次学会HBase 自带一个类似 SQL 的 Shell但它不是 SQL命令风格偏“面向对象”。进入 HBase Shell 的方式是在命令行输入hbase shell。下面是我日常使用频率最高的命令整理成一个速查表操作命令说明查看帮助help列出所有命令建表create user_info, base, attr创建表 user_info包含 base 和 attr 两个列族查看表信息describe user_info查看表结构包括列族预分区信息查看所有表list列出所有表写入数据put user_info, rk001, base:name, zhangsan向 rk001 行的 base:name 列写入 zhangsan读取一行get user_info, rk001读取 rk001 行的所有列读取一列get user_info, rk001, base:name读取指定列的值扫描表scan user_info全表扫描小表可以大表慎用带条件扫描scan user_info, {LIMIT 10}只扫描 10 条删除一行deleteall user_info, rk001删除整行数据删除一张表disable user_infodrop user_info先禁用表再删除表顺序不能颠倒统计行数count user_info大表做 count 很慢慎用清空表truncate user_info清空表数据保留表结构写完几行数据后我建议用scan看看结果。你会在输出的末尾看到时间戳这是 HBase 存的版本号。有个容易忽略的小细节执行put时如果完全相同的 RowKey、列族、列写两次再get拿到的只会是最新一次的值但旧版本数据还在只是默认不显示。3.3 入门阶段最容易踩的坑Shell 操作本身不难难点在于理解 HBase 的一些“特殊表现”。第一个坑删除不是瞬间生效。deleteall之后数据并不是立刻从磁盘消失HBase 只是给这条数据打了一个删除标记。真正物理删除要等下次 Major Compaction 才会执行。所以有时候删完数据看磁盘空间没释放是正常的不用慌。第二个坑修改表结构要先将表 disable。比如你要给表加一个列族直接执行alter会报错必须先执行disable user_info再执行alter最后重新enable。这相当于数据库里的“离线维护”HBase 不允许在表可用状态下修改表结构。第三个坑count 命令在大表上是灾难。新手刚学会count就喜欢拿它来数产线表的数据量结果要么跑半天没反应要么直接把 RegionServer 压垮。因为 count 是逐行扫描统计不是像 MySQL 的select count(*)那样有现成的元数据。真需要统计行数用 Hive 或 Spark 跑批或者用 HBase 的tableName.getRegionsInfo结合每个 Region 的行数估算不要直接 count。4. Java API 实战读写代码怎么写才稳4.1 建立连接新版 API 的写法HBase 的 Java API 在 2.x 版本有了一些变化推荐使用ConnectionFactory来创建连接。这里有一个非常重要的经验Connection 是重量级的要复用不要每次读写都新建。它内部维护了连接池、元数据缓存、RPC 通道频繁创建销毁会极大拖慢性能。下面是一段基础连接代码import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.HBaseConfiguration; import org.apache.hadoop.hbase.client.Connection; import org.apache.hadoop.hbase.client.ConnectionFactory; public class HBaseConnectionUtil { private static Connection connection null; public static synchronized Connection getConnection() { if (connection null) { Configuration conf HBaseConfiguration.create(); // 设置 ZooKeeper 地址Java API 通过 ZK 找到 HBase conf.set(hbase.zookeeper.quorum, node01,node02,node03); conf.set(hbase.zookeeper.property.clientPort, 2181); try { connection ConnectionFactory.createConnection(conf); } catch (Exception e) { e.printStackTrace(); } } return connection; } }注意在 2.x 版本中HTable和HBaseAdmin这些老类都被标记为过时了。你现在写新代码用Table、Admin、Connection这套新接口。老接口虽然还能用但建议新项目不要沿用因为后续版本可能移除。4.2 建表与数据读写把核心调用串起来建表操作通常只在初始化和调整结构时运行代码里会用到Admin接口。先定义表描述再添加列族import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.client.Admin; import org.apache.hadoop.hbase.client.Table; import org.apache.hadoop.hbase.client.Put; import org.apache.hadoop.hbase.client.Get; import org.apache.hadoop.hbase.client.Result; import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.HTableDescriptor; import org.apache.hadoop.hbase.HColumnDescriptor; // 建表 try (Admin admin getConnection().getAdmin()) { TableName tableName TableName.valueOf(user_info); if (!admin.tableExists(tableName)) { HTableDescriptor desc new HTableDescriptor(tableName); desc.addFamily(new HColumnDescriptor(base)); desc.addFamily(new HColumnDescriptor(attr)); admin.createTable(desc); } } catch (Exception e) { e.printStackTrace(); } // 写入数据 try (Table table getConnection().getTable(TableName.valueOf(user_info))) { Put put new Put(Bytes.toBytes(rk001)); put.addColumn(Bytes.toBytes(base), Bytes.toBytes(name), Bytes.toBytes(zhangsan)); put.addColumn(Bytes.toBytes(base), Bytes.toBytes(age), Bytes.toBytes(25)); table.put(put); } catch (Exception e) { e.printStackTrace(); } // 读取数据 try (Table table getConnection().getTable(TableName.valueOf(user_info))) { Get get new Get(Bytes.toBytes(rk001)); get.addFamily(Bytes.toBytes(base)); Result result table.get(get); byte[] nameBytes result.getValue(Bytes.toBytes(base), Bytes.toBytes(name)); System.out.println(Bytes.toString(nameBytes)); } catch (Exception e) { e.printStackTrace(); }这里有个新手容易忽略的点HBase 的Put是支持批量写的一个Put里可以同时 add 多个列也可以一次性table.put(ListPut)批量提交多条数据。逐行 put 的写法在数据量大的时候吞吐量很低。我见过很多产线代码就是一句table.put(put)在 for 循环里跑一条一条插入这种写法在百万级数据量下性能非常感性一定要改成批量提交或者使用 BufferedMutator。4.3 扫描与过滤器别把全表当列表HBase 的Scan是范围查询的主要手段。很多人会写出全表扫描的代码这在数据量小的时候没事数据量一上来就把 RegionServer 打爆了。正确姿势是先定位行键范围再做扫描import org.apache.hadoop.hbase.client.Scan; import org.apache.hadoop.hbase.client.ResultScanner; import org.apache.hadoop.hbase.filter.PrefixFilter; // 只扫描 rk 前缀为 001 的数据 Scan scan new Scan(); scan.setStartRow(Bytes.toBytes(001_)); // 开始行键 scan.setStopRow(Bytes.toBytes(002_)); // 结束行键左闭右开 try (ResultScanner scanner table.getScanner(scan)) { for (Result result : scanner) { byte[] nameBytes result.getValue(Bytes.toBytes(base), Bytes.toBytes(name)); System.out.println(Bytes.toString(nameBytes)); } }这里setStartRow是包含的setStopRow是不包含的。如果你想让前缀匹配更方便可以直接用PrefixFilter. 但注意PrefixFilter虽然能过滤底层还是扫描了起始范围内的所有行所以它更适合“查询一部分数据”而不是“定位少量数据”。真正的高效查询还是依靠行键直接定位。4.4 连接管理谁用连接谁负责关闭HBase 的连接和表对象用完之后要关闭。Java 的 try-with-resources 是一个很有用的特性。但要注意Connection是全局共享的长连接不能每次操作完就 close。正确做法是整个 JVM 进程只创建一个 Connection通过getConnection()获取应用程序退出时统一关闭。Table是轻量级的可以每次获取、用完关闭因为它底层复用同一份连接。// 这样写是错的每次请求都创建新的连接 public void query(String rowKey) throws IOException { Connection conn ConnectionFactory.createConnection(conf); try { // 查询逻辑 } finally { conn.close(); // 连接被关闭下次请求又要重建性能极差 } }提示HBase 客户端连接池默认值是 1如果你有高并发需求在 ConnectionFactory 创建时通过hbase.client.ipc.pool.size参数调大连接池。不过多数业务场景下一个连接池就够用。5. 面试高频题架构原理与 RowKey 设计5.1 HBase 读写链路一次读写到底经历了什么面试最常见的一个问题就是“一条数据写进 HBase 要经过哪些环节”。这个链路理解了很多排障思路就通了。写入路径客户端通过 ZooKeeper 找到元数据所在位置也就是hbase:meta表这个表记录了所有 Region 和 RegionServer 的对应关系。拿到目标 RegionServer 地址后先写 WAL预写日志保证数据不丢失然后写入 MemStore内存中的有序缓冲区。当 MemStore 达到一定大小默认 128MB会触发 flush把内存中的数据写成 HFile 存到 HDFS 上。HFile 达到一定规模后会触发合并Compaction。读取路径客户端先看 BlockCache读缓存存储最近读取过的数据块没有命中再看 MemStore再没有就查 HFile。HBase 会把一个 RowKey 可能在的多个 HFile 并发读取然后把结果合并返回。所以如果你的表长期没有做 CompactionHFile 数量很多读性能就会下降。理解这两条链路你就明白了几个关键结论HBase 写性能好的原因写入是先写 WAL 写内存不是每次写都直接落磁盘落盘是异步批量进行的。HBase 读性能通常不如写入因为读需要查多个地方BlockCache、MemStore、多个 HFile是典型的“读放大”架构。BlockCache 和 MemStore 是 JVM 堆内的两块重要区域它们的空间配置直接影响 GC 表现。5.2 Region 分裂与负载均衡Region 是 HBase 数据分布的基本单位。一张表刚创建时只有 1 个 Region数据涨到一定阈值默认单个 Region 的存储数据超过约 10GB 或者行数达到阈值HBase 会自动把 Region 分裂成两个子 Region。分裂出的子 Region 会由 Master 决定是否分配到别的 RegionServer上。这个机制既是优点也是坑。优点是可以无限水平扩展。坑是如果 RowKey 设计不当所有数据都集中在少数 Region 上会导致“热点”问题也就是某个 RegionServer 忙死其他 RegionServer 闲死。面试里经常会问到“Region 热点怎么解决”核心答案就是 RowKey 设计要预分区 打散。提前预分区的做法是建表时就给表划分好多个 Region而不是让 HBase 以后自动分裂。这样数据一进去就很均匀地分布在多个 RegionServer 上。Shell 里可以这样建预分区表# 指定 0-9 前缀的行键分别落不同 Region create user_info, base, {SPLITS [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}5.3 RowKey 设计用倒序、加盐、散列的思路化解热点RowKey 设计是 HBase 面试和实战中权重最高的话题。核心原则是“高基数、低热点、按需前缀”。如果你的业务里最新数据访问最频繁那 RowKey 设计成“时间戳倒序 用户ID”这样最新数据排在前面scan 的时候直接读到。比如手机号时间的场景可以通过反转手机号避免同一区域用户集中在同一 Region。几个常用套路加盐Salting在 RowKey 前面加一个随机前缀比如将userId变成随机数用户ID数据会分散到不同 Region。代价是按原 RowKey 精确查询时要先算出前缀才能定位。哈希对原始 RowKey 做 MD5 或 CRC 后取前几位作为前缀比如原始 ID 是 10001加前缀后变成ad12_10001。查询时对用户 ID 做同样的哈希计算即可定位。这种方式均匀性比随机加盐更稳定。反转直接把 RowKey 字符串反转适用于自增 ID 或时间戳这种单调递增的键。反转后顺序性保留但起点分散。记住没有万能的 RowKey 设计只有适合业务的 RowKey。面试官问你 RowKey 设计的时候重点是要你说出“为什么这么设计”——是从热点、查询场景、数据倾斜这三个角度回答的。5.4 高频面试题速查表我整理了几道 HBase 面试中反复出现的问题附上简要回答思路问题核心思路HBase 和 Hive 的区别HBase 是 NoSQL 在线实时读写Hive 是离线批处理分析HBase 面向单条/范围查询Hive 面向全量 SQL 分析HBase 为什么写快读慢写走 WAL 内存异步落盘读可能命中 BlockCache、MemStore 或多个 HFile需要跨文件合并HBase 的 Region 是如何分裂的数据量达到阈值时Region 从中间 row key 处分裂成两个子 Region由 Master 分配调度HBase 的 WAL 是什么作用WAL 是预写日志防止写入 MemStore 后、未 flush 前发生宕机导致数据丢失为什么 HBase 不支持 join数据按 RowKey 分散在多台机器join 需要全表数据传输计算成本极高与分布式设计冲突HBase 数据删除为什么不释放空间删除只打标记物理删除要等 Major Compaction面试题看着多但归根结底考的是你对架构的理解和对核心配置的掌握。把读写链路和 RowKey 设计吃透大多数问题都能以不变应万变。6. GC 延迟太高怎么办性能优化的四个抓手6.1 先定位延迟高一定是 GC 问题吗网上关于 HBase GC 延迟的问题特别多其实现实中“延迟高”是一个综合结果要先定位再优化。HBase 是 Java 应用JVM 堆内存管理着 MemStore写缓存和 BlockCache读缓存。当堆内存频繁发生 GC尤其是 Full GC 时RegionServer 会短暂停顿表现为某段时间内请求延迟飙涨。定位手段主要有两种第一步看日志。RegionServer 日志里搜索GC关键字尤其关注Pause时间如果单次 GC 停顿超过几百毫秒说明问题已经很严重。第二步看监控。在 Master Web UI 里找 RegionServer 的内存曲线如果堆内存使用率长期在 80% 以上且出现锯齿状快速跌落说明 GC 周期非常频繁。还可以用jstat -gcutil pid 1000看 YGC 和 FGC 的频率。不过要注意如果所有 RegionServer 都延迟高而 GC 指标正常那可能要查 HDFS 是不是慢了、网络是否有抖动、是否在做大合并Compaction等。GC 只是其中一个常见原因。6.2 参数调整Heap 与 GC 算法选择如果你确认是 GC 问题第一件事就是检查 RegionServer 的堆内存配置。默认情况下HBASE_HEAPSIZE 设置的是 HMaster 和 RegionServer 共用的堆大小。生产环境建议单独指定 RegionServer 的堆给足内存。# hbase-env.sh export HBASE_HEAPSIZE4G export HBASE_REGIONSERVER_OPTS-Xmx8G -Xms8G -XX:MaxDirectMemorySize2G堆内存给多大取决于你的机器总内存和数据量。经验法则是机器内存 32G 时给 RegionServer 分配 16G~20G 比较合理要留一部分给操作系统 Page Cache因为 HBase 读取 HDFS 文件时会用到操作系统的 Cache这层经常被忽略。GC 算法的选择上老版本HBase 1.x默认使用 CMS因为它的停顿时间比较短。HBase 2.x 在 JDK 8 上G1GC 表现更好尤其适合大堆。如果你在 2.x 上还在用默认的穿行 GC建议换上 G1。export HBASE_REGIONSERVER_OPTS-Xmx8G -Xms8G -XX:UseG1GC -XX:MaxGCPauseMillis100 -XX:UnlockExperimentalVMOptions -XX:G1NewSizePercent8 -XX:G1MaxNewSizePercent20这里MaxGCPauseMillis设置的是目标最大 GC 停顿时间。不要设得太激进比如设成 10msGC 会过度回收反而增加 CPU 消耗、拉低吞吐。设到 100ms 左右是一个比较务实的区间。6.3 从架构层面优化MemStore、BlockCache 与 Compaction堆内存大小调完还要看内部的占比配置。HBase RegionServer 的堆内存主要分为三块MemStore、BlockCache 和其他。默认情况下MemStore 占 40%BlockCache 占 40%。这两个比例可以根据业务特点调整。写多读少把hbase.regionserver.global.memstore.size调高到 0.5 左右给写缓存更大的空间减少 flush 频率。读多写少把hbase.bucketcache.ioengine配置成 offheap为读缓存分配堆外内存。同时把hfile.block.cache.size调高到 0.5 左右提高 BlockCache 命中率。读写均衡保持默认 0.4/0.4再留 20% 给其他对象和 GC 开销。GC 延迟还有一个容易被忽略的元凶大合并Major Compaction。Major Compaction 会将一个 Region 下的所有 HFile 合并成一个大文件期间会大量读写磁盘推高内存和 CPU 使用率导致 GC 频繁。线上建议关掉自动 Major Compaction在业务低峰期手动执行。!-- 关闭自动 Major Compaction -- property namehbase.hregion.majorcompaction/name value0/value /property然后在凌晨 2 点到 4 点用脚本在低峰期跑一次echo \major_compact user_info\ | hbase shell6.4 别忽略客户端代码层面的优化也很关键有时候 GC 延迟高根因不在 HBase 服务端而在客户端代码。举个最常见的例子客户端用Scan全表扫描且不设置批量大小batch size一次性把所有行都读到客户端数据量很大时RegionServer 要为这个大结果集不停创建对象堆内存很快被打满进而触发频繁 GC。客户端的优化经验有三条第一Scan 时设置setCaching和setBatch。setCaching控制每次 RPC 从服务器取多少行到客户端默认是 1很慢建议调到 100~500。setBatch控制每次 RPC 取多少列。如果行很大列很多只看需要的列簇不要读全列。第二不要长期持有ResultScanner。有些代码拿了一个 ResultScanner 不关闭服务端资源一直被占用。务必在 finally 或 try-with-resources 里关闭 scanner。第三合理使用批量 put控制单次批量的大小。一般一次批量提交 1000~3000 条是一个比较好的区间。太小了 RPC 次数多太大了单次请求内存占用高也容易触发 GC。7. 写在最后我的几条 HBase 实践经验做了几年 HBase 相关的工作我最大的感受是它不像有些组件那样“开箱即用”需要花时间理解它的运行机制并且持续观察线上数据。最后分享几条个人的经验希望能帮你少走弯路。第一先把监控搭好再谈优化。刚接手集群时先把 HBase 自带页面看懂至少把 Master UI 的请求量、RegionServer 的堆内存和 GC 情况做成可视化报警再做参数调整。没有监控做依据的调优都是“拍脑袋”。第二RowKey 设计是永远的 C 位。不管你是做用户表、订单表还是日志表建表之前一定要花时间想清楚 RowKey。它决定你的热点、你的查询性能、你以后的运维成本。RowKey 定错了表已经写了几十亿条数据想改就很难了。第三线性扩展不等于无脑加机器。HBase 可以水平扩展但机器加多了节点之间的元数据同步、负载均衡、GC 开销也会增大。能用代码层面的优化解决的问题比如批量写、合理 scan、预分区就不要总是靠加机器来解决。HBase 这个生态没有想象中那么复杂但也绝对不简单。从基础概念到实战运维从 Shell 到 Java API从架构原理到 GC 排查每一个知识点拿出来都可以深挖很久。希望这篇文章能成为你入门路上的一个垫脚石。如果你在实操中碰到什么奇怪的问题欢迎随时一起交流讨论。