HBase Java API 创建表实战:列族配置与报错排查

HBase Java API 创建表实战:列族配置与报错排查 头歌平台上这套 HBase 开发实训第一关的题目就叫创建表。很多同学看到这四个字第一反应是就这——建个表而已MySQL 里一句CREATE TABLE的事。但真动手写Java代码去操作HBase的时候卡住的人远比想象中多依赖拉不下来、连不上 ZooKeeper、API 用的是老版本的类、表建出来了但列族名对不上判题要求最后提交一次红一次。我前后带过几批人做这关也自己反复在不同的 HBase 版本上跑过这篇文章就把这一关从题目在考什么到代码怎么写到报错怎么查完整拆一遍。不管你是刚接触 HBase 的在校生还是从 MySQL 转过来想摸一摸列式存储的后端只要手上有一个能跑起来的 HBase 环境跟着走一遍就能把这道题吃透顺便把 HBase Java API 的建表这条线彻底打通。1. 这道创建表到底在考什么先把题目看明白再动手1.1 HBase 的数据模型决定了建表方式和 MySQL 完全不同很多人上手 HBase 建表时最不适应的一点是它没有 MySQL 那种字段的概念。MySQL 建表你得先定死列名、列类型、长度、约束表结构一旦定下来改起来要么改表要么加字段HBase 反过来建表的时候你只需要定两样东西表名以及一个到几个列族Column Family。至于列族里面到底有哪些列、每列的什么类型建表阶段完全不用管数据写进去的时候现场决定就行。这个差别不是设计者偷懒而是存储模型决定的。HBase 底层是稀疏的、按行键排序的多维映射表每一行的列都可以不一样物理上数据是按列族分开存在不同的 HFile 里的。所以列族是物理存储的最小单位一旦建表就固定了后面能改的只有参数比如版本数、TTL不能随便加列族或者删列族——加列族要 disable 表删列族得先把数据清掉。理解了这一点你就能明白为什么这道题只让你创建表而不是像 SQL 那样让你写一堆字段定义HBase 的建表本质上是在规划物理存储的容器不是在描述业务字段。列族的命名也有隐性的坑。头歌的判题脚本通常是拿固定的列族名去比对的常见的是info、cf、family这类。你代码写得再漂亮列族名写成了INFO大写或者多了个空格表建出来了但判题不认一样不过。所以动手之前一定要把题干里的表名和列族名逐字符抄下来别凭记忆写。1.2 Java 操作 HBase 的链路全貌在写代码之前脑子里得有一条完整的链路否则报错的时候你不知道该往哪一层看。Java 客户端操作 HBase 大致是这样的你的程序先通过Configuration加载配置主要是 ZooKeeper 的地址然后ConnectionFactory拿着配置去建立一条Connection。这条连接不是直连 HBase Master 的而是先连 ZooKeeper从 ZooKeeper 里拿到hbase:meta表所在的 RegionServer 位置之后所有对表的读写请求都走这条链路找到对应的 RegionServer。Connection拿到之后你要操作表结构建表、删表、加列族就得从它那儿取一个Admin对象要读写数据则取Table对象。这两个是最常用的入口也是这一关的核心建表用的是Admin不是Table。我见过有人拿着Table对象到处找建表方法找不到就以为类导错了其实方向从一开始就偏了。还有一层容易被忽略Connection是重量级对象内部维护了线程池和到各个 RegionServer 的连接缓存创建一次的开销不小。官方文档明确说了它是线程安全的一个应用里应该共享一个实例用完再关。新手最常见的写法是在每个方法里ConnectionFactory.createConnection(conf)一次跑几个测试用例下来连接数暴涨运气不好直接触发服务端的连接数限制。这个习惯在单机练习里看不出来一上规模就是事故。1.3 判题脚本真正在检查什么头歌这类实训平台的判题逻辑说白了就是跑一段测试代码然后断言几个条件。这一关的测试代码十有八九是这么干的先调用你写的方法然后用Admin去tableExists(TableName)返回 true 就算过再讲究一点的会拿admin.getDescriptor(tableName)把表的描述符取出来检查列族集合里有没有题干要求的那个名字甚至检查列族的版本数是不是指定的值。这意味着两件事。第一你建表的方法必须有实际的副作用不能只是打印一句话或者返回一个 true测试代码会真的去服务端查。第二表名和列族的任何细节都不能自作主张题干写student你就不能写成Student题干写列族info你就不能顺手加个data进去多出来的列族有些判题脚本是允许的有些会直接判错稳妥起见严格按题干来。还有一个实用的判断如果题干给了模板代码模板里通常已经把Configuration、Connection、Admin的创建写好了留一个空方法体给你填。这时候你要看清方法签名——参数是传进来的Admin还是需要你自己建连接。前者你只需要写建表那几行后者你还得管资源关闭。看错这一步后面全白写。2. 开工前的准备依赖、配置和参数错一个都跑不起来2.1 Maven 依赖怎么选版本为什么不能乱搭HBase 的 Java 客户端依赖不算多但版本这块特别容易出事。核心依赖是hbase-client它里面包含了Admin、Connection、TableDescriptorBuilder这些类。需要注意的是hbase-client会自动传递引入 Hadoop 和 ZooKeeper 的相关包如果你自己又在pom.xml里手动加了不同版本的hadoop-common两套版本一撞NoSuchMethodError就会在运行时冒出来。一个稳妥的写法是只声明hbase-client让 Maven 自己解析传递依赖同时把服务端的 HBase 版本对齐dependency groupIdorg.apache.hbase/groupId artifactIdhbase-client/artifactId version2.4.11/version /dependency版本选择上有几个关键判断。第一客户端大版本要和服务端一致服务端是 2.x客户端就别用 1.x 的依赖虽然通常兼容但 API 差异会让你的代码根本编译不过。第二HBase 2.x 需要 JDK 8 及以上1.x 在老版本 JDK 上没问题但如果在 JDK 11 或 17 上跑 1.x 的客户端拿不到反射权限会抛IllegalAccessError或者InaccessibleObjectException。第三日志实现别重复hbase-client会带上 slf4j你自己再引一个slf4j-simple或者log4j启动时会刷一屏警告虽然不影响功能但排查真正的错误时会被噪音淹没。顺便说一个细节hbase-client的TableDescriptorBuilder是 2.0 之后才有的1.x 时代用的是HTableDescriptor。这个差异是这一关最容易踩的雷我在第 3.4 节会单拎出来讲。2.2 hbase-site.xml 和 Configuration 的加载方式代码里连不上 HBase九成问题出在Configuration上。标准写法是Configuration conf HBaseConfiguration.create(); conf.set(hbase.zookeeper.quorum, localhost); conf.set(hbase.zookeeper.property.clientPort, 2181);HBaseConfiguration.create()内部会去 classpath 里找hbase-site.xml找到就加载。所以有两种路子一种是把服务端的hbase-site.xml直接拷到项目的src/main/resources下代码里就不用再 set 任何东西另一种是代码里手动 set ZooKeeper 地址和端口。头歌的实训环境通常两种都支持但如果你在本地做我建议用前者因为hbase-site.xml里除了 ZooKeeper 地址还有一堆客户端参数重试次数、超时时间、RPC 超时手动 set 容易漏。手动 set 的时候有两个坑。第一个是属性名容易写错hbase.zookeeper.quorum里是zookeeper不是zk端口属性是hbase.zookeeper.property.clientPort注意property这个词。第二个是端口默认值不设置 clientPort 的话默认是 2181如果你的 ZooKeeper 改了端口又没显式配置连接会超时然后报KeeperException$ConnectionLossException。连接超时也是可以调的。练习环境网络没问题一般不用管但如果服务端刚启动、还在恢复 region前几次连接可能失败可以在配置里加上hbase.client.retries.number默认 15和hbase.rpc.timeout默认 60000 毫秒。这些参数的作用是给客户端更多的重试机会而不是让错误消失——如果有重试之后还是失败说明环境本身有问题。2.3 建表之前必须想清楚的三件事第一件事是表名和命名空间。不指定命名空间的话表会建在default下TableName.valueOf(student)和TableName.valueOf(default, student)是等价的。如果题干要求建到某个自定义命名空间里你必须先admin.createNamespace()否则会抛NamespaceNotFoundException。命名空间这东西在练习里不常用但在做多租户隔离的时候是标配。第二件事是列族数量和名字。列族不是越多越好每个列族在底层至少对应一个 MemStore 和一组 HFile列族多了写入时的刷盘次数、压缩开销都成倍增长。实际项目里一张表超过三个列族就该重新审视设计了绝大多数场景一到两个列族就够。这一关通常只要求一个列族。第三件事是列族的关键参数maxVersions决定每个单元格保留几个历史版本默认是 1timeToLiveTTL单位秒决定数据多久后过期默认是HConstants.FOREVER也就是永不过期minVersions和keepDeletedCells配合 TTL 用控制过期时的清理行为。这些参数在创建列族的时候就能指定改起来也不算麻烦但建表时就定好能省很多事。3. 核心 API 逐个拆把每一行代码的意图弄明白3.1 Connection 和 Admin别把它当成随手 new 的对象Connection的正确用法是这样的Connection connection ConnectionFactory.createConnection(conf); Admin admin connection.getAdmin();这里没有new Connection()这种写法必须走ConnectionFactory工厂方法。从 2.0 开始HConnectionManager这类老入口已经被标记为废弃甚至移除了网上很多老教程还在用直接抄会编译不过。Admin是从Connection拿的它也不是线程安全的多个线程共用同一个Admin实例做建表删表操作可能出现状态错乱。但实际使用中建表这类操作频率极低一般就在启动流程里跑一次所以不用太纠结并发问题重点是把关闭顺序做对先关Admin再关Connection。反过来先关Connection的话Admin持有的引用会变成悬空状态后续如果还有调用就会抛异常。JDK 7 之后的对象都实现了AutoCloseable所以最干净的写法是用 try-with-resourcestry (Connection connection ConnectionFactory.createConnection(conf); Admin admin connection.getAdmin()) { // 建表逻辑 }这样不管中间是否抛异常两个资源都会被正确释放。头歌的判题脚本一轮跑下来要建好几个表如果资源没关干净后面的用例可能会因为连接被占满而失败——这种第一关过了第二关挂了的情况经常就是资源泄漏导致的。3.2 TableName 与命名空间的正确构造方式TableName是 HBase 2.x 里表示表名的唯一方式不要再用字符串直接拼表名传参了绝大多数 API 早就只接受TableName对象TableName tableName TableName.valueOf(student);带命名空间的写法是TableName.valueOf(myns, student)或者用拼接形式TableName.valueOf(myns:student)。注意冒号是分隔符不能出现在命名空间名或表名本身里命名空间和表名各自只允许字母、数字、下划线、点号和短横线。有一个很容易被忽略的点TableName的equals和hashCode是按字节比较的大小写敏感。所以要判断表是否存在必须用同一个TableName对象或者内容完全一致的字符串否则tableExists永远返回 false你就会一直重复建表然后一直抛TableExistsException。命名空间这一块创建语法是admin.createNamespace(NamespaceDescriptor.create(myns).build());如果命名空间已经存在会抛NamespaceExistException。稳妥的做法是先查一遍try { admin.getNamespaceDescriptor(myns); } catch (NamespaceNotFoundException e) { admin.createNamespace(NamespaceDescriptor.create(myns).build()); }这种先查后建的模式在 HBase 里到处都是建表、建命名空间都适用养成习惯能省掉很多反复调试的时间。3.3 TableDescriptorBuilder 和 ColumnFamilyDescriptorBuilder 的完整用法2.x 里建表的核心是两个 Builder一个描述表本身一个描述列族。顺序上先把列族描述符建好塞进表描述符最后交给admin.createTable()TableDescriptorBuilder tableBuilder TableDescriptorBuilder.newBuilder(TableName.valueOf(student)); ColumnFamilyDescriptor family ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(info)) .setMaxVersions(3) .build(); tableBuilder.setColumnFamily(family); admin.createTable(tableBuilder.build());几个必须注意的细节。ColumnFamilyDescriptorBuilder.newBuilder()的参数是byte[]所以要用Bytes.toBytes(info)转换直接传 String 编译不过。setColumnFamily()可以调用多次来加多个列族但同一个列族名加两次会覆盖。TableDescriptorBuilder还有一些可选设置比如setMaxFileSize单个 region 的最大文件大小超过就 split默认 10GB、setMemStoreFlushSizeMemStore 刷盘阈值默认 128MB练习场景不用动生产环境要根据数据量和机器配置调。除了maxVersions列族上常用的还有ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(info)) .setMaxVersions(3) // 保留 3 个历史版本 .setTimeToLive(7 * 24 * 60 * 60) // 7 天后过期单位秒 .setMinVersions(1) // 至少保留 1 个版本 .setCompressionType(Compression.Algorithm.SNAPPY) .setBloomFilterType(BloomType.ROW) .build()这里的每个参数背后都有取舍。maxVersions和 TTL 是一对搭档只设 TTL 不设minVersions过期数据在 compaction 时会被清掉但如果你还指望查历史版本就得把minVersions也设上让这部分数据豁免过期。Compression用 SNAPPY 在压缩率和 CPU 开销之间比较平衡但前提是服务端装了对应的本地库没装的话建表不会失败写数据时才报错。BloomFilter用 ROW 类型能显著提升按行键查询的性能代价是多占一点存储读多写少的表值得加。3.4 1.x 和 2.x 的 API 分水岭别把两套代码混着写这是这一关最容易翻车的地方。HBase 2.0 做了一次大重构建表相关的 API 几乎全换了用途HBase 1.x 写法HBase 2.x 写法表描述符new HTableDescriptor(TableName)TableDescriptorBuilder.newBuilder(TableName)列族描述符new HColumnDescriptor(info)ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(info))添加列族desc.addFamily(hcd)builder.setColumnFamily(cfd)建表admin.createTable(htd)admin.createTable(TableDescriptor)判断列族存在desc.hasFamily(Bytes.toBytes(info))desc.hasColumnFamily(Bytes.toBytes(info))如果你的pom.xml引的是 2.x用的却是左边那套类名编译阶段就会报找不到符号。反过来引了 1.x 却用右边的写法也一样过不去。看到网上教程的时候先确认它的 HBase 版本不要无脑抄。我个人建议新项目一律上 2.x头歌的环境现在也基本都是 2.x。遇到 1.x 的老代码HTableDescriptor那套看懂就行不用照着写。另外 2.x 还提供了AsyncAdmin这套异步 API建表这类操作也有异步版本返回CompletableFuture适合在启动流程里并发做多张表的初始化但这一关用不上先把同步版本写熟。4. 从零写一份能跑通这一关的完整代码4.1 完整示例建表 幂等判断 资源释放把头歌模板里那几行填成能跑通的代码我一般会写成下面这样。核心思路是先判断表是否存在存在就不重复建不存在再建这样不管判题脚本调用几次都不会出问题import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.HBaseConfiguration; import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.client.Admin; import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor; import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; import org.apache.hadoop.hbase.client.Connection; import org.apache.hadoop.hbase.client.ConnectionFactory; import org.apache.hadoop.hbase.client.TableDescriptor; import org.apache.hadoop.hbase.client.TableDescriptorBuilder; import org.apache.hadoop.hbase.util.Bytes; public class HBaseCreateTable { public static void createTable(String tableNameStr, String familyName) throws Exception { Configuration conf HBaseConfiguration.create(); conf.set(hbase.zookeeper.quorum, localhost); conf.set(hbase.zookeeper.property.clientPort, 2181); try (Connection connection ConnectionFactory.createConnection(conf); Admin admin connection.getAdmin()) { TableName tableName TableName.valueOf(tableNameStr); if (admin.tableExists(tableName)) { System.out.println(表已存在 tableNameStr); return; } ColumnFamilyDescriptor family ColumnFamilyDescriptorBuilder .newBuilder(Bytes.toBytes(familyName)) .setMaxVersions(3) .build(); TableDescriptor descriptor TableDescriptorBuilder .newBuilder(tableName) .setColumnFamily(family) .build(); admin.createTable(descriptor); System.out.println(建表成功 tableNameStr 列族 familyName); } } public static void main(String[] args) throws Exception { createTable(student, info); } }这份代码里有几个设计是刻意的。createTable方法接收表名和列族名两个参数不写死方便按题干修改也方便你自己测多个表。tableExists判断放在建表之前让整个方法变成幂等的——判题脚本多跑一次不会因为表已存在而抛异常。资源用 try-with-resources 包住方法正常返回或者抛异常都能释放。日志只打印必要信息不用System.out.println刷一大堆中间状态判题时输出太乱也不好看。4.2 进阶版带预分区和 TTL 的建表如果这一关的题干额外要求预分区或者你自己想练手代码要往上加一层。预分区的意思是建表的时候就把 region 的起始行键切好避免所有数据一开始都挤在一个 region 里等它自动 split 的时候产生抖动。API 上是给createTable多传一个byte[][]参数byte[][] splitKeys new byte[][] { Bytes.toBytes(1), Bytes.toBytes(2), Bytes.toBytes(3), Bytes.toBytes(4) }; ColumnFamilyDescriptor family ColumnFamilyDescriptorBuilder .newBuilder(Bytes.toBytes(info)) .setMaxVersions(1) .setTimeToLive(30 * 24 * 60 * 60) .build(); TableDescriptor descriptor TableDescriptorBuilder .newBuilder(TableName.valueOf(student)) .setColumnFamily(family) .build(); admin.createTable(descriptor, splitKeys);这段代码建出来的表会被预先切成 5 个 region4 个分割点产生 5 段。几个注意点splitKeys必须按字典序升序排列顺序错了会抛IllegalArgumentException数组里不能有重复元素空数组new byte[0][]会因为长度为零被当成不分区和直接调不带splitKeys的版本效果一样。另外admin.createTable()还有个可以指定 region 数量的重载但那个版本内部是用随机行键生成分割点的生产环境不推荐可控性太差。TTL 参数的单位是秒写30 * 24 * 60 * 60比直接写2592000可读性好得多。但注意这个数字是int类型如果 TTL 超过 68 年左右会溢出正常场景不用担心。4.3 用命令行和 Web UI 验证表是否真的建好了代码跑完不算完得确认表真的落到了 HBase 上。命令行是最直接的方式先进入 hbase shellhbase shell然后执行list describe studentlist会列出所有表能看到student就说明建表成功。describe student会打印表描述符重点看COLUMN FAMILIES DESCRIPTION那一段里面会列出列族名、MAX VERSIONS、TTL、COMPRESSION这些参数和你代码里设的对不对一眼就能看出来。有时候表建好了但列族名字不是自己预期的就是这一步发现的。Web UI 也很有用。HBase Master 默认的 Web 端口是 16010浏览器访问http://localhost:16010能看到表列表、region 分布、RegionServer 状态。页面里点进表名还能看到每个 region 的起止行键预分区有没有生效一目了然。ZooKeeper 的端口是 2181Master 和 RegionServer 之间的 RPC 端口是 16000 和 16020这几个端口在排查连接问题时经常要用来测连通性telnet localhost 2181 telnet localhost 16000能连上说明端口通连不上就是服务没起来或者端口被改了。5. 报错速查这些异常我基本都遇到过5.1 常见异常对照表把这一关可能遇到的异常整理成一张表对号入座能省不少时间异常信息大概率原因处理方向NoClassDefFoundError: org/apache/hadoop/hbase/...依赖没引全或版本冲突检查pom.xml只保留hbase-clientNoSuchMethodError客户端与服务端大版本不一致对齐 HBase 版本IllegalAccessError/InaccessibleObjectExceptionJDK 版本过高老客户端反射受限降到 JDK 8 或换客户端版本KeeperException$ConnectionLossException连不上 ZooKeeper检查 quorum、端口、服务状态MasterNotRunningExceptionHBase Master 没起来启动 HBase查 master 日志TableExistsException表已存在还重复创建加tableExists判断NamespaceNotFoundException指定了没创建的命名空间先建命名空间或用 defaultTableNotFoundException建表后立刻按表名查元数据还没同步重试或稍等再查IllegalArgumentException: Split keys must be sorted预分区行键没排序按字典序升序排列这张表里最值得说的是NoSuchMethodError和IllegalAccessError这两个。它们共同的特点是编译能过运行才炸因为编译时看到的是你本地依赖里的类签名运行时加载的可能是传递依赖里另一个版本的同名类。排查方法是在项目根目录跑mvn dependency:tree把 HBase、Hadoop、ZooKeeper 相关的依赖链条打印出来看看有没有同一个 artifact 出现两个版本。有的话用exclusions把多余的排掉。5.2 我踩过的几个坑第一个坑是表名带命名空间但忘了加引号。在 hbase shell 里describe myns:student如果不加引号冒号会被 shell 解析成别的意思报语法错误。这个和 Java API 无关但验证阶段经常卡住人。第二个坑是本地 hosts 没配。在单机伪分布式环境里hbase-site.xml里配的 hostname 如果和hosts文件里的解析对不上代码里看着用的是localhost实际解析到了别的地址连接就超时。排查方式很简单ping一下配置里写的那个主机名看解析出来的 IP 对不对。第三个坑是误以为admin.createTable()是异步的。它不是。这个方法会阻塞到 Master 完成建表并返回所以方法返回之后立刻describe是能看到的。但如果你在分布式集群上、RegionServer 很多Master 分配 region 需要一点时间这时候立刻去读写可能会遇到NotServingRegionException等一两秒重试即可。第四个坑是列族名用了中文或者特殊字符。Bytes.toBytes(info)传中文不会报错表也能建出来但后面各种工具显示和判题比对都会出问题。老老实实用小写字母和下划线。5.3 一套固定的排查顺序遇到跑不通的时候我一般按这个顺序过一遍基本能定位到问题先确认 HBase 服务本身是活的。浏览器打开http://localhost:16010能看到页面说明 Master 正常再用hbase shell敲个status能看到集群状态说明整体没问题。再确认网络能通。telnet localhost 2181和telnet localhost 16000这是客户端到 ZooKeeper 和 Master 的必经之路。然后看代码里的配置。hbase.zookeeper.quorum是不是写成了localhost或者真实主机名端口是不是 2181。接着看依赖。mvn dependency:tree确认没有版本冲突。最后看代码本身。TableName、列族名、API 版本是不是和题干一致。这个顺序的核心逻辑是从外到内环境问题永远优先于代码问题因为环境不通的时候你看什么代码都是错的。6. 把这一关的经验往生产推建表这件事远不止创建这么简单6.1 行键设计才是 HBase 表设计的真正核心这一关只让你建表没让你设计行键但真实项目里行键设计的重要性远超列族参数。HBase 的数据是按行键字典序排列的行键设计得好读的时候能精确命中一个 region设计得差热点全集中在一个 region 上其他节点闲着看戏。常见的几个套路加盐salting在行键前面拼一个哈希前缀把数据打散到多个 region反转reversing把手机号、时间戳这类递增的行键反转避免写入都落到最后一个 region组合键把查询维度用固定分隔符拼起来比如用户ID_时间戳这样按用户查、按时间范围查都能走前缀扫描。预分区和行键设计是配套的。你切了 4 个分割点但行键全是连续递增的写入还是会全压到最后一个 region 上预分区就白做了。这两件事得一起想。6.2 列族数量、版本数和 TTL 的取舍原则列族这件事我在实践中总结了几条经验。列族数量控制在 1 到 3 个超过 3 个基本说明设计有问题通常是按访问频率或者数据大小分组而不是按业务字段分组。同一个列族里的数据访问模式应该相近因为一次读操作会把整个列族的数据块都拉进来。maxVersions的设置要看业务需求。绝大多数业务只需要最新版本设成 1 就行多留版本会显著增加存储和 compaction 压力。只有需要查历史快照、做审计或者时间序列分析的时候才设成 2 或 3。设成 10 以上要非常谨慎HBase 的压缩机制在处理多版本数据时效率会下降。TTL 是控制存储成本最直接的手段。日志类、监控类数据加上合理的 TTL能自动清理过期数据不用自己写定时任务。但要注意 TTL 是在 compaction 时生效的不是到点就立刻消失所以设了 7 天 TTL实际数据可能在第 8 天、第 10 天才被清掉取决于 compaction 的触发时机。如果需要精确的过期控制得配合minVersions和keepDeletedCells一起调。6.3 建表之后的收尾工作别漏了表建好只是开始后面还有一串事情要做。权限控制生产环境里通常要给不同应用分配不同的命名空间和权限grant命令或者AccessControlClient都能做练习环境一般用不上但要知道有这么回事。监控表建好之后要监控 region 数量、storefile 大小、读写延迟Master 的 Web UI 和 JMX 都能拿到这些指标。备份重要表要配置快照策略snapshot命令做全量快照配合ExportSnapshot同步到其他集群。还有一件事是清理测试表。练习环境里反复建表删表很容易留下一堆垃圾表删表要两步admin.disableTable(tableName); admin.deleteTable(tableName);先 disable 再 delete顺序不能反。表在 enabled 状态下直接删会抛TableNotDisabledException。删除命名空间之前要把里面的表全删干净否则也会失败。我在实际项目里遇到过一次因为建表时没设预分区上线一个月后写入量上来自动 split 频繁触发每次 split 都会造成短暂的写入延迟抖动业务方那边报了好几次超时告警。后来重建表按行键前缀切了 16 个 region问题直接消失。所以这一关虽然只是练习但建表时多想一步分区策略这个习惯带到生产环境里去能省掉很多事后救火的时间。如果你这一关已经跑通了下一关大概率是往表里插数据或者查询那时候会用到Table、Put、Get、Scan这几个对象思路和建表差不多都是从Connection拿入口、用完关掉。我建议在写下一关之前先回头把这一关的代码重写一遍不看书不看模板纯手写写完能一次跑通说明 HBase Java API 的这套模式你是真的记住了而不只是抄了一遍。