ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

HBase学习实战笔记:从核心概念到RowKey设计与Java API应用

HBase学习实战笔记:从核心概念到RowKey设计与Java API应用 HBase这块我前前后后啃了一个多月从懵懵懂懂看概念到在自己的笔记本上把伪分布式环境跑起来再到用Java API写增删改查中间踩了不少坑。这篇学习笔记就当是给自己做个总结也希望能给正在学大数据、尤其是接触HBase的朋友一些参考。我尽量把“为什么这么做”也写清楚而不是只贴命令和代码。1. 先搞清楚HBase到底是什么以及它凭什么在大数据圈里有一席之地很多初学者上来就背“HBase是一个高可靠、高性能、面向列、可伸缩的分布式数据库”这句话每个字都认识但连起来就不知道在说什么。我个人的理解是想象你有一张极其巨大的Excel表行数多到上亿甚至几十亿普通Excel打开就卡死MySQL这种关系型数据库要么存不下要么读写慢得让人崩溃。HBase就是专门为这种场景设计的“超级大表”它有几千几万台机器一起干活数据拆成很多份分布在这些机器上对外却仍然像一张表一样提供服务。1.1 从“面向列”这个特性说起“面向列”这个词很容易误解以为它是把数据竖着存。其实准确的说法是“面向列族”HBase里最核心的存储单元是列族Column Family一个列族里可以有成百上千个列。传统关系型数据库是“行”为单位的一行数据必须一起写、一起读HBase在存储时同一列族的数据物理上会放在一起如果你只查询某一个列根本不需要把整行都读出来。这就好比你去自助餐厅传统数据库是“必须买整份套餐”HBase是“想吃什么菜就单独拿什么菜”节省了大量I/O开销。另外一个关键点是稀疏存储。传统数据库如果一行有10个字段你只填了3个另外7个往往要占空间或填NULLHBase里不存在的列不会占用任何物理空间这对实际业务中字段严重稀疏的场景特别友好。1.2 HBase在技术栈里的定位学习HBase一定要先理顺它在整个大数据生态里的位置。HDFS是底层分布式文件系统负责海量数据存储但它有个明显短板不支持随机读写只能追加写入想做实时更新基本没戏Hive是数据仓库工具跑的是离线批量分析底层依然是MapReduce或Spark你查一条数据可能要等几分钟而HBase就填补了“海量数据 实时随机读写 低延迟”这块空白。如果你手头的数据只有几百GB查询又需要多表关联、复杂事务那HBase不是合适的选择MySQL或者PostgreSQL更省心如果你的数据量到了PB级需要毫秒级随机查询单条数据同时对并发写入要求很高那HBase几乎是绕不开的方案。网约车订单轨迹存储、电商用户行为日志查询、物联网设备上报数据还有像阿里、美团内部的很多数据中台系统都是HBase的典型应用场。再说说HBase的架构四个核心角色HMaster、RegionServer、ZooKeeper和Region。HMaster管的是“元数据”和“调度”比如表级别的增删改、Region的分配与均衡真正干活的是RegionServer每台RegionServer上可以挂很多Region每个Region管理一张大表的一部分数据ZooKeeper负责协调比如HMaster的选举、RegionServer上下线通知。你可以粗浅地理解成HMaster是项目经理只管安排任务和记录台账RegionServer是一线工人具体的读写操作都是工人干的ZooKeeper则是公司内部的通知系统谁来了谁走了大家都得知道。有个概念特别重要数据真正存储在Region里而Region底层是HFileHFile是存储在HDFS上的。也就是说HBase不自己存文件文件全部借住在HDFS上。这样设计的好处是数据天然多副本、高可用HDFS的datanode挂了影响也不大。2. 开始动手HBase安装与配置的完整复盘安装HBase是新手的第一道坎。网上教程很多但版本组合五花八门一不小心就在环境依赖上折腾一整天。这一步我反复装了三次才彻底理清下面把关键过程和我踩过的坑都记录下来。2.1 环境准备与版本搭配装HBase之前必须先有Java环境同时最好有Hadoop环境因为HBase要连ZooKeeper、要把数据落到HDFS上。版本搭配非常讲究用官方二进制包时Java和Hadoop必须都兼容。我这里用的是Hadoop 3.3.x搭配HBase 2.4.xJava版本选的JDK 8这组搭配经过大量实践验证最稳。JDK 11或17也能用但在某些组件上可能遇到模块限制问题没必要给自己找麻烦。如果你只是单机学习不需要单独安装独立的ZooKeeper集群HBase自带ZooKeeper配置参数可以启用内置的但如果生产环境建议单独部署ZooKeeper集群毕竟它也是HBase高可用的关键角色。2.2 配置文件的修改细节解压安装包后重点改两个文件conf/hbase-env.sh和conf/hbase-site.xml。前者要设置JAVA_HOME同时建议把HBASE_MANAGES_ZK设为true单机学习时让HBase自己管理ZooKeeper后者是整个配置的核心最基础的内容大概是这样的configuration property namehbase.rootdir/name valuehdfs://localhost:9000/hbase/value /property property namehbase.cluster.distributed/name valuetrue/value /property property namehbase.zookeeper.quorum/name valuelocalhost/value /property property namehbase.zookeeper.property.dataDir/name value/home/hadoop/zookeeper-data/value /property /configuration有几个容易被忽略的细节值得专门说。第一hbase.rootdir写的是HDFS路径如果你只是想让HBase存本地文件做体验可以改成file:///home/hadoop/hbase-data但这样就体验不到它对HDFS的依赖了还是建议把Hadoop搭起来第二hbase.cluster.distributed虽然叫“集群分布式”但单机伪分布式也要设为true把它理解为“是否依赖HDFS和ZooKeeper”更准确第三hbase.zookeeper.property.dataDir最好改成非临时目录否则重启机器后元数据丢了HBase会各种诡异报错。还有一个全局配置hbase.regionserver.handler.count它决定每个RegionServer能同时处理多少RPC请求默认30。学习环境不用动但在实际项目里你需要根据业务并发量调大或调小这个参数直接关系到高并发下的响应速度。2.3 启动验证和端口说明启动前先确保Hadoop的NameNode和DataNode都活着然后执行start-hbase.sh等十几秒后访问HBase自带的Web界面。我整理了一份常见的端口清单这个列表是我自己在排查问题时反复用到的端口用途16010HMaster的Web UI在这里能看到RegionServer列表、表列表、Region分布16030RegionServer的Web UI能看到单个RegionServer的请求量、缓存命中率、Region详情2181ZooKeeper端口客户端连接HBase时要先连这里16020RegionServer的RPC端口Java客户端或HBase Shell正是通过它读写数据16000HMaster的RPC端口主要用于管理操作第一次装完后我卡在了一个特别低级的问题Web UI能打开但Shell里执行list命令报连接异常。排查了半天才发现是防火墙没关端口或者是ZooKeeper的数据目录被清了。这类问题后面放到第五部分统一说。启动完毕之后建议先跑一下hbase shell执行status看到类似1 active master, 1 backup masters, 1 servers的输出就说明环境基本通了。3. 从设计到落地表结构设计与数据操作的完整思路环境通了之后真正考验人的是表和RowKey的设计。很多人在这一步开始犯迷糊因为没有现成的SQL语法可以参考一切都得自己设计。我选择用一个简单但贴近实际场景的例子——网约车订单轨迹表来把整个逻辑串起来。3.1 数据模型和几个绕不开的基本概念HBase一条数据的完整定位需要四个维度行键RowKey、列族Column Family、列限定符Qualifier就是列名、时间戳Timestamp。RowKey是每行数据的唯一标识查询时最快的方式就是直接根据RowKey去Get同一RowKey下的数据可以有多个版本通过时间戳区分默认保留最近三个版本。建表语句非常简洁核心就是指定列族比如create trip_order, info, location这就创建了一张名为trip_order的表它有两个列族info和location。列族创建之后还能修改比如调整TTL、压缩算法但列族数量一旦多了会有性能问题官方建议不超过2到3个。设计表的第一步是定列族不要想着像MySQL那样建几十个字段而是把字段按“访问特性”和“存储特性”划进少数几个列族里。3.2 RowKey设计这是HBase的灵魂RowKey的设计直接影响读写性能这是我学习过程中印象最深的一点。最核心的原则有三个唯一性、散列性、长度控制。唯一性容易理解散列性是为了防止数据热点。举个例子如果直接用车牌号当RowKey那同一辆车的所有订单都落在同一个Region上一旦某辆车高频上报位置这个Region的写入压力会特别大而其他Region闲着。解决方法通常是在RowKey前面加盐Salt或哈希前缀比如把车牌号的hashCode取模后拼在前面变成0a_京A12345_20240101103000这样的格式长度则是要尽可能短RowKey太长会让HFile的索引和内存占用都上去除非业务确实需要否则不要超过一百字节。查询模式也要在设计RowKey时提前想好。因为RowKey是字典序存储的如果你要查某辆车某天的所有轨迹可以设计成md5(车牌)前几位_车牌_日期这样同一辆车同一时段的数据在物理上是连续的Scan的效率会非常高。相反如果你把时间戳放在RowKey最前面然后Scan全表那就意味着查询要走全表扫描设计等于失败了。3.3 Shell环境下的常用数据操作HBase Shell是学习时最直观的练习工具常用的命令最好全部亲手敲一遍我总结了下面几个典型操作# 建表预分区方式可以提前指定Region数量 create trip_order, {NAME info, VERSIONS 3}, {NAME location, VERSIONS 3} # 插入一条数据 put trip_order, rowkey001, info:driver_id, 2001 put trip_order, rowkey001, location:lat, 39.9042 put trip_order, rowkey001, location:lng, 116.4074 # 查询单行 get trip_order, rowkey001 # 按区间扫描指定行键范围 scan trip_order, {STARTROW rowkey001, ENDROW rowkey010} # 删除 deleteall trip_order, rowkey001 # 修改列族的版本数 alter trip_order, {NAME info, VERSIONS 5}这里有个细节HBase Shell删除一行默认删除的是最新的那个时间戳版本deleteall才是删整行。Shell还会把类型信息显示出来比如columninfo:driver_id, timestamp...这个timestamp就是数据写入时自动生成的版本号。预分区是个很容易被忽略但极其有用的操作。如果你创建表时不指定默认只有1个Region数据量增加到一定程度会自动分裂但在自动分裂的过程中可能会有短暂的性能抖动而且热点问题往往从初期就埋下了。所以生产环境建表我习惯在create时直接用SPLIITS [rowkey001, rowkey002, ...]或者NUMREGIONS 20来指定预分区让数据一开始就打散分布。3.4 我在表设计阶段踩过的坑第一次做表设计时我犯过一个现在看来很幼稚的错。当时给一张用户行为日志表设计了5个列族理由是“不同业务线的字段分开管理”。结果Badge里每个列族都会单独生成对应的存储文件多个列族意味着在读写一行数据时要访问多个不同的文件RegionServer的压力成倍增加。后来我们压缩到2个列族性能明显改善。另一个经常出现的坑是过度设计。有人为了“考虑未来扩展”把本来属于同一个业务实体的数据分散到多张表里结果查询时要跨表拼数据。HBase没有原生join能力跨表关联非常难受要么用MapReduce或Spark做离线加工要么在应用层做多路查询。在设计之初就要想清楚这张表是“服务查询”的还是“服务分析”的两种场景的表结构很可能完全不一样。4. 用Java操作HBase从连接客户端到增删改查的实战记录学习HBase的最终落点一定是要能写代码因为生产环境永远是用程序去读写HBaseShell只用来做维护和快速验证。这一部分我完整记录了一个最基础的Java客户端开发过程包含依赖、连接、建表、插入和查询以及我实际开发中总结的规范。4.1 引入依赖和建立连接如果你的项目是Maven工程引入HBase客户端依赖非常直接dependency groupIdorg.apache.hbase/groupId artifactIdhbase-client/artifactId version2.4.17/version /dependency建立连接时传统写法是每次操作都创建一个Connection这是绝对的反模式。因为HBase的Connection底层维护了RPC连接池、ZooKeeper会话等重量级资源创建一次的成本很高。正确做法是全局只创建一个Connection实例在整个应用生命周期里复用用完之后由应用进程退出时统一关闭。这一点和数据库连接池的思路一致。Configuration public class HBaseConfig { Bean public Connection hBaseConnection() throws IOException { Configuration conf HBaseConfiguration.create(); conf.set(hbase.zookeeper.quorum, localhost); conf.set(hbase.zookeeper.property.clientPort, 2181); return ConnectionFactory.createConnection(conf); } }4.2 建表和写入数据的代码实现建表用Admin操作DML用Table操作两者源头都是Connection。下面是一段建表和插入数据的示例public void createTable(Connection connection, String tableName, String... columnFamilies) throws IOException { Admin admin connection.getAdmin(); TableName tn TableName.valueOf(tableName); if (admin.tableExists(tn)) { admin.disableTable(tn); admin.deleteTable(tn); } TableDescriptorBuilder builder TableDescriptorBuilder.newBuilder(tn); for (String cf : columnFamilies) { ColumnFamilyDescriptor cfd ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(cf)).build(); builder.setColumnFamily(cfd); } admin.createTable(builder.build()); admin.close(); } public void putRow(Connection connection, String tableName, String rowKey, String cf, String col, String value) throws IOException { Table table connection.getTable(TableName.valueOf(tableName)); Put put new Put(Bytes.toBytes(rowKey)); put.addColumn(Bytes.toBytes(cf), Bytes.toBytes(col), Bytes.toBytes(value)); table.put(put); table.close(); }这里的坑有几个。第一Bytes.toBytes这个工具类几乎是所有操作的必经之路HBase的所有读写API都是byte[]类型提交字符串之前要做字节转换第二这里的putRow方法一次只插一列实际业务中强烈建议把多列拼到一个Put对象里一次提交因为一次网络RPC和多次网络RPC的开销差距明显第三大量写入时建议使用BufferedMutator它就相当于HBase客户端的write buffer攒够一批再发出去。数据量大时千万不要一行一行put尤其是循环里new Table性能会指数级下降。正确做法是复用一个Table实例批量把Put对象add到一个List里每攒到1000条或1MB左右提交一次这样RegionServer也能更高效地批量写HFile。4.3 查询代码和过滤器使用查询分两类精确查询也就是Get和范围/条件查询也就是Scan。精确查询是按RowKey直接用Get这是HBase最快的查询方式建议线上接口尽量走这条路径。public String getValue(Connection connection, String tableName, String rowKey, String cf, String col) throws IOException { Table table connection.getTable(TableName.valueOf(tableName)); Get get new Get(Bytes.toBytes(rowKey)); Result result table.get(get); Cell[] cells result.rawCells(); if (cells ! null cells.length 0) { Cell cell cells[0]; return Bytes.toString(CellUtil.cloneValue(cell)); } return null; }Scan的灵活之处在于配合过滤器使用。最常用的是RowKey范围扫描setStartRow和setStopRow、列值过滤器SingleColumnValueFilter、行前缀过滤PrefixFilter。但需要记住一个原则能通过RowKey范围解决的问题不要用过滤器。过滤器相当于在RegionServer端逐行判断如果过滤条件很弱它会拖慢整个查询。HBase二面面试也喜欢在这里挖坑“Filter查询快吗”答案是不一定性能取决于你的rowkey设计能不能最大程度裁剪要扫描的数据范围。4.4 开发中的几个经验心得我在写Java客户端时最常犯的错误是忘关资源。createTable里的Admin调用完了要close但真正的高并发场景里反复开关Admin反而是浪费正确思路是同一个Connection派生的轻量资源尽量复用重量级资源由容器管理生命周期。此外连接参数里有个zookeeper.retries默认值偏大服务端ZooKeeper假死时客户端会一直重试导致请求堆积。我习惯把它调整为3次超时时间设置短一些宁可快速失败也不要让上游接口挂死。5. 常见问题排查和面试高频知识点整理最后这部分我把实际操作中最常见的问题和面试喜欢覆盖的知识点放在一起说因为它们本质上是一回事搞懂了问题背后的原理面试题自然就通了。5.1 环境部署阶段的典型报错和处理思路我遇到的第一个大问题是执行start-hbase.sh后HMaster进程起来了但几秒钟后又自动退出。日志里报的却是ZooKeeper连接超时。排查过程供参考第一先看hbase-site.xml里hbase.zookeeper.quorum配置是否正确我最初写成了机器的主机名而/etc/hosts里没做映射解析不了改成localhost立刻就好了第二确认ZooKeeper数据目录有写权限如果你用root启动过集群再用hadoop用户启动就会因为目录权限不一致起不来第三查看logs目录下的hbase-hadoop-master-xxx.log这个日志文件基本能定位90%的问题。第二个高频问题Shell能连上但读写时报RegionServer is not online。这个大概率是RegionServer进程没有正常启动或者系统负载太高导致RegionServer宕机后又注册不上。查看Web UI里的RegionServer列表找不到节点就说明进程挂了。处理办法是清掉ZooKeeper里遗留的HBase元数据重启服务。但注意清元数据一定要先停掉HBase否则可能造成数据元信息错乱。5.2 HBase读写性能问题快速定位实际使用中经常遇到“写入很慢”或者“查询很慢”的反馈。我的排查思路是分三步走第一步看RegionServer的Web UI关注Num. of requests这个指标确认是读写请求量太大导致线程池繁忙还是本身耗时长第二步看Region Count和Storefile Count如果单个RegionServer上的Region太多超过几百个说明负载不均衡或者预分区不合理第三步看缓存命中率也就是BlockCacheHitRatio命中率低于70%通常意味着Scan扫了太多不需要的数据或者BlockCache设得太小。调整方式一个是增大hfile.block.cache.size但这段空间是从堆内存里划分的给得太多会影响MemStore写入需要找到平衡点另一个是优化查询逻辑让Scan尽可能命中与RowKey前缀相同的连续数据。大数据量下的查询卡顿也和大内存GC相关。HBase是Java进程堆内存里既要存MemStore写入缓冲区又要存BlockCache读缓存两者之间有个比例分配。默认配置是MemStore占40%BlockCache占40%如果你发现读多写少可以把BlockCache调高一些反过来写多读少就调低。不要指望HBase自动帮你优化这些参数在生产环境必须手动调。5.3 面试高频问题速查学习HBase的过程中我顺手整理了几个高频面试问题的回答思路按照“起因、对比、血泪坑”的方式背下来几乎不会卡壳面试问题回答要点HBase和Hive有什么区别Hive是数仓工具跑离线批处理延迟高HBase是NoSQL数据库支持实时随机读写延迟低Hive不擅长单行查询HBase不擅长复杂分析为什么要用LSM树而不是B树B树写入需要随机I/O海量写入时磁盘性能跟不上LSM树先把写入放到MemStore内存中攒够再批量落盘成HFile把随机写变成顺序写写入吞吐量大幅提升RowKey设计怎么避免热点加盐、哈希前缀、反转RowKey目标都是让RowKey的字典序分布均匀避免大量读写落到同一Region上Region分裂和合并是怎么发生的Region变大到阈值后自动分裂成两个由HMaster管理多个Region数据量小且连续时会合并减少元数据开销HBase数据删除是真删除吗不是标记删除给数据加一个Delete类型的墓碑标记真正的物理删除要等Major Compaction之后才会发生MemStore刷写条件是什么达到hbase.hregion.memstore.flush.size默认128MB或者MemStore总大小占RegionServer堆内存比例超限或者WAL文件数量过多时也会触发刷写其中LSM树这个点值得多写几句。HBase写操作先把数据写进WAL预写日志再放到内存MemStore里这不纯粹是为了速度更重要的是保证数据不丢。机器突然断电时WAL能帮你恢复还没落盘的数据。理解了WAL就理解了为什么HBase在设计上是“写快读慢”写只要写一份顺序日志加一份内存读可能要查MemStore、多个HFile文件真实工程里经常需要引入布隆过滤器来加速读路径。5.4 学习路线和阶段划分建议我的建议是三个阶段递进。第一阶段能跑通环境会用Shell完成建表和增删改查理解Region、Store、MemStore几个核心概念第二阶段能用Java客户端完成常见读写操作理解RowKey设计、预分区优化、批量读写能自主排查连接和端口问题第三阶段深入源码或官方文档理解Region分裂合并流程、Compaction机制、HBase的备份与容灾方案。完成了前两个阶段应付日常的开发和初级面试基本没有问题。如果遇到看不懂源码的时候我的办法是抓主线而不是抓细节先弄明白一条Put请求从客户端到服务端经历了哪些类、哪些步骤其他一切都能串起来。结尾我个人在反复实操中最想提醒的三件小事这一路学下来我自己印象最深的不是那些宏大的架构概念反而是三个小到几乎被人忽视的细节一是Connection要复用任何代码里反复创建连接都是性能灾难的开始二是配好一套稳定的版本组合后就别再乱动HBase、Hadoop、JDK之间版本高高低低的坑足够让人陪上一整天三是遇到问题先看RegionServer和HMaster的日志不要盲目重启集群日志文件里往往直接写着原因。最后再分享一个小技巧学习时尝试给自己出一道综合题比如“模拟一个共享单车轨迹查询系统”从表设计、预分区、RowKey规则到Java读写接口全流程做一遍比看十篇教程都管用。把这个过程踩过的坑都记录下来这些比任何面试题答案都更有价值。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表