
很多人第一次接触 HBase都是被那句“分布式、列式、海量数据实时读写”吸引过来的。可真到了服务器上面对一行hbase shell不少人会愣住这玩意儿到底怎么用网上资料要么只丢几个命令要么一上来就讲源码原理中间缺了“能用起来”的那一层。这篇文章不打算重复官方文档而是从一个实际运维和开发者的角度把 HBase Shell 里那些高频命令、关键参数、还有不踩一遍根本发现不了的坑完整过一遍。1. 进入 Shell 之前版本差异、连接方式和退出姿势1.1 快速定位 Shell 入口与两种启动方式HBase Shell 本质是一个 JRuby 写的交互式客户端hbase命令装在 HBase 安装目录的bin/下。启动方式取决于 HBase 是以独立集群部署还是伪分布式跑在同一台机器上。# 最简单的方式进入默认集群 hbase shell # 指定集群的 Zookeeper 地址进入生产环境常用 hbase shell --config /etc/hbase/conf cn1,cn2,cn3:2181第二种方式适用于你手头有多套环境或者默认配置指向不对的情况。--config指定的是 HBase 客户端配置目录不是 HBase 的根目录里面要放hbase-site.xml。我见过有同事把这个参数直接指向 HBase 安装目录结果客户端读到的还是旧的 ZK 地址连了半天连不上。进入后会出现一段 JRuby 版本、HBase 版本的信息然后提示符变成hbase:001:0这之后再输入的才是真正的 HBase Shell 命令。另外要注意从 HBase 2.x 开始Shell 里支持用quit或exit退出前者直接退出进程后者会先执行一些清理逻辑。日常使用两者差异不大但脚本里建议用exit更规范。1.2 为什么我不建议一上来就执行安装类操作很多教程喜欢在讲 Shell 之前先铺垫 HBase 安装、Java 环境变量这些东西。但实操中Shell 命令和安装配置是两个完全独立的技能面。安装是系统管理员的事Shell 命令是开发、测试、运维日常都会用到的交互入口。如果你是开发角色完全不需要关心 RegionServer 是怎么启动的只需要知道集群连得上、hbase shell能进去就行。另一个原因Shell 命令是面向集群数据操作的你一旦执行建表、删表、flush、major_compact 这类命令影响的是整个集群不是本地文件。所以学习路径上先搞懂命令本身的语法和语义比先折腾安装更重要。安装搞错了可以重来数据搞没了就真的没了。1.3 help 命令和“忘词”时的查法Shell 命令记不住很正常关键是会查。HBase Shell 内置了按主题分类的 helphelp help ddl help dml help tools输出结果里每个命令附带了简要说明。比如help ddl会列出 create、alter、drop、disable、enable、list、describe 等命令。后面讲到具体命令时每个命令都可以用help 命令名再看详细参数比如help create help scan这是 Shell 自带的“离线文档”比翻网页快得多。我在实际使用中遇到参数记不清第一反应就是help scan而不是去搜文档。2. 建表与改表DDL 命令的关键参数和执行陷阱2.1 create / describe / alter 的基本用法建表是 HBase 里第一个要过的坎。看似简单的create其实牵扯到列族、版本数、TTL、预分区等多个决策点。最基础的写法create t_user, info这句的意思是在默认命名空间下创建一张名为t_user的表里面包含一个列族info。如果要多列族create t_user, info, detail但实际生产里我更建议初始只建一个列族后面再按需alter添加。因为列族数量影响 HBase 的 MemStore 和 Region 分裂行为多列族在底层是共享 Region 的某个列族数据量大时会连累同一个 Region 里的其他列族执行 flush 和 compaction。这句话要划重点不要为了设计上的整洁把列族拆得太细。建好之后用describe看表结构describe t_user它会返回表的所有参数、列族属性比如Table t_user is ENABLED t_user, {TABLE_ATTRIBUTES {METADATA {}} COLUMN FAMILIES DESCRIPTION {NAME info, BLOOMFILTER ROW, VERSIONS 1, TTL FOREVER, ...能看到VERSIONS版本数、TTL过期时间、BLOOMFILTER布隆过滤器类型、COMPRESSION压缩算法等关键属性。这里要特别注意describe输出的属性很多有默认值比如VERSIONS 1意味着同一行同一个单元格默认只保留最新一份数据。如果你需要保留历史版本必须在建表或改表时显式指定。改表用alter。最常见的操作是增加列族alter t_user, {NAME detail}修改列族的 TTL 或版本数alter t_user, {NAME info, VERSIONS 3} alter t_user, {NAME info, TTL 86400}再强调一次alter不需要先禁用表。HBase 2.x 支持在线修改表结构整个修改过程对读写是透明的。但在修改期间不推荐做大批量写入因为表状态异常会把客户端请求打挂。2.2 预分区Shell 命令里最容易被忽略的能力很多从关系型数据库转过来的同事建表时直接写create t_user, info然后以为完事了。等到数据量上来发现所有写入都压在少数几个 Region 上热点严重再做拆分就很被动。HBase 建表时是可以指定预分区的。Shell 里有两种常见写法。一种是指定固定数目的 RegionHBase 自动均分create t_user, info, {NUMREGIONS 10, SPLITALGO HexStringSplit}另一种是手动指定分区边界比如用十六进制字符串做 rowkeycreate t_user, info, {SPLITS [1000, 2000, 3000, 4000]}这就把 Region 边界从1000、2000、3000、4000处切开了rowkey 落在不同区间会进入不同 Region从而分散写入压力。还有一种更直观的写法是直接用SPLITS_FILE指向一个文件文件里每行是一个 split keycreate t_user, info, {SPLITS_FILE /tmp/splits.txt}实际经验是预分区数量不要拍脑袋。一般按照 Region Server 数量乘以 2 到 3 作为初始分区数即可比如 5 台 RegionServer预分区 10 到 15 个。分区太多会导致初始阶段每个 Region 数据量很少Region 数量管理开销变大太少则热点问题依旧。这一块的权衡比单纯记住create语法更重要。2.3 删除与禁用drop 之前必须先 disable 的原因drop命令删除表但它在执行前要求表必须先处于 disabled 状态。这是 HBase 设计上的一个保护机制一个被客户端正在写入的表直接删除会导致一堆请求落空、元数据错乱。所以正确的删除姿势是disable t_user drop t_user如果你不执行disable直接drop会看到类似下面的报错ERROR: Table t_user is enabled, so it cannot be dropped. Disable it first.这就不是“语法错误”而是状态检查不通过。反之如果想恢复表的使用enable t_user实际运维里我还遇到过一种情况disable命令发出后因为这张表正在进行 big compaction 或者有大量 flush 排队命令会长时间不返回。这时候不要急着重启客户端先看 RegionServer 日志确认表状态是否已切到 DISABLING。如果长时间卡在 DISABLING通常意味着还有 active 的写请求没处理完。这时候可以用status detailed去查看 Region 在线情况确认问题根源而不是反复执行 disable。2.4 版本与 TTLShell 侧最简单的数据治理手段HBase 的版本管理很多人理解成像 Git 那样的分支版本管理其实不是。它只是对同一个 rowkey 同一个列族的同一个列限定符保留最近 N 次写入的 value。默认 VERSIONS1新写入会直接覆盖旧值历史数据读不出来。如果你想知道某个单元格的历史变化可以在建表时给列族设置多个版本create t_audit, {NAME log, VERSIONS 5}这样同一单元格最多保留最近 5 次写入。查询时用get带{VERSIONS 5}就能取到多版本。TTL 则更实用。HBase 会在 major compaction 时根据 TTL 删掉过期数据或者查询时直接过滤。Shell 里设置 TTL 的单位是秒alter t_audit, {NAME log, TTL 86400}上面的命令让log列族的数据只存活一天。TTL 对生产环境非常友好比如日志表、行为表天然适合设置 TTL 来控制数据生命周期省去写定时任务删数据的麻烦。但这里面有坑TTL 不是精确到秒立刻删除的。它依赖后台 compaction 的触发实际生效时间可能比设置的 TTL 晚。如果你有非常严格的“数据过期必须立刻不可见”的需求应该在应用侧自己做时间戳过滤不能只依赖 TTL。3. 数据读写put、get、delete 的细节与批量操作3.1 put 的写入逻辑与时间戳参数put是 HBase 写入的基本单元。用法put t_user, u_001, info:name, zhangsan put t_user, u_001, info:age, 25这条命令的含义是往表t_user里rowkey 为u_001那行写列info:name值为zhangsan。如果u_001这行之前不存在它会直接创建如果之前存在就更新这个单元格。这里有个容易忽略的参数时间戳。put可以显式指定版本时间戳put t_user, u_001, info:name, lisi, 1650000000000但日常开发中我不建议手动指定时间戳。因为 HBase 是按时间戳判断版本新旧和 TTL 的如果手写的时间戳和真实时间差异过大会产生两种后果时间戳偏早数据可能被 TTL 直接判定过期写入成功后读不到时间戳偏晚数据可能“提前”出现在未来某个时间点导致恢复或查询错乱。除非你确实在做数据迁移需要保留原始写入时间否则别碰这个参数。关于put还有一个常见误区put不支持同时写多个单元格要么用多个put要么用后面会讲的putall实际上 HBase Shell 里没有putall这个命令我见过有同事被某些教程误导在 Shell 里敲 putall 报错。多个单元格需要多次 put。如果你有批量写入需求Shell 本身不是最佳工具应该用 Java API 的BufferedMutator或者 Spark/MapReduce 批处理。Shell 的 put 更适合测试验证不适合大数据量灌入。3.2 get 的列族、列限定符和版本读取get是读单行的核心命令。基础用法get t_user, u_001如果要读指定列get t_user, u_001, {COLUMN info:name}如果要读整个列族get t_user, u_001, {COLUMN info}多列用逗号分隔get t_user, u_001, {COLUMN [info:name, info:age]}get还有几个对排障很有用的参数。比如FILTERget t_user, u_001, {FILTER ValueFilter(, binary:zhangsan)}这是基于值的过滤可以帮你快速确认某一行的某个值是否符合预期。再比如指定时间范围get t_user, u_001, {TIMERANGE [1650000000000, 1651000000000]}TIMERANGE 的左闭右开语义容易记反务必注意。左边界是包含的右边界不包含也就是[start, end)。还有一个值得说的参数是VERSIONS。如果列族配置了多个版本get t_user, u_001, {COLUMN info:name, VERSIONS 3}它会把最近的 3 个版本都返回按时间戳倒序排列。这个操作在排查“数据为什么和我写入的不一样”时特别好用。3.3 delete / deleteall 与墓碑标记删除在 HBase 里不是一个即时的物理删除。Shell 的delete默认是删除某个单元格的最新版本而不是所有版本。delete t_user, u_001, info:name注意上面这条命令会删除info:name的最新版本但保留历史版本。如果你要删除整行deleteall t_user, u_001deleteall会删除这一行的所有列、所有版本。底层机制上删除操作会写入一个Delete类型的 Marker一般叫墓碑标记标记对应的 KeyValue 在读取时会被跳过但物理空间要等下一次 compaction 才会真正释放。这个机制的直接影响是删除后立刻查询数据可能读不到但表的存储空间并不会立刻变小。如果你用 deleteall 删了很多数据却发现 HDFS 空间没降这是正常的不必恐慌。等 major compaction 跑完后空间才会释放。由于这个原因生产环境里“删数据”一定要谨慎。如果不是特别必要设置 TTL 让它自然过期通常比 delete/deleteall 更安全、对集群的冲击更小。尤其是大量删除历史数据时可以用 TTL 让后台慢慢处理而不是一次性 deleteall 几十亿行。3.4 用 Shell 做小规模数据验证的几个思路虽然 Shell 不是批处理工具但在数据量不大、需要快速验证的场合它还是能顶一下。最常用的方法是循环调用 put。因为 HBase Shell 底层是 JRuby它支持简单的 Ruby 循环语法但不建议写太复杂。for i in 1..1000 do put t_test, row_ i.to_s, info:data, value_ i.to_s end上面的写法是直接在hbase shell里输入的它能跑但性能很差每 put 一次就是一次 RPC。如果你想造几万条测试数据这种方式会让你等到怀疑人生。更合适的做法是生成 HFile 然后 bulkload或者写一个简单的 Java/Python 程序用批量 API 灌数据。Shell 只承担“检查数据是否写入正确”的角色这比用 Shell 做数据灌入靠谱得多。验证时我习惯配合count命令看总行数count t_test默认会触发全表扫描数据量大时非常慢。可以用INTERVAL和CACHE参数控制count t_test, INTERVAL 1000, CACHE 1000INTERVAL表示每扫描多少行打印一次进度CACHE是每次 RPC 拉取的行数两者都能有效降低扫描压力。4. 扫描与过滤scan 的参数、过滤器与翻页技巧4.1 scan 的关键参数对照scan是 HBase Shell 里用法最丰富、也最容易出问题的命令。全表扫描是它的基础能力但生产环境几乎不会有人裸执行scan t_user因为数据量大时会扫描出所有 Region 的全部数据既慢又耗资源。常用参数如下参数示例含义STARTROW{STARTROW 1000}扫描起始 rowkey含该行STOPROW{STOPROW 2000}扫描结束 rowkey不含该行LIMIT{LIMIT 10}最多返回的行数COLUMN{COLUMN info:name}指定读取的列TIMERANGE{TIMERANGE [start, end]}按时间戳范围过滤VERSIONS{VERSIONS 3}返回多版本FILTER{FILTER PrefixFilter(abc)}配合过滤器使用最常见的场景是按 rowkey 范围扫描比如 rowkey 按时间前缀设计scan t_user, {STARTROW 20240101, STOPROW 20240102}这条命令扫出 20240101 这一天前缀的所有行STOPROW为什么是20240102而不是20240101因为 STOPROW 是不包含的边界。如果你写 STOPROW 20240101范围就是空集合一条都扫不出来。这个边界语义和前面 get 的 TIMERANGE 一样都要记牢。LIMIT参数对控制输出量很有用scan t_user, {STARTROW 20240101, STOPROW 20240102, LIMIT 100}它限制最多返回 100 行适合快速确认数据有没有写进去。注意 LIMIT 的作用是提前中断扫描而不是先扫完再截断所以对性能优化是有实际帮助的。4.2 过滤器在 Shell 中的写法与常见误区HBase Shell 支持通过字符串方式直接写过滤器格式是scan t_user, {FILTER RowFilter(, substring:zhang)}这句话的意思是筛选 rowkey 中包含zhang的行。RowFilter的运算符还有,,,,!等比较方式除了substring还有binary、binaryprefix、regexstring等。新手最容易犯的错误是把比较方式写错比如scan t_user, {FILTER RowFilter(, zhangsan)}这样会直接报错因为第二参数必须带比较器类型。正确写法是scan t_user, {FILTER RowFilter(, binary:zhangsan)}如果是列值过滤scan t_user, {FILTER ValueFilter(, substring:lisi)}ValueFilter是针对单元格的值做过滤。它和SingleColumnValueFilter的区别是ValueFilter只要某一列的值符合条件整行就会输出但所有列都会显示SingleColumnValueFilter是只针对指定列值过滤而且可以控制不匹配的行是否输出。比如下面这个写法scan t_user, {FILTER SingleColumnValueFilter(info, name, , binary:zhangsan)}SingleColumnValueFilter的参数顺序是列族、列名、运算符、值这个顺序在写长过滤条件时极容易弄混建议每次写之前先help scan里确认一遍。多个过滤条件用 AND 或 OR 连接scan t_user, {FILTER RowFilter(, substring:2024) AND ValueFilter(, substring:active)}这一条在数据抽查时很常用能快速定位符合多个条件的行。4.3 大批量 Scan 时的内存与超时控制Shell 里 scan 全表不指定任何参数看起来好像没什么问题但在数据量大的表上执行极有可能把 RegionServer 搞得很紧张。原因是 scan 到每个 Region 都要占据一定堆内存来缓存结果如果 Region 数量多、数据又大多个 Region 并发拉取RegionServer 的 JVM 堆很快会被撑起来严重的会触发 Full GC 甚至 OOM。所以我给一个基本原则生产环境用 Shell 做 scan一定要带 STARTROW/STOPROW或者 LIMIT至少带一个。纯粹的全表 scan 应该留给运维在低峰期确认数据规模或者用专门的 MapReduce/Spark 任务来做而不是在 Shell 里裸敲。如果确实需要 scan 一张大表的一部分数据还可以调小每次 RPC 拉取的缓存行数避免单次请求返回太多数据。Shell 里对应的参数是CACHEscan t_user, {CACHE 100}数值大代表每次 RPC 返回更多行性能更好但内存更紧张数值小则反之。对于 Shell 交互式查询默认值足够脚本里批量 scan 时建议根据自己的堆内存大小手动设置。另外还有一个RAW true参数它会把 HBase 内部的数据原始条目也扫出来包括已经被打上删除标记但尚未 compaction 的数据主要用于排查底层数据问题不是常规查询手段别随便开。5. 运维向命令Region 状态、均衡器和辅助排障5.1 status 与 Region 分布查看Shell 不只是给开发者用的运维在排查集群健康状态时也会频繁用到它。排在第一位的是statusstatus status simple status detailedstatus默认输出当前集群的 RegionServer 数量、Region 总数、请求量等信息。status detailed输出更细会列出每个 RegionServer 上服务的 Region 列表、Store 数量等。实际排障时我经常先用status detailed看 Region 分布是否均匀。如果某些 RegionServer 的 Region 数量明显高于其他节点说明热点有可能已经形成或者均衡器没跑。这时候可以进一步看具体是哪张表的 Region 集中在哪个节点locate_region t_user, row_1000locate_region接收表名和 rowkey返回这个 rowkey 当前所在的 Region 名和 RegionServer 地址。这个命令在定位“某行数据到底在哪个节点上”时非常有用调试读写超时问题时第一步就是确认目标 Region 在哪个 RegionServer。5.2 均衡器与 Region 分裂的 Shell 入口HBase 的负载均衡是通过balance_switch、balancer这一组命令来控制的。Shell 里可以查询均衡器状态balance_switch true balance_switch falsebalance_switch true打开均衡器false关闭。默认情况下均衡器是打开的HBase 会定期检查 Region 分布把过载节点上的 Region 迁走。但注意打开均衡器不代表立刻重新分布它只是允许均衡器运行真正触发均衡还需要balancer命令手动干预或者等后台流程自动跑。balancerbalancer返回true说明均衡操作执行成功。但我在生产环境一般不主动执行balancer因为它在集群负载高峰期跑会导致 Region 迁移产生大量 RPC 和网络开销可能加剧抖动。如果集群确实不均衡建议在低峰期执行并且结合status detailed前后对比效果。Region 分裂是 HBase 的自动行为当单个 Region 超过阈值时会自动分裂。Shell 里没有直接的split命令用于自动管理但可以手动触发split t_user split 表名,Region名称前一种是让整张表的所有 Region 都重新评估后一种是指定某个 Region 手动分裂。手动分裂多用于针对热点 Region 的紧急干预。比如发现某个 Region 的读写流量极高可以手动把它切分成两个 Region让请求分流。但这个过程也是有代价的分裂瞬间 RegionServer 需要生成新的 Region 元数据对性能有一点影响。5.3 常见 Shell 报错与处理方法Shell 用多了总会遇到一些报错。这里把最常见的整理成一张表后面遇到可以直接对照报错特征常见原因处理方式UnknownHostException客户端无法解析 ZooKeeper 地址检查hbase-site.xml的hbase.zookeeper.quorumRegionServer is not onlineRegionServer 挂掉或正在重启status detailed确认节点状态等重启完成Table is disabled表处于禁用状态enable 表名恢复Table is enabled, cannot be droppeddrop 前没 disable先disable再dropNoSuchColumnFamilyException写入了不存在的列族describe 表名确认列族名ScannerTimeoutExceptionscan 时间过长租约过期适当调大客户端 scan 超时或分批扫描Master is initializingMaster 还在启动稍等重试或查看 Master 日志这里面ScannerTimeoutException值得单独展开一下。Shell 的 scan 默认租约时间是 60 秒如果你 scan 的数据量大或者服务端响应慢扫描时间超过租约就会被强制中断。这不是 Shell 能通过一条命令解决的需要改 HBase 服务端的hbase.client.scanner.timeout.period参数同时建议在应用侧改用全量扫描的 MapReduce/Spark 任务而不是无限调大超时。5.4 Shell 之外的一个小习惯最后分享一个我自己的习惯。虽然 HBase Shell 是交互式的但很多常用命令组合我会提前写成一个.hbase脚本文件通过hbase shell批量执行。比如巡检时用到的状态检查脚本echo status simple /tmp/hbase_check.hbase echo list /tmp/hbase_check.hbase hbase shell /tmp/hbase_check.hbase脚本里的每行就是一条 Shell 命令执行顺序从上到下。这种方式的好处是不需要打开交互式终端逐条输入也方便在多个集群之间复用。个人经验是任何操作类的 Shell 命令一旦你需要第二次做就值得把它写进脚本。我自己靠这个习惯省下了不少重复敲命令的时间也避免了手误在核心表上执行危险操作。HBase Shell 命令看起来只是一个个单词背后却是存储模型、版本机制、Region 管理、元数据一致性这一整套体系。很多时候一条命令报错暴露的不是语法问题而是你对这个体系某个环节的理解还有缺口。把这些命令放到大数据系统的完整链路里去理解而不是孤立地背参数会让你的使用效率提升一个档次。上面这些内容基本覆盖了我日常工作中绝大多数 Shell 操作场景希望能帮你少踩一些我当年踩过的坑。