
如果你正在搭数据平台或者刚接手一套离线数仓又接到一个“实时读写”的需求HBase迟早会出现在你面前。它能撑千万级QPS、百TB级数据量还带列族设计这个面子工程确实不好绕开。这篇东西不是官方文档翻译是我在一台4核8G的测试机上从零装完、跑完、又折腾了好几轮之后的完整记录。从tar包解压到Shell读写再到Region分裂、MemStore参数和那些报错排查全都过一遍。1. 为什么是HBase先搞清楚它在整个大数据体系里的位置上手任何一个组件之前我习惯先弄清楚它能干什么、不能干什么。HBase是一个分布式、面向列的KV数据库跑在HDFS之上底层用HDFS做持久化本身不存文件。它解决的问题是对海量数据的随机实时读写。传统关系型数据库撑到几千万行就开始吃力HBase在几百TB甚至PB级别的数据集上仍然能保证毫秒级延迟靠的是自动分片、LSM树和列式存储这套组合拳。你平时听到的“HBase是宽表”“HBase擅长写不擅长读”这些说法都太粗糙了。它确实擅长写因为写入走的是顺序追加不是随机写盘但读也不弱前提是你知道RowKey的设计规矩。用生活化的类比来讲关系型数据库像一个大仓库所有货品都分门别类放在固定的货架上查询时按索引快速定位HBase更像一个物流中转仓货物按标签堆在一起每次写货只往门口扔由后台工人统一整理入库取货时只要拿着标签去对应区域翻找。标签就是RowKey区域就是Region。适合用HBase的典型场景用户行为日志、订单流水、消息记录、监控指标、推荐系统的特征存储。不适合用HBase的场景复杂关联查询、事务强一致的账务系统、需要动态扩展字段且不预先设计Schema的场景。如果你只是做离线分析每天跑一次批处理那Hive加Parquet可能更合适没必要把HBase的运维复杂度背在身上。还有一点必须提前说清楚HBase不是选了就能用好的。它的性能上限基本取决于三个因素——RowKey设计、列族设计、Region预分区。这三个东西改起来都相对费劲设计错了后面再调等于重建表。所以安装之前花半小时理解它的架构和工作原理性价比极高。2. 环境准备与版本选择这一步错了后面全是泪2.1 硬件与软件环境说明先说我的测试环境比较典型的小公司配置一台4核8G内存的云主机50G系统盘加100G数据盘操作系统是CentOS 7.9JDK版本1.8Hadoop版本3.1.4。HBase版本用的是2.4.17。这套组合我踩过坑也帮朋友排查过问题可以负责任地说稳。HBase和Hadoop的版本兼容是个老生常谈但永远有人栽跟头的点。官方文档里有个兼容矩阵但很多人不看结果装完启动一看RegionServer起不来日志里全是ClassNotFoundException或者NoSuchMethodError。这类问题十有八九是Hadoop和HBase版本不匹配。经验法则HBase 2.x配Hadoop 2.10.x或3.1.x都没问题但别拿Hadoop 3.2以上去配老版本HBaseHadoop的RPC协议变了认不全。JDK版本统一用1.8。HBase 2.x官方支持Java 8虽然新版本也在尝试支持Java 11但生产环境没必要冒险大数据生态链上很多组件都依赖Java 8长期维护省心。2.2 主机名与SSH免密登录配置这一步我吃过亏。HBase的RegionServer启动时会通过主机名解析地址如果你的/etc/hosts里配的是临时主机名或者干脆没配启动日志就会卡在“waiting for regionserver to start”然后超时。所以我现在的固定流程是先配好主机名和hosts再装别的。我建了一个单节点伪分布式环境主机名叫hbase-testhosts文件里加一行192.168.1.100 hbase-test然后配SSH免密登录否则HBase远程启动RegionServer时会要你输密码非常烦ssh-keygen -t rsa -P -f ~/.ssh/id_rsa cat ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys2.3 JDK与Hadoop安装验证JDK用yum装或者tar包解压都行装完必须检查JAVA_HOME对不对。HBase启动脚本是通过JAVA_HOME去找Java的这个变量没配好启动必挂。我吃过一个更隐蔽的亏系统里同时存在OpenJDK 1.8和CDH自带的JDK导致HBase用了错误版本的Java启动时反复提示“Unsupported major.minor version”。查了半天最后发现是/etc/profile里配的JAVA_HOME被后加载的脚本覆盖了。我的习惯是优先用tar包安装Oracle JDK如果还能找到的话或OpenJDK路径统一放在/usr/local/java然后写入/etc/profileexport JAVA_HOME/usr/local/java/jdk1.8.0_202 export PATH$JAVA_HOME/bin:$PATHsource /etc/profile之后执行java -version确认版本。Hadoop这边如果只是为了跑HBase单机测试可以只启动HDFS的NameNode和DataNode。HBase依赖HDFS做最终的落盘存储但不太需要YARN。我之前犯过一个错把所有Hadoop组件全启动了内存消耗巨大8G的小机器直接卡死后来只保留HDFS相关进程才顺畅。启动完后用jps验证进程我这边能看到NameNode和DataNode两个进程。然后执行hdfs dfs -mkdir -p /hbase把HBase要用的根目录建好虽然HBase自己会建但提前建好可以指定权限避免后续写权限的坑。3. HBase安装全流程下载、解压、核心配置逐项拆解3.1 下载与解压下载HBase二进制包可以通过Apache镜像站获取选择bin.tar.gz包。不推荐源码包编译耗时长不说还容易引入依赖问题。解压到/opt目录同时建立软链方便版本升级tar -zxvf hbase-2.4.17-bin.tar.gz -C /opt/ ln -s /opt/hbase-2.4.17 /opt/hbase这里有个小习惯值得养成统一在/opt下建软链版本升级时只需要重新指定软链指向配置里的路径不用动。我见过有人在多个目录里解压多份HBase最后自己都搞不清在跑哪个版本。3.2 conf目录三件套hbase-env.sh、hbase-site.xml、regionservers解压完成后进入conf目录需要动三个文件hbase-env.sh、hbase-site.xml、regionservers。伪分布式环境单机跑全部角色到这里已经够了大部分测试环境就是这种模式。hbase-env.sh里有个烦人的点默认配置会让HBase自己管理JDK路径。我之前没注意看结果HBase启动时用了系统自带的Java版本与预期不一致。最好把JAVA_HOME显式写死export JAVA_HOME/usr/local/java/jdk1.8.0_202 export HBASE_HEAP2g export HBASE_OFFHEAPSIZE1g export HBASE_PID_DIR/var/run/hbaseHBASE_HEAP和HBASE_OFFHEAPSIZE这两个参数很多人不知道。HBase的读路径大量使用堆内缓存BlockCache和堆外内存做读写缓冲堆外配小了高并发读写时延迟会一路走高。单机测试我给了2G堆和1G堆外生产环境至少4G堆以上视RegionServer上Region数量而定。hbase-site.xml是重头戏伪分布式核心配置如下configuration property namehbase.rootdir/name valuehdfs://hbase-test:9000/hbase/value /property property namehbase.cluster.distributed/name valuetrue/value /property property namehbase.zookeeper.quorum/name valuehbase-test/value /property property namehbase.zookeeper.property.dataDir/name value/data/zookeeper/value /property property namehbase.unsafe.stream.capability.enforce/name valuefalse/value /property /configuration这里有三个坑需要展开说明hbase.cluster.distributed设为true是伪分布式和单机模式的区别。如果设为falseHBase会以local模式运行ZooKeeper和HDFS都不需要但你也用不了完整特性适合快速验证代码逻辑。如果要和HDFS联动必须设为true。hbase.rootdir里的端口要和Hadoop的fs.defaultFS保持一致。我见过有人在这里写错端口HBase一直连不上HDFS日志报“FileSystem not found”。检查方式很简单Hadoop的core-site.xml里写了什么端口这里就写什么端口。hbase.unsafe.stream.capability.enforce设为false是因为HDFS-3.1.4的StreamCapability接口和HBase的某些旧版实现不兼容。不设这个参数RegionServer启动时很可能因为WAL写入HDFS失败而反复崩溃。我在2.4.x和Hadoop 3.1.x的组合下必加这个参数。另外regionservers文件里写一行主机名hbase-test这个文件是HBase用来远程启动RegionServer的清单。单机写自己分布式就写所有参与节点的主机名每行一个。3.3 启动与日志观察一切配置好启动之前建议把HDFS先拉起来。HBase启动时如果HDFS没就绪不会等待而是直接报错。启动命令很简单start-hbase.sh启动后在每个节点上执行jps应该能看到HMaster: 主控进程HRegionServer: 数据节点进程HQuorumPeer: HBase自带的ZooKeeper进程注意一点HBase自己会下载并启动一个内置的ZooKeeper。伪分布式或测试环境用这个内置ZooKeeper完全够用不用额外装一套。生产环境建议用独立ZooKeeper集群避免HMaster和RegionServer各挂一个ZooKeeper导致脑裂。怎么把内置换掉把hbase-site.xml里的hbase.zookeeper.quorum指向外部ZK地址并把hbase-env.sh里的HBASE_MANAGES_ZK设为false。启动后最有效的验证方式是打开HBase的Web UI默认端口是16010浏览器访问能看到HMaster的状态、RegionServer列表、Region分布等信息。如果你发现Web UI打不开别急着重启先查这里tail -100 /opt/hbase/logs/hbase-hbase-master-hbase-test.log日志是排查问题的一手信息来源。HBase的日志写得还算友好基本会告诉你哪个配置不对、哪个目录权限不足、哪个端口被占用。我每次排查都从日志最后200行看起十次有八次能直接定位。4. 核心概念速览建表前必须先想明白的几个名词4.1 RowKey、列族、Region和Cell接触HBase时你会遇到一堆名词RowKey、Column Family、Region、Cell、TimeStamp。这几个概念不搞清楚后面建表、写代码、调优全都无从谈起。RowKey是HBase的一级索引就是每条记录的唯一标识按字典序排列。HBase的“随机读快”本质上是靠RowKey快速定位到对应Region再在Region内部查MemStore和HFile。RowKey设计是整个HBase使用中最重要的一件事——顺序读、随机读、热点规避都靠它。比如存储用户行为数据常见的设计是把用户ID反转后拼接时间戳避免同一用户的数据连续写入导致Region热点。列族Column Family是列的集合一个表可以定义1到3个列族不宜多。列族内的列可以动态添加不需要提前建Schema这是很多人觉得HBase“爽”的地方。但列族多了会严重影响性能因为一个Region的数据是按列族分开存放的列族多了意味着同一个RowKey的数据被拆到多个文件里读一条记录要跨多个路径去拼装。Region是数据分片的基本单位。一张表初始只有一个Region数据写到一定大小后会自动分裂成两个。Region的分裂和分布是HBase自动完成的但你完全可以手动干预这就是预分区。Cell就是具体的某个值由“行键列族列时间戳”定位不同时间戳可以存多个版本。这个版本数可以通过表参数VERSIONS配置默认值是1也就是说默认只保留最新版本。4.2 MemStore与HFile理解HBase“写快读也快”的底层秘密写入HBase的流程是这样的客户端先写WALWrite-Ahead Log预写日志然后数据进入RegionServer内存中的MemStore。MemStore攒到一定大小默认128MB之后会刷写成HFile落盘到HDFS。HFile是不可变的后续再刷写会产生新HFile后台会做合并Compaction把小文件合成大文件。这整套机制和传统数据库的B树索引不同HBase用的是LSM树Log-Structured Merge-Tree日志结构合并树的思路。写入时只做顺序追加不做原地修改所以写性能极好读取时先查MemStore再查HFile通过布隆过滤器跳过不存在数据的文件所以读性能也不错。明白了这个机制很多调优决策就有依据了。比如为什么RegionServer的堆内存要给大因为MemStore和BlockCache都要吃内存。为什么要控制刷写频率因为频繁刷写会产生大量小HFile后台合并会占用大量IO和CPU。这些都对应具体的配置参数后面第7节会展开讲。另外有一点新手容易误解HBase的“删除”也不是真正的物理删除而是写入一条带有删除标记Tombstone的数据。真正的物理删除要等合并的时候才做。所以删除频繁的表HDFS上占用的空间可能不会明显下降不要慌那是正常现象。5. HBase Shell实操从建表、增删改查到预分区5.1 进入Shell与基础建表HBase Shell是和HBase交互最直接的方式。执行命令hbase shell进入交互界面后先看有哪些命名空间list_namespace默认有default和hbase两个hbase是系统表所在的命名空间一般不要动。业务数据建议建一个专属命名空间比如create_namespace blog建一个用户行为表create blog:user_action, {NAME info, VERSIONS 3}, {NAME detail}这里创建了一张表blog:user_action有两个列族info和detail。info列族保存3个版本的数据。注意VERSIONS设置过大会浪费存储默认1在很多业务场景够用设置版本的时候也要考虑业务是否真的需要回溯历史值。我曾经遇到过有人把VERSIONS设成Integer.MAX_VALUE结果表膨胀速度飞快最后不得不做一轮大合并来清理。5.2 写数据put命令的细节写入数据用put命令put blog:user_action, rowkey_001, info:user_id, U10001 put blog:user_action, rowkey_001, info:action, click put blog:user_action, rowkey_001, detail:page, /homeput命令的语法结构是表名、行键、列族:列名、值。同一个RowKey可以连续put多个列HBase会为你拼装出完整的一行数据。这里我想强调几个实际使用中会犯的错一是列名写错比如把info:user_id写成inf:user_id数据会静默写到一个不存在的“列”上查的时候怎么都查不出来这种错误很难排查因为HBase不会报错。二是写入的时间戳问题默认使用服务器当前时间如果你的应用服务器和HBase服务器时间相差很大写入的数据版本顺序会乱查出来的可能是旧数据所以生产环境务必配置NTP时间同步。5.3 读数据get、scan与过滤器读取单行用getget blog:user_action, rowkey_001读取指定列族get blog:user_action, rowkey_001, {COLUMN info}扫描整表用scan生产环境慎用无过滤条件的scan数据量大时会全表扫描把RegionServer的IO彻底打满scan blog:user_action, {LIMIT 10}scan支持很多高级参数比如指定列、指定时间范围、指定RowKey范围scan blog:user_action, {STARTROW rowkey_001, ENDROW rowkey_002, COLUMN info:action}过滤器的组合也常用虽然底层性能不一定好但调试和小范围查询很方便scan blog:user_action, {FILTER ValueFilter(,substring:click)}5.4 删除与表管理操作删除一行deleteall blog:user_action, rowkey_001删除某个单元格的某个版本可以指定时间戳。但注意如果VERSIONS1删了就找不回来了因为历史版本没保留。更新表结构和停用表disable blog:user_action alter blog:user_action, {NAME info, VERSIONS 5} enable blog:user_action创建表之后如果要改列族的属性比如VERSIONS、TTL必须先disable再alter再enable。这个流程很多人记不住直接alter会报错Table is enabled。删除表同样要先disabledisable blog:user_action drop blog:user_action5.5 预分区设计要不要做怎么做预分区可能是在Shell操作里最容易被忽略、但生产环境最该做的一件事。如果没有预分区一张新表只有一个Region所有写入都打在一个RegionServer上数据量起来后虽然会自动分裂但分裂过程会产生大量IO和短暂的停顿。预分区的做法是在建表时通过SPLITS指定分区边界create blog:user_action, {NAME info}, {NAME detail}, SPLITS [001, 002, 003, 004]这样表一开始就有5个Region数据按RowKey字典序落入不同的分区写入压力会被分散到多个RegionServer。如果对RowKey的分布有预判也可以结合十六进制边界create blog:user_action, {NAME info}, SPLITS_FILE /data/hbase_splits.txtSPLITS_FILE方式适合分区数量多、手工写Splits不方便的场景文件里每行一个边界RowKey。我在生产环境做埋点表设计时通常按业务前缀对RowKey分段比如用户ID首位数字分段这样同一区间的用户走同一个Region便于范围扫描。6. 数据读写完整流程从Shell到Java API6.1 一次put请求的旅程了解数据在HBase内部是怎么流转的排查问题时会更有头绪。我把一次put请求的完整路径拆开客户端根据RowKey通过ZooKeeper找到元数据表hbase:meta的位置。从hbase:meta表中查到这个RowKey对应的Region位置在哪个RegionServer上。客户端直接向目标RegionServer发送写请求不再走ZooKeeper和HMaster写路径上HMaster不参与。RegionServer收到请求后先把数据顺序追加写入WAL日志HDFS上的一个文件。数据写入RegionServer内存中的MemStore按列族分别存放。WAL写入成功且MemStore更新完成向客户端返回写入成功。后台异步执行刷写把MemStore固化为HFile落到HDFS。这个过程中“客户端缓存了RegionServer的地址”是个关键优化。建表之后第一次读写慢是因为需要去查元数据之后快是因为本地缓存了位置信息。如果某个Region发生分裂或迁移客户端会收到NotServingRegionException然后重新刷新meta缓存这个过程完全自动。6.2 继续深入读请求的路径与优化点读请求比写请求多一些步骤。客户端定位Region之后RegionServer需要在MemStore和多个HFile中查找数据。如果数据刚写入还没刷写就从MemStore读如果已经刷写就从HFile读。实际查询会把结果合并后返回。优化的关键在于减少“需要打开的文件数”。做过优化工作的人都应该知道这两个手段一是布隆过滤器Bloom Filter在建表时设置是优化随机读的利器它能快速判断某个StoreFile里是否有你要的RowKey不用逐个文件打开比较二是BlockCache读链路会缓存热数据块重复读同一个RowKey时直接从内存返回速度极快。我遇到过一个实际场景某表做了全表扫描查询耗时几十秒RegionServer CPU飙高。排查后发现没有用任何过滤条件、没有设置LIMIT纯粹是一次低级失误。后来改成按RowKey范围扫描加了布隆过滤器耗时降到几百毫秒。6.3 Java API示例生产代码的常见写法业务系统集成HBase一般用Java API。下面这段代码是生产环境常见的写法包含了连接管理、单条写入和批量写入对比Configuration config HBaseConfiguration.create(); config.set(hbase.zookeeper.quorum, hbase-test); config.set(hbase.zookeeper.property.clientPort, 2181); Connection connection ConnectionFactory.createConnection(config); Table table connection.getTable(TableName.valueOf(blog:user_action)); // 单条写入 Put put new Put(Bytes.toBytes(rowkey_001)); put.addColumn(Bytes.toBytes(info), Bytes.toBytes(user_id), Bytes.toBytes(U10001)); put.addColumn(Bytes.toBytes(info), Bytes.toBytes(action), Bytes.toBytes(click)); table.put(put); // 批量写入 ListPut puts new ArrayList(); for (int i 0; i 1000; i) { Put p new Put(Bytes.toBytes(rowkey_ String.format(%05d, i))); p.addColumn(Bytes.toBytes(info), Bytes.toBytes(value), Bytes.toBytes(v i)); puts.add(p); } table.put(puts); table.close(); connection.close();几个实际开发中容易踩的坑Connection是重量级对象整个应用维护一个单例即可不要每次操作都创建。我第一次写的时候没注意每次请求都创建Connection结果线上出现了大量ZooKeeper连接把HBase的ZooKeeper会话数打满直接拒绝新连接。Table对象是轻量级的但也不建议每次new可以复用或者用连接池管理。批量put时最好控制单批次大小1000到5000条是比较合理的区间。一次性塞几万条容易内存溢出或触发RegionServer的写阻塞。写完记得调用table.close()不关闭连接会导致ZooKeeper会话泄漏时间一长整个集群就无响应了。我见过有人不看日志只发现集群越来越慢最后发现是客户端连接泄漏把ZooKeeper拖垮了。6.4 批量导入的高效方式BulkLoad写入海量数据时用put一条条写效率太低。HBase提供BulkLoad方式直接生成HFile后加载到表里绕过了写WAL和MemStore的路径导入速度可以比put快数倍到数十倍。流程是写一个MapReduce程序或用HBase的自带工具把数据源打包成HFile然后用HBase的loadIncrementalHFiles工具加载。这一步操作比看起来复杂但工业级导入基本都走这条路。用HBase自带的ImportTsv工具做个示例hbase org.apache.hadoop.hbase.mapreduce.ImportTsv -Dimporttsv.columnsHBASE_ROW_KEY,info:user_id,info:action -Dimporttsv.bulk.output/tmp/hfile_output blog:user_action /data/input.tsv hbase org.apache.hadoop.hbase.mapreduce.LoadIncrementalHFiles /tmp/hfile_output blog:user_action注意两点BulkLoad生成HFile前表必须存在且列族匹配BulkLoad的HFile文件目录要和表所处的文件系统一致如果HBase开启了HDFS的HA要注意路径里的namenode地址配置。这个方法我强烈建议每个用HBase的人都掌握哪天要迁移数据、回放历史消息时你会发现它是救命的。7. 高频问题排查实录这些坑我替你踩过了7.1 RegionServer启动失败最常见的问题之一排查思路按顺序走先看RegionServer日志重点看是不是HDFS连接问题。常见报错是短读或FileSystem closed这类问题多半是hbase.rootdir配错或者HDFS端口和hbase.rootdir端口不一致。把core-site.xml里的fs.defaultFS找出来逐一对比。再看ZooKeeper连接问题。报错信息里有ConnectionLossException多半是ZooKeeper地址配错或者内置ZooKeeper没起来。检查hbase.zookeeper.quorum是否写了可解析的主机名检查防火墙是否放行了2181端口。最后看内存问题。报错里有Unable to load native-hadoop library这不是致命错误只是提示HBase找不到Hadoop的原生库性能会有折扣但功能正常。真正致命的是OutOfMemoryError或Java heap space这时要把hbase-env.sh里的HBASE_HEAP调大。遇到过一种很隐蔽的情况RegionServer进程在但Web UI里看不到它日志反复报sleeping before rechecking。这是RegionServer和HMaster之间心跳断了常见原因是机器负载过高、GC时间过长或者网络抖动。优先看系统负载和GC日志不要在HBase日志里死磕。7.2 写数据变慢Region的写入阻塞机制写数据变慢的原因很多最常见的是MemStore达到了阻塞阈值。HBase的RegionServer默认有四个阈值hbase.hregion.memstore.flush.size默认128MB、hbase.regionserver.global.memstore.lowerLimit默认0.4即堆内存的40%、upperLimit默认0.45即45%和hbase.hregion.memstore.block.multiplier默认4。当一个Region的MemStore大小达到flush.size时触发刷写当全部Region的MemStore总和达到upperLimit时整个RegionServer开始阻塞写入直到刷写降到lowerLimit以下。此时你会在RegionServer日志里看到大量“Blocking updates”的提示。调优思路堆内存够大时可以把upperLimit和lowerLimit之间的差值拉大一点避免频繁阻塞堆内存不够时减少单Region的MemStore大小或增加RegionServer数量。此时flush.size不宜设太大否则刷写时间长阻塞期间的延迟更明显。我在4G堆的测试机上试过默认配置下连续写入几万条数据后写延迟从几毫秒涨到几十毫秒日志里出现了Blocking updates字样。把hbase.regionserver.global.memstore.upperLimit调成0.5后延迟恢复平稳。但注意堆内存里MemStore占多了留给BlockCache的就少了读性能会下降。这需要根据读写比例找平衡点。7.3 遇到的经典报错与解法把过去遇到过的高频报错整理成一张速查表方便大家直接对照排查报错信息出现原因解决方案Region server is shutting down启动时HDFS写入失败或Region文件损坏检查hbase.rootdir和HDFS状态必要时在HDFS里删除损坏的临时文件NoServerForRegionException元数据缓存过期Region发生迁移客户端会自动刷新meta缓存重试即可如果频繁出现检查RegionServer是否频繁宕机PleaseHoldException正在做负载均衡或Region分裂等待片刻重试属正常现象java.io.IOException: Table is disabled对已禁用表执行写操作先用enable恢复表Connection refusedRegionServer的RPC端口被防火墙挡住检查16020端口和防火墙规则Master is initializingHMaster正在进行初始化等待初始化完成不要反复重启Scanner timeout扫描超时调大客户端scan的caching值或分批扫描少量数据有一种报错比较隐蔽平时不报但一旦报表流量大就会冒出来叫“Regionserver上的handler数量不够”。默认RPC handler数是10如果单台RegionServer上并发请求太多会出现RpcTimeoutException或者RegionTooBusyException。此时调大hbase.regionserver.handler.count到30或50但别调到100以上会引发线程上下文切换开销反而变慢。这块调优我吃过亏原先设了64发现CPU使用率比默认高了快一倍后来调回32延迟反而降下来了。7.4 扫描慢Region数过多或StoreFile过多scan慢一般分两种情况。第一种是表Region过多。一张表几十个Region每个Region分布在不同的RegionServer上扫描时要跨很多节点汇总数据慢是正常的。解决办法是减少Region数量或者通过预分区设计控制Region数量增长。第二种是StoreFile数量过多。MemStore刷写频繁导致小HFile文件堆积扫描要读很多文件。HBase默认超过3个StoreFile就会触发合并但合并是异步的如果写入速度远大于合并速度文件会越积越多。这时需要调大hbase.hstore.compactionThreshold默认3减少合并频率或者调大hbase.hregion.memstore.flush.size减少刷写次数。怎么看StoreFile数量HBase Web UI的RegionServer详情页里能看到每个Region的StoreFile数。如果发现一个Region有几十个StoreFile扫描想快都难。我调优时会把重点放在减少文件数量而非压榨单文件读取性能上事实证明这个思路更有效。8. 参数调优与性能观察让HBase真正跑舒服8.1 内存布局和核心参数调优的前提是理解RegionServer的内存布局。RegionServer的JVM堆内存主要分三块MemStore写缓冲、BlockCache读缓存、其他RPC响应、元数据等。MemStore和BlockCache是此消彼长的关系配置比例取决于业务读写特征。我常用的参数组合4G堆内存、混合读写场景property namehbase.regionserver.global.memstore.size/name value0.4/value /property property namehfile.block.cache.size/name value0.3/value /property property namehbase.hregion.memstore.flush.size/name value134217728/value /property property namehbase.regionserver.handler.count/name value32/value /property写多读少的场景MemStore的比例可以调到0.5BlockCache降到0.2读多写少的场景反过来。这个比例不是拍脑袋定的我建议上线前做一次读写压测根据延迟曲线调整。还有一个容易忽略的参数hbase.hstore.blockingStoreFiles默认10当StoreFile数量超过10时Region会阻塞写入直到合并线程处理完。写入压测时如果发现延迟突然恶化检查一下是不是触发了这个阻塞机制。可以适当调大这个值但不建议超过16否则文件堆积会显得不可控。8.2 关键指标观察方法安装好后不知道怎么判断“它到底跑得好不好”这是很大的问题。我建议重点关注四个指标HBase Web UI的RegionServer页面能看到MemStore大小、StoreFile数量、读写请求数readRequestsCount和writeRequestsCount。通过观察这几个指标能判断集群负载是否均衡。系统层面的IO指标iostat能看到磁盘的真实IO压力HBase的写入虽然走顺序IO但如果合并频繁磁盘IO依然会飙高。GC日志HBase的RegionServer对延迟敏感Full GC会导致整个RegionServer停顿数秒。在hbase-env.sh里加上GC日志参数export HBASE_OPTS$HBASE_OPTS -XX:PrintGCDetails -XX:PrintGCDateStamps -Xloggc:/opt/hbase/logs/gc.log观察gc.log里Full GC的次数和耗时如果Full GC频繁优先调整堆大小和MemStore/BlockCache比例。ZooKeeper会话数通过ZooKeeper的3 Letter命令查看会话数异常升高说明客户端连接泄漏要及时处理。8.3 临时调优与长期规划的区分有个容易犯的错误一看到性能问题就调参数先把参数改了再说。但参数调优只能缓解“配置不合理”的问题如果表设计RowKey、列族、预分区本身有问题调参就是治标不治本。比如频繁的热点写问题RowKey设计不分散就算把参数调到最优写入还是压在少数几个Region上。再比如单表数据量无限增长Region无限分裂Region数量变多后元数据管理、扫描效率都会退化这种时候要考虑分区归档或数据淘汰策略。我自己的经验是高压测试阶段遇到问题先看表结构和RowKey设计再查参数配置最后才怀疑集群本身。按这个顺序走通常能较快定位根因。9. 实测案例写入压力测试中的性能曲线与参数调整过程9.1 测试目标与表结构设计为了验证前面讲的参数调优效果我自己做了一轮模拟压测模拟一个消息类场景持续写入1000万条数据观察写入延迟和集群状态的变化。这个案例可以当作HBase性能基线测试的参考模板。表结构设计create blog:test_msg, {NAME m, VERSIONS 1, COMPRESSION SNAPPY, BLOOMFILTER ROW}, {NUMREGIONS 10, SPLITS [0000000000,1111111111,2222222222,3333333333,4444444444,5555555555,6666666666,7777777777,8888888888]}RowKey采用“消息ID取反发送时间戳”的组合形式。为什么取反因为消息ID通常自增如果顺序增长这些消息会全部写入同一个分区导致Region热点严重时一台RegionServer被打满其他节点闲置。取反之后ID分散在各个分区写入负载也被打散。列族设成单列族m开启SNAPPY压缩CPU换IO默认建议开启布隆过滤器设为ROW适合随机读场景。9.2 压测过程与结果读法压测环境就是开头说的4核8G测试机用Java多线程批量写入每次批次1000条。第一次测试用默认参数写了几十万条后延迟曲线开始抖从平均2毫秒涨到了20毫秒以上堆内存占用持续上升。看日志发现频繁刷写和合并站在测试机旁边都能听到磁盘咔咔响。调整后的参数这里再列一遍这是最终验证有效的配置property namehbase.regionserver.global.memstore.size/name value0.45/value /property property namehbase.regionserver.global.memstore.lowerLimit/name value0.38/value /property property namehfile.block.cache.size/name value0.25/value /property property namehbase.hregion.memstore.flush.size/name value268435456/value /property property namehbase.hstore.compactionThreshold/name value5/value /property刷写阈值从128MB调到256MB减少了刷写次数合并阈值从3调到5减少了小文件合并触发频率MemStore比例为堆内存的45%Buffer空间更充足。调整后重新压测平均写延迟稳定在3毫秒左右Full GC次数明显减少集群平稳跑完了1000万条写入。这次压测还有一个意想不到的发现布尔过滤器在随机读场景下的收益比预期更大。我在1000万条数据中随机读1万条开启了BloomFilter的耗时是从关闭状态的一半左右。有次在真实环境中排查线上延迟最后定位为一个列族没有开BloomFilter线上数据读取要扫大量HFile开完之后延迟直接降了一个数量级。9.3 结果分析与取舍逻辑读这些结果时我想提醒你一个容易忽略的点性能测试的结果受环境条件影响极大。单节点4核8G压出来的数据和分布式环境里十几台机器压出来的数据不能直接对比。你真正要对比的是“同样环境下配置调整前后的差距”。参数的调整取决于业务场景。比如线上是做实时风控的那么读写比例可能偏向“写多读少”MemStore的比例可以更大BlockCache可以更小如果做用户画像查询则是“读多写少”BlockCache适当调大。另外机器内存越大MemStore和BlockCache的比例同样需要微调因为堆内存大了之后单块缓存的绝对数值变化会影响刷新触发时机。我建议你把每次调参前后的指标截图保存下来慢慢积累自己环境的基准值。这个过程比翻阅文档有用得多你慢慢会培养出对“这个配置在我的环境里会产生什么效果”的直觉。10. 必要提醒伪分布式到生产环境还有哪些事要做测试环境跑通之后从伪分布式切换到生产部署有几个事早晚要面对提前列一下一是外部ZooKeeper集群。生产环境必须用独立的三节点ZooKeeper不能拿HBase内置的ZooKeeper顶数。部署方式是安装独立ZooKeeper集群改hbase-site.xml的hbase.zookeeper.quorum同时把hbase-env.sh里的HBASE_MANAGES_ZK设成false。二是HDFS的高可用。HBase的元数据和数据最终都存在HDFS上如果NameNode挂了整个HBase服务也就不可用了。生产环境建议让HDFS配置NameNode HA避免单点故障。三是安全认证。开启Kerberos认证之后所有客户端的访问都要先认证配置复杂度明显提升。如果公司有统一的认证平台建议早点接入后期再改很痛苦。四是数据备份。HBase提供了快照和复制功能。快照适合做例行备份复制适合跨机房容灾。至少配置一个定时快照策略防患于未然。五是监控告警。至少监控HMaster状态、RegionServer状态、HDFS使用率、ZooKeeper会话数、RegionServer的GC时间。用自带的Web UI应急可以但是长期跑一定要有外部监控和告警机制。六是参数固化。把最终调优的参数配置提交到Git里记录修改时间和原因。我见过不少团队一台机器挂了重建时参数没保留恢复后性能下降一大截找半天才发现是配置丢了。七是客户端连接管理。生产环境写入量大的时候底层的连接池必须管理好。之前遇到过某业务系统使用HBase连接池不当每次请求都新建连接把ZooKeeper会话数打满集群直接被拖到拒绝服务。11. 写在最后几个值得长期保持的习惯HBase这套生态说复杂也复杂说简单也简单。装起来一两个小时真正难的是把它用稳、用明白。我在实际使用中最深的体会是——排错时先看日志调优时先看内存布局设计时先看RowKey。这三个顺序如果颠倒了大概率越调越乱。最后再分享一个实用技巧每次HBase升级或者迁移前导出一份当前表的Schema定义。HBase没有那种一键导出的命令但可以用describe命令逐表导出配合Shell脚本批量生成。迁移时先建表、再BulkLoad数据能节省大量时间。这个建议是我在一次跨集群迁移中踩了坑之后总结的当时只顾着迁移数据忽略了表参数结果新集群跑出来的性能跟旧集群差了一大截回头才发现TTL、VERSIONS、COMPRESSION这些配置全都丢了。如果你打算在生产环境认真用HBase先从一台测试机把它完整跑起来然后把上面的坑都踩一遍——不是诅咒你踩坑这些坑是几乎一定会遇到的。提前知道它们长什么样对系统稳定性会有本质的帮助。