十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HBase从入门到实战:架构原理、集群搭建与RowKey设计全指南

HBase从入门到实战:架构原理、集群搭建与RowKey设计全指南 1. HBase在Hadoop生态里的真实位置别把它当成普通的数据库学大数据的人迟早会遇到这样一个问题HDFS能存海量数据MapReduce能算海量数据但是想从海量数据里“秒查”某几条记录或者随手做几次随机读写这俩兄弟就都不太顶用了。HDFS是流式读取适合顺序扫描大文件让它去定位某个具体key等于在一整座图书馆里靠书名首字母人工翻卡片能翻但对实时响应没什么指望MapReduce更是批量计算模型启动一轮任务要几十秒完全不适合线上请求。这就是HBase被设计出来的原因它是Hadoop生态里那个专门解决“海量数据下随机读写”问题的组件。很多人第一次听到“HBase是一个数据库”时会自然地拿它跟MySQL、PostgreSQL做对比然后陷入困惑为什么它不支持SQL为什么要自己维护RowKey为什么列族不能随便加这些困惑的本质是没有理解HBase不是“关系型数据库的替代品”而是一个分布式的、面向列的、键值对存储系统它只服务特定场景。更准确地说它是BigTable的开源实现设计目标是在成百上千台廉价服务器上存储PB级数据同时保持读写延迟在毫秒级到几十毫秒级。这篇文章适合三类人一是正在做大数据的课程设计或毕业设计需要用HBase存业务数据的学生二是已经在用Hadoop集群、想把HBase接入现有数据链路的一线工程师三是准备大数据岗位面试需要把HBase原理和实战串起来的人。下面这些内容都是我在实际搭建和调优过程中一步步验证过的不是把官方文档抄一遍就完事。2. HBase的底层架构Master、RegionServer和Region到底怎么配合2.1 一个写入请求的完整旅程要理解HBase先得撕开它的分层结构。从顶往下看HBase集群有三个核心角色HMaster、RegionServer和ZooKeeper。HMaster不负责实际的数据读写它管的是表结构的增删改、Region的分配和迁移、以及故障时Region的重新分配真正扛住读写流量的是RegionServer每个RegionServer上挂着若干个RegionRegion才是一张表中按RowKey范围切分出来的数据分片。看一个写入请求的完整路径你就明白它们各干什么了。客户端发起PUT请求时先访问ZooKeeper拿到hbase:meta表所在的RegionServer地址接着从这个RegionServer读取meta表找到目标RowKey所在的Region落在哪个RegionServer上拿到真实地址后客户端带着数据直奔那台RegionServer。整个过程类似快递分拣先查总调度表再查片区归属表最后送到具体派送点。数据到达RegionServer之后写入处理要经过三个环节。第一步是顺序追加到WALWrite-Ahead Log也就是预写日志这一步保证即使RegionServer在数据落盘前宕机也能通过日志重放找回数据第二步是写入MemStore这是内存里的一块有序结构按RowKey排序第三步是在MemStore达到阈值后批量刷写成HFile落到HDFS上。这套流程的精妙之处是写入时只动内存和日志文件完全避开了随机磁盘I/O所以HBase的写性能才那么稳。注意客户端是直接跟RegionServer通信的读写路径上根本没有Master参与。Master挂了读写照常进行只是不能做建表、改表、Region迁移这类管理操作了。2.2 Region分裂与自动均衡Region是分布式扩展的基本单元。刚建好的表只有一个Region随着数据量增长当这个Region的大小超过预设阈值默认是10GB由hbase.hregion.max.filesize控制时它就会分裂成两个子Region这两个子Region会被HMaster分配到不同的RegionServer上实现负载均衡。这个设计使得HBase“天生会横向扩展”。你有10台RegionServer一张表默认就有多个Region分散在10台机器上查询和写入天然就是并行的。但是这里有一个很多新手容易踩的坑如果建表时不做预分区所有写入一开始都会砸向唯一那个Region使用初期会出现所谓的“热点Region”问题而这台RegionServer就成了性能瓶颈。这个问题的解法我在后面的表设计章节会详细讲。Region分裂还有一个隐藏的细节分裂操作是先在全内存里完成的两个子Region的元信息会被写入meta表而真正的数据文件HFile并不会立刻被物理拆分子Region引用父Region的数据文件等到下一轮Compaction文件合并时才彻底分开。这就是为什么你观察HDFS上的文件会发现分裂前后文件数量没有立刻翻倍。2.3 强一致性的来源WAL和MemStore我之前遇到不少面试者把HBase跟Cassandra、Riak这类“最终一致性”的NoSQL画等号这是个大误区。HBase是强一致性的一个数据写入成功后任何客户端立刻读都能读到最新值。这个强一致性依赖两层保障。第一层是WAL所有变更先落日志再进内存即使宕机也不丢已确认的写入。第二层是HFile的不可变性刷写后的HFile不会被修改更新数据是写入新的HFile再通过Compaction合并旧文件。读取时RegionServer会依次检查MemStore和所有相关HFile取版本号最新的记录。因为没有并发修改同一份数据文件的问题HBase天然规避了大量分布式一致性难题这就是它能在架构上做到强一致的原因。3. 从零搭建HBase环境单机、伪分布式与集群的关键差异3.1 部署模式选择和端口清单自己动手装HBase之前要先想清楚装哪种模式。单机模式Standalone下HBase所有进程跑在一个JVM里数据存放在本地文件系统不涉及HDFS和ZooKeeper适合入门体验API伪分布式Pseudo-Distributed下HBase的各个角色分散成独立进程跑在同一台机器上数据放到HDFS上ZooKeeper也由HBase自己托管适合在开发机上模拟真实集群完全分布式则是由多台服务器组成集群适合生产或毕业设计演示。这里有必要列一下HBase的端口清单排查问题时候你会感谢自己的端口用途16010HMaster的Web UI新版默认端口旧版本是16010部分发行版为6001016030RegionServer的Web UI可查看每个Region的状态和存储信息2181ZooKeeper客户端连接端口HBase依赖它做协调16020RegionServer的RPC通信端口客户端数据读写走这里16000HMaster的RPC通信端口管理操作走这里如果你发现HBase的Web UI打不开八成不是HBase的问题先检查防火墙有没有放开16010和16030再确认HDFS的50070NameNode UI是否正常。这是环境搭建中最高频的故障点。3.2 安装配置的关键步骤假设你已经有一台装好了JDK 8、配好了SSH免密、跑着HDFS的Linux机器。接下来安装HBase的流程是这样的下载与Hadoop版本兼容的HBase二进制包并解压。版本的兼容性非常关键HBase 2.x和Hadoop 3.x之间有对应关系表版本错配会让你在启动时遇到各种诡异的RPC报错。修改conf/hbase-env.sh设置JAVA_HOME同时关闭HBase自带的ZooKeeper管理或显式指定ZooKeeper地址。修改conf/hbase-site.xml核心配置项如下configuration !-- 设置数据存储目录 -- property namehbase.rootdir/name valuehdfs://localhost:9000/hbase/value /property !-- 开启分布式模式 -- property namehbase.cluster.distributed/name valuetrue/value /property !-- 这个配置需要格外谨慎设成true会导致本地文件系统被误用 -- property namehbase.unsafe.stream.capability.enforce/name valuefalse/value /property /configuration注意hbase.rootdir中的localhost:9000必须和你Hadoop的fs.defaultFS完全一致否则HBase会去连一个不存在的NameNode。很多人第一次启动失败都是因为这个地址没对齐。修改conf/regionservers文件把集群中所有RegionServer主机名逐行写入。如果是完全分布式还需要修改conf/backup-masters文件配置备用Master实现HMaster的高可用。执行bin/start-hbase.sh启动集群然后打开16010端口确认Web UI上Active Master正常。3.3 和ZooKeeper整合的细节ZooKeeper和HBase的整合是部署中最容易翻车的环节。HBase对ZooKeeper的依赖很深Region路由信息、Master选举、RegionServer的在线状态全部由ZooKeeper维护。如果你已经有独立的ZooKeeper集群需要在hbase-site.xml中指定property namehbase.zookeeper.quorum/name valuenode1,node2,node3/value /property property namehbase.zookeeper.property.clientPort/name value2181/value /property如果让HBase托管自己的ZooKeeper伪分布式的默认方式启动后你会看到HQuorumPeer进程它跟独立ZooKeeper的QuorumPeerMain进程不同排查问题时注意别认错。实践中的一个经验是生产环境务必使用独立ZooKeeper集群因为HBase自带的管理方式一旦ZooKeeper数据目录损坏恢复会非常麻烦而独立ZooKeeper集群可以单独重启、单独备份。3.4 安装后的验证启动完毕先别急着写代码用下面几条命令确认集群状态# 进入HBase命令行 hbase shell # 查看集群状态能看到RegionServer数量和版本信息 status detailed # 建一张测试表 create test, cf # 插入一条数据 put test, row1, cf:name, zhangsan # 全表扫描确认数据存在 scan test # 按行键读取单条数据 get test, row1如果status显示RegionServer数量为0最常见的三个原因依次是ZooKeeper地址配错、hbase.rootdir连不上HDFS、RegionServer所在主机的hostname映射缺失/etc/hosts没有配置相应条目。尤其是第三个分布式环境下机器之间靠主机名互相识别hostname解析不了RegionServer永远不会向Master成功注册。4. 表设计才是HBase性能的分水岭RowKey和列族细节4.1 RowKey设计的三条铁律如果面试官让你谈谈HBase实践经验你提RowKey设计基本就答对了方向。RowKey是HBase里最重要的设计决策它直接决定读写性能的上限。三条铁律一定要刻在脑子里第一条RowKey要保证唯一性。同一行数据的RowKey不能重复后写入的同RowKey数据会覆盖前一条。所以设计RowKey时必须携带唯一业务标识比如订单号、用户ID或时间戳组合。第二条RowKey要尽量散列。HBase按RowKey字典序排序存储并且按RowKey范围切分Region。如果RowKey是自增ID或有序时间戳那么新写入的数据全部落在最后一个Region上前面的Region闲置后面的Region过热这就是“热点”问题。解决办法是在RowKey前拼接散列值比如对用户ID做MD5取前4位或者使用反转后的手机号。我见过一个极端例子一个物联网项目用IMEI号直接做RowKey结果写入全部压在了一个Region上吞吐量从每秒五万掉到几千加前缀散列后马上恢复。第三条RowKey设计要迎合查询模式不能为了散列而散列。也就是说你希望怎么查就用什么前缀组织RowKey让目标数据落在相邻位置。典型场景是订单查询如果业务上一半请求是“查某个用户最近的订单”RowKey应该设计成用户ID反转 时间戳倒序这样同一个用户的数据顺序排列Scan时能顺序读取并快速截断。4.2 列族设计为什么官方建议列族越少越好HBase里列族是一个极其重要的物理概念。同一个列族下的数据存放在一起有自己的MemStore和HFile而不同列族的数据完全分离。官方文档明确建议列族数量通常不要超过3个实际最佳实践是1到2个。原因是多方面的。首先列族越多每个RegionServer持有的MemStore数量越多刷写内存的压力越大compaction时跨文件合并的成本越高其次如果一个列族数据量很大、另一个很小两个列族共用一份Region元数据Region分裂时两个列族都要跟着拆会造成无谓的IO放大最后列族一旦创建修改操作如增加列族会触发表级别的Region重新部署生产环境属于大动作尽量别干。所以建表前想清楚需要几个列族是正经事。4.3 预分区避免初期热点最有效的办法一张新表只有一个Region无论RowKey设计得多好写入初期所有请求都会打在这一个Region上。解决办法是建表时显式预分区一次性把表切成多个Region# 把表预分成4个Region边界分别为a、b、c create user_order, info, {SPLITS [a, b, c]}预分区数量怎么定经验公式是单Region的写入吞吐大约在每秒一万到数万行和机器性能正相关预估你的峰值写入速率除以单Region吞吐再留一倍余量就是合适的分区数。比如你预估峰值每秒10万行单Region撑2万行那就预先分10到20个Region。提示Region切分的边界要避开RowKey的高频前缀。比如RowKey前缀是用户ID的散列值0到9切分点可以设为3、6、9这样4个Region的负载比较平均。如果切分点选得太少并且接近反而会让部分Region空闲。5. Region高可用原理宕机恢复和分裂过程拆解5.1 RegionServer宕机后发生了什么RegionServer是HBase的“一线员工”它挂了以后HBase的自愈机制会被立即激活。整个过程可以分成下面四步第一RegionServer与ZooKeeper之间的会话断开ZooKeeper上的临时节点rs消失。这是故障被感知的起点通常30秒到3分钟不等取决于ZooKeeper的会话超时参数hbase.zookeeper.property.tickTime和zookeeper.session.timeout。第二HMaster监听到这个临时节点的消失从hbase:meta表中找到这台RegionServer上曾经托管的所有Region列表。第三HMaster把这些Region重新分配到其他存活的RegionServer。分配时有一个优先级的讲究如果新RegionServer的机器上有这些Region的HFile本地副本HDFS短期本地化机制帮助实现它会优先选择该机器减少远程读。第四新RegionServer加载这些Region同时重放WAL日志。WAL在HDFS上是按RegionServer分目录保存的新RegionServer会去原来的目录读取日志文件按照日志里记录的变更按Region归组把数据恢复到内存MemStore再对外提供服务。这一步完成之后这组Region就恢复可用状态了。整个过程基本能做到分钟级自动恢复。生产环境的经验是如果宕机时间太长超过10分钟先检查WAL重放是不是卡在HDFS的NameNode备份恢复上因为WAL重放需要频繁读取HDFSNameNode性能不佳会拖慢整个恢复流程。5.2 Region分裂流程从触发到元数据更新Region分裂的完整流程值得仔细讲一遍因为它是理解HBase自动负载均衡的关键也是面试题“Region分裂过程”的标准答案。第一步RegionServer定期检查Region的大小超过hbase.hregion.max.filesize默认10GB时触发分裂。触发点的RegionServer会在内存中创建一个分裂事务把原Region的起始RowKey和结束RowKey一分为二得到两个子Region的key范围。第二步在ZooKeeper的/hbase/replication和/hbase/region-in-transition节点中记录分裂状态其他组件能看到这个Region正处于分裂中。第三步父Region的数据文件HFile不会立刻被物理拆分。两个子Region各自创建引用文件.tmp文件指向父Region的HFile。这一步是分裂速度极快的原因只动元数据不复制数据。第四步更新hbase:meta表把父Region的行改成两个子Region的两行并清除父Region的引用。此时客户端就能路由到新的子Region了。第五步后台做异步的Major Compaction把父Region的HFile真正拆分成两个部分再删除引用文件和父Region的数据文件物理资源才算真正释放。如果你观察线上集群会发现Region分裂期间的读写性能会有一阵抖动。这是因为分裂瞬间有元数据更新和引用文件切换把那些对延迟极其敏感的业务流量安排在分裂窗口之外是有经验的运维才会做的细活。5.3 HMaster的高可用Active与Standby切换HMaster有单点风险但可以通过配置多个Master实现自动切换。配置方法前面已经提过在backup-masters文件里写入备用Master的主机名即可。主备Master之间通过ZooKeeper的分布式锁机制竞争Active状态。Active Master持有/hbase/master这个临时节点Standby Master会一直监听这个节点当Active Master宕机时临时节点消失Standby Master被唤醒尝试创建这个节点谁创建成功谁就成为新的Active Master。切换完成后新Master会做两件事一是扫描所有RegionServer收集在线Region的分配情况纠正不一致的元数据二是重新处理那些处于“过渡态”的Region比如之前正在分裂或迁移的Region。实际中我见过切换后出现Region卡在RITRegion In Transition状态的情况这时候可以用assign命令手动把Region指定到某台RegionServer上# 手动回到正常状态 assign region_encoded_name6. Java API实操一个订单查询场景的完整实现6.1 场景设定和表结构理论讲了这么多不上手写代码总觉得虚。这里我以一个典型的订单查询场景来演示Java API的使用一张订单表需要支持按用户ID查订单、按订单号查订单、以及分页浏览某个用户的订单。按照第4节的设计原则建表方案如下表名orders列族info存放订单基础信息、detail存放商品条目RowKeyreverse(userId)_reverse(orderTime)保证同一用户的订单物理相邻且最新订单排在最前建表时做预分区// 预分区按用户ID散列前缀划分这里划分为4个Region byte[][] splitKeys new byte[][] { Bytes.toBytes(2), Bytes.toBytes(5), Bytes.toBytes(8) }; admin.createTable(tableDescriptor, splitKeys);6.2 基础CRUD代码拆解初始化连接是第一步注意Connection是线程安全的整个应用共享一个实例就够不要每次操作都新建。一个很常见的性能杀手就是每次请求创建一个Connection会导致ZooKeeper连接数暴涨Configuration conf HBaseConfiguration.create(); conf.set(hbase.zookeeper.quorum, node1,node2,node3); conf.set(hbase.zookeeper.property.clientPort, 2181); Connection connection ConnectionFactory.createConnection(conf);写入一条订单数据Table table connection.getTable(TableName.valueOf(orders)); String rowKey reverseUserId _ reverseOrderTime; Put put new Put(Bytes.toBytes(rowKey)); put.addColumn(Bytes.toBytes(info), Bytes.toBytes(order_id), Bytes.toBytes(orderId)); put.addColumn(Bytes.toBytes(info), Bytes.toBytes(pay_status), Bytes.toBytes(PAID)); put.addColumn(Bytes.toBytes(detail), Bytes.toBytes(total_amount), Bytes.toBytes(99.5)); table.put(put);按RowKey直接查询Get get new Get(Bytes.toBytes(rowKey)); Result result table.get(get); byte[] payStatus result.getValue(Bytes.toBytes(info), Bytes.toBytes(pay_status));按用户ID前缀扫描这个用户最近的订单Scan scan new Scan(); // 起始RowKey为userId反转 最大时间戳 scan.withStartRow(Bytes.toBytes(reverseUserId _)); scan.withStopRow(Bytes.toBytes(reverseUserId _ 000000)); scan.setReversed(true); // 倒序扫描最新订单在前 ResultScanner scanner table.getScanner(scan); for (Result r : scanner) { // 逐行处理 } scanner.close();这里有个关键细节withStartRow和withStopRow的边界处理。HBase的Scan默认是前闭后开区间即包含起始行、不包含结束行。如果你习惯MySQL的between语义需要额外注意这一点否则查出来的数据会不知不觉多一行或少一行。6.3 批量写入和版本管理订单系统经常遇到批量入库的场景HBase的Table.put(ListPut)接口支持批量提交。批量时有一条经验值得记下多行写入要共用同一个Put列表减少网络往返但也要控制批次大小一次几千条是常见的合理范围太大容易撑爆RegionServer的RPC缓冲区。版本管理也是HBase一个独特的功能。每写入一个单元格可以指定版本号默认保留3个历史版本。读取时如果不带版本条件拿到的是最新版本// 写入带版本的数据 Put put new Put(Bytes.toBytes(rowKey)); put.addColumn(Bytes.toBytes(info), Bytes.toBytes(status), System.currentTimeMillis(), Bytes.toBytes(CREATED)); table.put(put); // 读取时获取所有版本 Get get new Get(Bytes.toBytes(rowKey)); get.readVersions(10); // 注意readVersions上限不能超过表定义的版本数 Result result table.get(get);这个特性在记录操作历史、审计日志类的场景里很有用但千万别在设计时依赖它做核心业务版本控制因为HBase的版本机制并不支持跨列的事务性版本读取复杂版本逻辑落到应用层维护会更可控。7. 性能调优方向与HBase高频面试题整理7.1 常见性能瓶颈和调参思路在实际使用中HBase集群性能问题通常出在三个方面。第一是JVM内存配置不合理。RegionServer默认堆内存一般设为机器物理内存的一半HBASE_HEAPSIZE但MemStore占堆的比例、刷写触发阈值、BlockCache的占比这三个参数直接影响读写平衡。写多读少的场景把hfile.block.cache.size调小比如0.15把MemStore比例调大读多写少的场景相反BlockCache可以给到0.4以上。第二是Compaction风暴。HBase后台的Minor Compaction和Major Compaction会占用大量CPU和磁盘I/O尤其是Major Compaction会把一个Region的所有HFile全部合并成一个大文件期间读写性能会明显下降。生产上通常会把自动Major Compaction关掉改到业务低峰期定时手动触发# 关闭某张表的自动Major Compaction alter orders, {METHOD table_att, CONFIG {hbase.hregion.majorcompaction 0}} # 手动触发Major Compaction major_compact orders第三是Region大小设置不当。Region太小分裂过于频繁元数据管理开销大Region太大单次分裂耗时变长宕机恢复时重放WAL的时间也变长。单Region的合理大小在10GB到20GB之间同时要结合单台RegionServer的Region总数控制一台RegionServer建议承载20到200个Region超过200个时管理成本会急剧上升。7.2 高频面试真题考点梳理根据我对近几年大数据岗位面试题的观察HBase相关的考点集中在下面这些话题上HBase和Hive的区别是必考题。一句话版答案Hive是数据仓库工具建立在HDFS之上擅长批量离线计算不支持随机读写HBase是分布式NoSQL数据库支持低延迟的随机读写但复杂计算能力弱。两者的定位不冲突实际架构中经常配合使用Hive算完结果导到HBase供前台查询。“HBase是怎么实现高可用的”也常考。答案分三点Master通过ZooKeeper做Active/Standby切换RegionServer宕机后Region自动迁移到其他机器并重放WALHDFS的快照和副本机制保证数据文件不丢。“HBase的读写路径”是原理题答的时候要把WAL、MemStore、HFile、BlockCache这几个组件串起来同时说明为什么写快、为什么读也不慢。“怎么解决热点问题”是实战题标准答案围绕RowKey散列、预分区、盐值Salting三种方式展开再加一个业务级的方案按时间分表。7.3 一个容易被忽视的运维细节最后分享一个我踩过的坑。HBase表被删除后它的数据文件不会立即从HDFS上消失只是被标记删除。如果你反复建表、删表会发现HDFS的“已用空间”只增不减。需要跑一次HDFS的垃圾回收机制才能彻底释放# 设置HDFS回收站保留时间为0随即可执行删除清理 hdfs dfs -expunge这不算什么高深技术但第一次遇到时确实会让人误以为是磁盘满了或者数据泄漏。希望对正在折腾HBase的你有点帮助少走几步弯路。
返回列表